1515< script src ="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js "> </ script >
1616
1717< style >
18+ /* Light is default */
1819 : root {
20+ --bg : # fbfaf6 ;
21+ --surface : # ffffff ;
22+ --surface-2 : # f4f1ea ;
23+ --line : rgba (0 , 0 , 0 , 0.07 );
24+ --line-2 : rgba (0 , 0 , 0 , 0.15 );
25+ --ink : # 1c1c1f ;
26+ --ink-mute : # 6e6a64 ;
27+ --ink-dim : # a8a39c ;
28+ --sip : # 2563eb ;
29+ --dip : # ea580c ;
30+ --hyb : # b45309 ;
31+ --gain : # 15803d ;
32+ --loss : # b91c1c ;
33+ --serif : 'Fraunces' , Georgia, serif;
34+ --sans : 'IBM Plex Sans' , system-ui, sans-serif;
35+ --mono : 'IBM Plex Mono' , ui-monospace, monospace;
36+ --bg-grad-1 : rgba (180 , 83 , 9 , 0.04 );
37+ --bg-grad-2 : rgba (37 , 99 , 235 , 0.03 );
38+ }
39+
40+ [data-theme = "dark" ] {
1941 --bg : # 0d0d10 ;
2042 --surface : # 15151a ;
2143 --surface-2 : # 1c1c22 ;
2951 --hyb : # f5b961 ;
3052 --gain : # 7dd181 ;
3153 --loss : # ef6b6b ;
32- --serif : 'Fraunces' , Georgia, serif;
33- --sans : 'IBM Plex Sans' , system-ui, sans-serif;
34- --mono : 'IBM Plex Mono' , ui-monospace, monospace;
54+ --bg-grad-1 : rgba (245 , 185 , 97 , 0.06 );
55+ --bg-grad-2 : rgba (110 , 168 , 254 , 0.04 );
3556 }
3657
3758 * { box-sizing : border-box; }
4465 line-height : 1.5 ;
4566 -webkit-font-smoothing : antialiased;
4667 background-image :
47- radial-gradient (ellipse 80% 50% at 50% -20% , rgba (245 , 185 , 97 , 0.06 ), transparent 60% ),
48- radial-gradient (ellipse 60% 40% at 100% 100% , rgba (110 , 168 , 254 , 0.04 ), transparent 60% );
68+ radial-gradient (ellipse 80% 50% at 50% -20% , var (--bg-grad-1 ), transparent 60% ),
69+ radial-gradient (ellipse 60% 40% at 100% 100% , var (--bg-grad-2 ), transparent 60% );
70+ transition : background-color 0.2s , color 0.2s ;
4971 }
5072
5173 /* HEADER */
5274 header {
5375 padding : 48px 48px 24px ;
5476 border-bottom : 1px solid var (--line );
77+ position : relative;
78+ }
79+ .theme-toggle {
80+ position : absolute;
81+ top : 24px ;
82+ right : 24px ;
83+ width : 36px ;
84+ height : 36px ;
85+ border-radius : 50% ;
86+ border : 1px solid var (--line-2 );
87+ background : var (--surface );
88+ color : var (--ink );
89+ cursor : pointer;
90+ display : flex;
91+ align-items : center;
92+ justify-content : center;
93+ transition : background 0.15s , transform 0.15s ;
5594 }
95+ .theme-toggle : hover { background : var (--surface-2 ); transform : scale (1.05 ); }
96+ .theme-toggle svg { width : 16px ; height : 16px ; }
97+ [data-theme = "dark" ] .icon-moon { display : none; }
98+ : root : not ([data-theme = "dark" ]) .icon-sun { display : none; }
99+
56100 .eyebrow {
57101 font-family : var (--mono );
58102 font-size : 11px ;
68112 line-height : 1.05 ;
69113 letter-spacing : -0.02em ;
70114 margin : 0 0 8px ;
115+ font-variation-settings : "opsz" 48 ;
116+ font-optical-sizing : none;
71117 }
72118 h1 em {
73119 font-style : italic;
135181 -webkit-appearance : none;
136182 width : 100% ;
137183 height : 4px ;
138- background : var (--surface -2 );
184+ background : var (--line -2 );
139185 border-radius : 2px ;
140186 outline : none;
141187 }
256302 font-family : var (--sans );
257303 font-size : 12px ;
258304 }
259- table .cmp tr : hover td { background : rgba (255 , 255 , 255 , 0.02 ); }
305+ table .cmp tr : hover td { background : rgba (127 , 127 , 127 , 0.04 ); }
260306 table .cmp .pos { color : var (--gain ); }
261307 table .cmp .neg { color : var (--loss ); }
262308 table .cmp .big {
263309 font-size : 15px ;
264310 color : var (--ink );
265311 }
312+ table .cmp tr .delta-row td {
313+ border-top : 1px solid var (--line-2 );
314+ color : var (--ink-mute );
315+ font-size : 12px ;
316+ }
317+ table .cmp tr .delta-row td : first-child {
318+ color : var (--ink-dim );
319+ font-style : italic;
320+ }
266321
267322 /* CHART */
268323 .chart-wrap {
298353< body >
299354
300355< header >
356+ < button class ="theme-toggle " id ="theme-toggle " aria-label ="Toggle theme " title ="Toggle theme ">
357+ < svg class ="icon-moon " viewBox ="0 0 24 24 " fill ="none " stroke ="currentColor " stroke-width ="1.6 " stroke-linecap ="round " stroke-linejoin ="round "> < path d ="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z "/> </ svg >
358+ < svg class ="icon-sun " viewBox ="0 0 24 24 " fill ="none " stroke ="currentColor " stroke-width ="1.6 " stroke-linecap ="round " stroke-linejoin ="round "> < circle cx ="12 " cy ="12 " r ="4 "/> < path d ="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41 "/> </ svg >
359+ </ button >
301360 < div class ="eyebrow "> Backtest · 2000 → 2026</ div >
302361 < h1 > Buy the dip < em > vs</ em > just keep buying.</ h1 >
303362 < p class ="sub ">
@@ -737,15 +796,39 @@ <h2>Strategy comparison</h2>
737796 const v = ( Math . pow ( r . finalValue / r . invested , 1 / periodYears ) - 1 ) * 100 ;
738797 return fmtPct ( v ) ;
739798 } ;
740- const fmtXirr = ( r ) => {
799+ const xirrPct = ( r ) => {
741800 const x = xirr ( r . cashflows ) ;
742- return x == null ? '—' : fmtPct ( x * 100 ) ;
801+ return x == null ? null : x * 100 ;
802+ } ;
803+ const fmtXirr = ( r ) => {
804+ const v = xirrPct ( r ) ;
805+ return v == null ? '—' : fmtPct ( v ) ;
743806 } ;
744807 const fmtGain = ( r ) => {
745808 const g = r . finalValue - r . invested ;
746809 const pct = ( r . finalValue / r . invested - 1 ) * 100 ;
747810 const cls = g >= 0 ? 'pos' : 'neg' ;
748- return `<span class="${ cls } ">${ indianFmt ( g ) } <small>(${ pct . toFixed ( 0 ) } %)</small></span>` ;
811+ return `<span class="${ cls } ">${ indianFmt ( g ) } <small>(${ pct . toFixed ( 1 ) } %)</small></span>` ;
812+ } ;
813+
814+ // Δ vs SIP
815+ const sipFinal = sip . finalValue ;
816+ const sipXirr = xirrPct ( sip ) ;
817+ const fmtDeltaFinal = ( r ) => {
818+ if ( r === sip ) return '<span style="color:var(--ink-dim)">—</span>' ;
819+ const d = r . finalValue - sipFinal ;
820+ const cls = d >= 0 ? 'pos' : 'neg' ;
821+ const sign = d >= 0 ? '+' : '' ;
822+ return `<span class="${ cls } ">${ sign } ${ indianFmt ( d ) } </span>` ;
823+ } ;
824+ const fmtDeltaXirr = ( r ) => {
825+ if ( r === sip ) return '<span style="color:var(--ink-dim)">—</span>' ;
826+ const v = xirrPct ( r ) ;
827+ if ( v == null || sipXirr == null ) return '—' ;
828+ const d = v - sipXirr ;
829+ const cls = d >= 0 ? 'pos' : 'neg' ;
830+ const sign = d >= 0 ? '+' : '' ;
831+ return `<span class="${ cls } ">${ sign } ${ d . toFixed ( 2 ) } %</span>` ;
749832 } ;
750833
751834 const rows = [
@@ -755,33 +838,62 @@ <h2>Strategy comparison</h2>
755838 [ 'Absolute gain' , [ sip , dip , hyb ] . map ( fmtGain ) ] ,
756839 [ 'CAGR' , [ sip , dip , hyb ] . map ( fmtCagr ) ] ,
757840 [ 'XIRR' , [ sip , dip , hyb ] . map ( fmtXirr ) ] ,
841+ [ 'Interest on idle cash' , [ sip , dip , hyb ] . map ( r => r . interestEarned > 0 ? indianFmt ( r . interestEarned ) : '—' ) ] ,
758842 ] ;
759843
760- const idleRow = [
761- [ sip , dip , hyb ] . map ( r => r . interestEarned > 0 ? indianFmt ( r . interestEarned ) : '—' ) ,
844+ const deltaRows = [
845+ [ 'Δ Final vs SIP' , [ sip , dip , hyb ] . map ( fmtDeltaFinal ) ] ,
846+ [ 'Δ XIRR vs SIP' , [ sip , dip , hyb ] . map ( fmtDeltaXirr ) ] ,
762847 ] ;
763- rows . push ( [ 'Interest on idle cash' , idleRow [ 0 ] ] ) ;
764848
765- document . getElementById ( 'cmp-body' ) . innerHTML = rows . map ( ( [ label , vals ] ) => `
766- <tr>
849+ const renderRow = ( label , vals , cls = '' ) => `
850+ <tr${ cls ? ` class=" ${ cls } "` : '' } >
767851 <td>${ label } </td>
768852 <td>${ vals [ 0 ] } </td>
769853 <td>${ vals [ 1 ] } </td>
770854 <td>${ vals [ 2 ] } </td>
771855 </tr>
772- ` ) . join ( '' ) ;
856+ ` ;
857+
858+ document . getElementById ( 'cmp-body' ) . innerHTML =
859+ rows . map ( ( [ l , v ] ) => renderRow ( l , v ) ) . join ( '' ) +
860+ deltaRows . map ( ( [ l , v ] ) => renderRow ( l , v , 'delta-row' ) ) . join ( '' ) ;
861+ }
862+
863+ function chartTheme ( ) {
864+ const dark = document . documentElement . dataset . theme === 'dark' ;
865+ return dark ? {
866+ line : 'rgba(236,232,223,0.85)' ,
867+ sip : 'rgba(110,168,254,0.85)' ,
868+ dip : 'rgba(255,122,89,0.9)' ,
869+ hyb : 'rgba(245,185,97,0.95)' ,
870+ fg : '#ece8df' ,
871+ grid : 'rgba(255,255,255,0.04)' ,
872+ axis : 'rgba(255,255,255,0.15)' ,
873+ muted :'#8b857a' ,
874+ hoverBg : '#1c1c22' ,
875+ } : {
876+ line : 'rgba(28,28,31,0.78)' ,
877+ sip : '#2563eb' ,
878+ dip : '#ea580c' ,
879+ hyb : '#b45309' ,
880+ fg : '#1c1c1f' ,
881+ grid : 'rgba(0,0,0,0.06)' ,
882+ axis : 'rgba(0,0,0,0.18)' ,
883+ muted :'#6e6a64' ,
884+ hoverBg : '#ffffff' ,
885+ } ;
773886}
774887
775888function renderChart ( data , sip , dip , hyb ) {
889+ const t = chartTheme ( ) ;
776890 const xLine = data . map ( r => r . date ) ;
777891 const yLine = data . map ( r => r . close ) ;
778892
779893 const buyY = ( dates ) => dates . map ( d => {
780- // find closest data point — dates are already actual data point dates
781- const t = d . getTime ( ) ;
782- // simple linear search; data is small enough
894+ const ts = d . getTime ( ) ;
783895 for ( let i = 0 ; i < data . length ; i ++ ) {
784- if ( data [ i ] . date . getTime ( ) === t ) return data [ i ] . close ;
896+ if ( data [ i ] . date . getTime ( ) === ts ) return data [ i ] . close ;
785897 }
786898 return null ;
787899 } ) ;
@@ -790,25 +902,25 @@ <h2>Strategy comparison</h2>
790902 {
791903 x : xLine , y : yLine , type : 'scatter' , mode : 'lines' ,
792904 name : 'Nifty 50 TRI' ,
793- line : { color : 'rgba(236,232,223,0.85)' , width : 1.2 } ,
905+ line : { color : t . line , width : 1.2 } ,
794906 hovertemplate : '<b>%{x|%b %Y}</b><br>%{y:,.0f}<extra></extra>' ,
795907 } ,
796908 {
797909 x : sip . buyDates , y : buyY ( sip . buyDates ) , type : 'scatter' , mode : 'markers' ,
798910 name : `SIP buys (${ sip . buyDates . length } )` ,
799- marker : { color : 'rgba(110,168,254,0.85)' , size : 5 , symbol : 'circle' } ,
911+ marker : { color : t . sip , size : 5 , symbol : 'circle' } ,
800912 hovertemplate : 'SIP · %{x|%d %b %Y}<extra></extra>' ,
801913 } ,
802914 {
803915 x : dip . buyDates , y : buyY ( dip . buyDates ) , type : 'scatter' , mode : 'markers' ,
804916 name : `Dip buys (${ dip . buyDates . length } )` ,
805- marker : { color : 'rgba(255,122,89,0.9)' , size : 6 , symbol : 'diamond' } ,
917+ marker : { color : t . dip , size : 6 , symbol : 'diamond' } ,
806918 hovertemplate : 'Dip · %{x|%d %b %Y}<extra></extra>' ,
807919 } ,
808920 {
809921 x : hyb . dipDates , y : buyY ( hyb . dipDates ) , type : 'scatter' , mode : 'markers' ,
810922 name : `Hybrid dip buys (${ hyb . dipBuys } )` ,
811- marker : { color : 'rgba(245,185,97,0.95)' , size : 7 , symbol : 'triangle-up' } ,
923+ marker : { color : t . hyb , size : 7 , symbol : 'triangle-up' } ,
812924 hovertemplate : 'Hybrid-dip · %{x|%d %b %Y}<extra></extra>' ,
813925 visible : 'legendonly' ,
814926 } ,
@@ -817,25 +929,23 @@ <h2>Strategy comparison</h2>
817929 const layout = {
818930 paper_bgcolor : 'rgba(0,0,0,0)' ,
819931 plot_bgcolor : 'rgba(0,0,0,0)' ,
820- font : { color : '#ece8df' , family : 'IBM Plex Mono, monospace' , size : 11 } ,
932+ font : { color : t . fg , family : 'IBM Plex Mono, monospace' , size : 11 } ,
821933 margin : { l : 60 , r : 20 , t : 20 , b : 50 } ,
822934 xaxis : {
823- gridcolor : 'rgba(255,255,255,0.04)' , linecolor : 'rgba(255,255,255,0.15)' ,
824- tickcolor : 'rgba(255,255,255,0.15)' , zeroline : false ,
935+ gridcolor : t . grid , linecolor : t . axis , tickcolor : t . axis , zeroline : false ,
825936 } ,
826937 yaxis : {
827938 type : 'log' ,
828- gridcolor : 'rgba(255,255,255,0.04)' , linecolor : 'rgba(255,255,255,0.15)' ,
829- tickcolor : 'rgba(255,255,255,0.15)' , zeroline : false ,
830- title : { text : 'Nifty 50 TRI (log)' , font : { size : 11 , color : '#8b857a' } } ,
939+ gridcolor : t . grid , linecolor : t . axis , tickcolor : t . axis , zeroline : false ,
940+ title : { text : 'Nifty 50 TRI (log)' , font : { size : 11 , color : t . muted } } ,
831941 } ,
832942 legend : {
833943 orientation : 'h' , y : - 0.18 , x : 0 ,
834944 bgcolor : 'rgba(0,0,0,0)' , font : { size : 11 } ,
835945 } ,
836946 hoverlabel : {
837- bgcolor : '#1c1c22' , bordercolor : 'rgba(255,255,255,0.15)' ,
838- font : { family : 'IBM Plex Mono, monospace' , color : '#ece8df' } ,
947+ bgcolor : t . hoverBg , bordercolor : t . axis ,
948+ font : { family : 'IBM Plex Mono, monospace' , color : t . fg } ,
839949 } ,
840950 } ;
841951
@@ -858,6 +968,7 @@ <h2>Strategy comparison</h2>
858968 const sip = strategySip ( DATA , p ) ;
859969 const dip = strategyDip ( DATA , p ) ;
860970 const hyb = strategyHybrid ( DATA , p ) ;
971+ lastResults = { sip, dip, hyb } ;
861972 const periodYears = ( DATA [ DATA . length - 1 ] . date - DATA [ 0 ] . date ) / MS_YR ;
862973 renderTable ( sip , dip , hyb , periodYears ) ;
863974 renderChart ( DATA , sip , dip , hyb ) ;
@@ -870,8 +981,23 @@ <h2>Strategy comparison</h2>
870981 el . addEventListener ( 'input' , debouncedRecompute ) ;
871982 el . addEventListener ( 'change' , debouncedRecompute ) ;
872983 } ) ;
984+ document . getElementById ( 'theme-toggle' ) . addEventListener ( 'click' , ( ) => {
985+ const cur = document . documentElement . dataset . theme || 'light' ;
986+ const next = cur === 'dark' ? 'light' : 'dark' ;
987+ document . documentElement . dataset . theme = next ;
988+ try { localStorage . setItem ( 'theme' , next ) ; } catch ( e ) { }
989+ if ( DATA && lastResults . sip ) renderChart ( DATA , lastResults . sip , lastResults . dip , lastResults . hyb ) ;
990+ } ) ;
873991}
874992
993+ let lastResults = { sip : null , dip : null , hyb : null } ;
994+
995+ ( function initTheme ( ) {
996+ let saved = 'light' ;
997+ try { saved = localStorage . getItem ( 'theme' ) || 'light' ; } catch ( e ) { }
998+ document . documentElement . dataset . theme = saved ;
999+ } ) ( ) ;
1000+
8751001( async ( ) => {
8761002 try {
8771003 const { data, source } = await loadData ( ) ;
0 commit comments