@calculator53295/backtest-engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +161 -0
  3. package/dist/canonical/allocation.d.ts +64 -0
  4. package/dist/canonical/allocation.d.ts.map +1 -0
  5. package/dist/canonical/allocation.js +91 -0
  6. package/dist/canonical/allocation.js.map +1 -0
  7. package/dist/canonical/dates.d.ts +45 -0
  8. package/dist/canonical/dates.d.ts.map +1 -0
  9. package/dist/canonical/dates.js +86 -0
  10. package/dist/canonical/dates.js.map +1 -0
  11. package/dist/canonical/index.d.ts +12 -0
  12. package/dist/canonical/index.d.ts.map +1 -0
  13. package/dist/canonical/index.js +12 -0
  14. package/dist/canonical/index.js.map +1 -0
  15. package/dist/canonical/rebalance.d.ts +35 -0
  16. package/dist/canonical/rebalance.d.ts.map +1 -0
  17. package/dist/canonical/rebalance.js +56 -0
  18. package/dist/canonical/rebalance.js.map +1 -0
  19. package/dist/canonical/returns.d.ts +61 -0
  20. package/dist/canonical/returns.d.ts.map +1 -0
  21. package/dist/canonical/returns.js +65 -0
  22. package/dist/canonical/returns.js.map +1 -0
  23. package/dist/canonical/strategy.d.ts +62 -0
  24. package/dist/canonical/strategy.d.ts.map +1 -0
  25. package/dist/canonical/strategy.js +103 -0
  26. package/dist/canonical/strategy.js.map +1 -0
  27. package/dist/engine/align.d.ts +34 -0
  28. package/dist/engine/align.d.ts.map +1 -0
  29. package/dist/engine/align.js +97 -0
  30. package/dist/engine/align.js.map +1 -0
  31. package/dist/engine/backtest.d.ts +7 -0
  32. package/dist/engine/backtest.d.ts.map +1 -0
  33. package/dist/engine/backtest.js +95 -0
  34. package/dist/engine/backtest.js.map +1 -0
  35. package/dist/engine/frontier.d.ts +25 -0
  36. package/dist/engine/frontier.d.ts.map +1 -0
  37. package/dist/engine/frontier.js +40 -0
  38. package/dist/engine/frontier.js.map +1 -0
  39. package/dist/engine/index.d.ts +6 -0
  40. package/dist/engine/index.d.ts.map +1 -0
  41. package/dist/engine/index.js +5 -0
  42. package/dist/engine/index.js.map +1 -0
  43. package/dist/engine/metrics.d.ts +27 -0
  44. package/dist/engine/metrics.d.ts.map +1 -0
  45. package/dist/engine/metrics.js +202 -0
  46. package/dist/engine/metrics.js.map +1 -0
  47. package/dist/engine/types.d.ts +118 -0
  48. package/dist/engine/types.d.ts.map +1 -0
  49. package/dist/engine/types.js +2 -0
  50. package/dist/engine/types.js.map +1 -0
  51. package/dist/index.d.ts +25 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +25 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/montecarlo/data.d.ts +17 -0
  56. package/dist/montecarlo/data.d.ts.map +1 -0
  57. package/dist/montecarlo/data.js +38 -0
  58. package/dist/montecarlo/data.js.map +1 -0
  59. package/dist/montecarlo/simulate.d.ts +106 -0
  60. package/dist/montecarlo/simulate.d.ts.map +1 -0
  61. package/dist/montecarlo/simulate.js +264 -0
  62. package/dist/montecarlo/simulate.js.map +1 -0
  63. package/dist/retirement/bucketRules.d.ts +3 -0
  64. package/dist/retirement/bucketRules.d.ts.map +1 -0
  65. package/dist/retirement/bucketRules.js +37 -0
  66. package/dist/retirement/bucketRules.js.map +1 -0
  67. package/dist/retirement/engine.d.ts +7 -0
  68. package/dist/retirement/engine.d.ts.map +1 -0
  69. package/dist/retirement/engine.js +297 -0
  70. package/dist/retirement/engine.js.map +1 -0
  71. package/dist/retirement/index.d.ts +6 -0
  72. package/dist/retirement/index.d.ts.map +1 -0
  73. package/dist/retirement/index.js +6 -0
  74. package/dist/retirement/index.js.map +1 -0
  75. package/dist/retirement/runner.d.ts +15 -0
  76. package/dist/retirement/runner.d.ts.map +1 -0
  77. package/dist/retirement/runner.js +178 -0
  78. package/dist/retirement/runner.js.map +1 -0
  79. package/dist/retirement/strategies/index.d.ts +30 -0
  80. package/dist/retirement/strategies/index.d.ts.map +1 -0
  81. package/dist/retirement/strategies/index.js +270 -0
  82. package/dist/retirement/strategies/index.js.map +1 -0
  83. package/dist/retirement/types.d.ts +206 -0
  84. package/dist/retirement/types.d.ts.map +1 -0
  85. package/dist/retirement/types.js +2 -0
  86. package/dist/retirement/types.js.map +1 -0
  87. package/package.json +50 -0
@@ -0,0 +1,40 @@
1
+ import { runBacktest } from './backtest.js';
2
+ /**
3
+ * Sweep the A/B mix from 0→100% A in `steps` increments. Two assets is the
4
+ * classic, honest frontier: the whole curve is enumerable, so there's no
5
+ * optimizer to trust — every point is a portfolio you could actually hold.
6
+ */
7
+ export function twoAssetFrontier(a, b, config, steps = 21) {
8
+ const series = [a, b];
9
+ const points = [];
10
+ for (let i = 0; i < steps; i++) {
11
+ const weightA = Math.round((i / (steps - 1)) * 10000) / 100;
12
+ const weightB = Math.round((100 - weightA) * 100) / 100;
13
+ const allocations = [
14
+ { ticker: a.ticker, weight: weightA },
15
+ { ticker: b.ticker, weight: weightB },
16
+ ].filter((x) => x.weight > 0);
17
+ // Single-asset endpoints: one allocation at 100%.
18
+ const spec = { name: `mix-${i}`, allocations: allocations.length ? allocations : [{ ticker: a.ticker, weight: 100 }] };
19
+ const m = runBacktest(series, spec, config).metrics;
20
+ points.push({ weightA, volatility: m.volatility, cagr: m.cagr, sharpe: m.sharpe });
21
+ }
22
+ return points;
23
+ }
24
+ /** Index of the minimum-variance point. */
25
+ export function minVarianceIndex(points) {
26
+ let idx = 0;
27
+ for (let i = 1; i < points.length; i++)
28
+ if (points[i].volatility < points[idx].volatility)
29
+ idx = i;
30
+ return idx;
31
+ }
32
+ /** Index of the maximum-Sharpe (tangency) point. */
33
+ export function maxSharpeIndex(points) {
34
+ let idx = 0;
35
+ for (let i = 1; i < points.length; i++)
36
+ if (points[i].sharpe > points[idx].sharpe)
37
+ idx = i;
38
+ return idx;
39
+ }
40
+ //# sourceMappingURL=frontier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontier.js","sourceRoot":"","sources":["../../src/engine/frontier.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAgB3C;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,CAAe,EACf,CAAe,EACf,MAAsB,EACtB,KAAK,GAAG,EAAE;IAEV,MAAM,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IACrB,MAAM,MAAM,GAAoB,EAAE,CAAA;IAClC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,GAAG,CAAA;QAC3D,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAA;QACvD,MAAM,WAAW,GAAG;YAClB,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE;YACrC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE;SACtC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;QAC7B,kDAAkD;QAClD,MAAM,IAAI,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,EAAE,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,CAAA;QACtH,MAAM,CAAC,GAAG,WAAW,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAA;QACnD,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAA;IACpF,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,2CAA2C;AAC3C,MAAM,UAAU,gBAAgB,CAAC,MAAuB;IACtD,IAAI,GAAG,GAAG,CAAC,CAAA;IACX,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,UAAU;YAAE,GAAG,GAAG,CAAC,CAAA;IAClG,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,cAAc,CAAC,MAAuB;IACpD,IAAI,GAAG,GAAG,CAAC,CAAA;IACX,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM;YAAE,GAAG,GAAG,CAAC,CAAA;IAC1F,OAAO,GAAG,CAAA;AACZ,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { runBacktest } from './backtest.js';
2
+ export { twoAssetFrontier, minVarianceIndex, maxSharpeIndex, type FrontierPoint, } from './frontier.js';
3
+ export { alignSeries, isNewMonth, isPeriodStart } from './align.js';
4
+ export { annualIncome, annualReturns, computeMetrics, irr, maxDrawdown, monthlyReturns, monthlyReturnsLabeled, rollingReturns, } from './metrics.js';
5
+ export type * from './types.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/engine/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,KAAK,aAAa,GACnB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AACnE,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,GAAG,EACH,WAAW,EACX,cAAc,EACd,qBAAqB,EACrB,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,mBAAmB,YAAY,CAAA"}
@@ -0,0 +1,5 @@
1
+ export { runBacktest } from './backtest.js';
2
+ export { twoAssetFrontier, minVarianceIndex, maxSharpeIndex, } from './frontier.js';
3
+ export { alignSeries, isNewMonth, isPeriodStart } from './align.js';
4
+ export { annualIncome, annualReturns, computeMetrics, irr, maxDrawdown, monthlyReturns, monthlyReturnsLabeled, rollingReturns, } from './metrics.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/engine/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,GAEf,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AACnE,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,GAAG,EACH,WAAW,EACX,cAAc,EACd,qBAAqB,EACrB,cAAc,GACf,MAAM,cAAc,CAAA"}
@@ -0,0 +1,27 @@
1
+ import type { DrawdownInfo, MetricSet, RollingPoint, YearReturn } from './types.js';
2
+ /** Compound the TWR index into calendar-month returns. */
3
+ export declare function monthlyReturns(dates: string[], twrIndex: number[]): number[];
4
+ /** Calendar-month returns with their 'yyyy-mm' labels (for rf matching, regression). */
5
+ export declare function monthlyReturnsLabeled(dates: string[], twrIndex: number[]): Array<{
6
+ month: string;
7
+ ret: number;
8
+ }>;
9
+ export declare function annualReturns(dates: string[], twrIndex: number[]): YearReturn[];
10
+ /**
11
+ * Trailing-window annualized returns, one observation per month-end.
12
+ * Empty when the history is shorter than the window.
13
+ */
14
+ export declare function rollingReturns(dates: string[], twrIndex: number[], windowYears: number): RollingPoint[];
15
+ /** Cash dividend income summed per calendar year. */
16
+ export declare function annualIncome(dates: string[], dividendIncome: number[]): Array<{
17
+ year: number;
18
+ income: number;
19
+ }>;
20
+ export declare function maxDrawdown(dates: string[], twrIndex: number[]): DrawdownInfo;
21
+ /**
22
+ * Money-weighted annual return via bisection on the rate that zeroes the
23
+ * NPV of {initial investment, flows, final value}.
24
+ */
25
+ export declare function irr(dates: string[], flows: number[], initialAmount: number, finalValue: number): number;
26
+ export declare function computeMetrics(dates: string[], twrIndex: number[], values: number[], flows: number[], riskFreeRate: number, rfByMonth?: Record<string, number>): MetricSet;
27
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/engine/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAmBnF,0DAA0D;AAC1D,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAE5E;AAED,wFAAwF;AACxF,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,MAAM,EAAE,GACjB,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAcvC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,UAAU,EAAE,CAiB/E;AAYD;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,MAAM,EAAE,EAClB,WAAW,EAAE,MAAM,GAClB,YAAY,EAAE,CAehB;AAED,qDAAqD;AACrD,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EAAE,EACf,cAAc,EAAE,MAAM,EAAE,GACvB,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAUzC;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,YAAY,CAmC7E;AAED;;;GAGG;AACH,wBAAgB,GAAG,CACjB,KAAK,EAAE,MAAM,EAAE,EACf,KAAK,EAAE,MAAM,EAAE,EACf,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,GACjB,MAAM,CA4BR;AAED,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,MAAM,EAAE,EAClB,MAAM,EAAE,MAAM,EAAE,EAChB,KAAK,EAAE,MAAM,EAAE,EACf,YAAY,EAAE,MAAM,EACpB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,SAAS,CAsCX"}
@@ -0,0 +1,202 @@
1
+ const DAY_MS = 86_400_000;
2
+ function yearsBetween(start, end) {
3
+ return (Date.parse(end) - Date.parse(start)) / DAY_MS / 365.25;
4
+ }
5
+ function mean(xs) {
6
+ return xs.reduce((s, x) => s + x, 0) / xs.length;
7
+ }
8
+ /** Sample standard deviation. */
9
+ function stdev(xs) {
10
+ if (xs.length < 2)
11
+ return 0;
12
+ const m = mean(xs);
13
+ return Math.sqrt(xs.reduce((s, x) => s + (x - m) ** 2, 0) / (xs.length - 1));
14
+ }
15
+ /** Compound the TWR index into calendar-month returns. */
16
+ export function monthlyReturns(dates, twrIndex) {
17
+ return monthlyReturnsLabeled(dates, twrIndex).map((m) => m.ret);
18
+ }
19
+ /** Calendar-month returns with their 'yyyy-mm' labels (for rf matching, regression). */
20
+ export function monthlyReturnsLabeled(dates, twrIndex) {
21
+ const out = [];
22
+ let monthStartIdx = 0;
23
+ for (let t = 1; t < dates.length; t++) {
24
+ if (dates[t].slice(0, 7) !== dates[t - 1].slice(0, 7)) {
25
+ out.push({ month: dates[t - 1].slice(0, 7), ret: twrIndex[t - 1] / twrIndex[monthStartIdx] - 1 });
26
+ monthStartIdx = t - 1;
27
+ }
28
+ }
29
+ out.push({
30
+ month: dates[dates.length - 1].slice(0, 7),
31
+ ret: twrIndex[dates.length - 1] / twrIndex[monthStartIdx] - 1,
32
+ });
33
+ return out;
34
+ }
35
+ export function annualReturns(dates, twrIndex) {
36
+ const out = [];
37
+ let yearStartIdx = 0;
38
+ for (let t = 1; t < dates.length; t++) {
39
+ if (dates[t].slice(0, 4) !== dates[t - 1].slice(0, 4)) {
40
+ out.push({
41
+ year: Number(dates[t - 1].slice(0, 4)),
42
+ return: twrIndex[t - 1] / twrIndex[yearStartIdx] - 1,
43
+ });
44
+ yearStartIdx = t - 1;
45
+ }
46
+ }
47
+ out.push({
48
+ year: Number(dates[dates.length - 1].slice(0, 4)),
49
+ return: twrIndex[dates.length - 1] / twrIndex[yearStartIdx] - 1,
50
+ });
51
+ return out;
52
+ }
53
+ /** Indices of the last trading day of each calendar month (incl. the final day). */
54
+ function monthEndIndices(dates) {
55
+ const out = [];
56
+ for (let t = 1; t < dates.length; t++) {
57
+ if (dates[t].slice(0, 7) !== dates[t - 1].slice(0, 7))
58
+ out.push(t - 1);
59
+ }
60
+ out.push(dates.length - 1);
61
+ return out;
62
+ }
63
+ /**
64
+ * Trailing-window annualized returns, one observation per month-end.
65
+ * Empty when the history is shorter than the window.
66
+ */
67
+ export function rollingReturns(dates, twrIndex, windowYears) {
68
+ const ends = monthEndIndices(dates);
69
+ const w = windowYears * 12;
70
+ const out = [];
71
+ for (let k = w; k < ends.length; k++) {
72
+ const start = ends[k - w];
73
+ const end = ends[k];
74
+ const years = yearsBetween(dates[start], dates[end]);
75
+ if (years <= 0)
76
+ continue;
77
+ out.push({
78
+ date: dates[end],
79
+ value: (twrIndex[end] / twrIndex[start]) ** (1 / years) - 1,
80
+ });
81
+ }
82
+ return out;
83
+ }
84
+ /** Cash dividend income summed per calendar year. */
85
+ export function annualIncome(dates, dividendIncome) {
86
+ const byYear = new Map();
87
+ for (let t = 0; t < dates.length; t++) {
88
+ if (dividendIncome[t] === 0)
89
+ continue;
90
+ const year = Number(dates[t].slice(0, 4));
91
+ byYear.set(year, (byYear.get(year) ?? 0) + dividendIncome[t]);
92
+ }
93
+ return [...byYear.entries()]
94
+ .map(([year, income]) => ({ year, income }))
95
+ .sort((a, b) => a.year - b.year);
96
+ }
97
+ export function maxDrawdown(dates, twrIndex) {
98
+ let peak = twrIndex[0];
99
+ let peakDate = dates[0];
100
+ let maxDd = 0;
101
+ let ddPeakDate = dates[0];
102
+ let troughDate = dates[0];
103
+ let troughIdx = 0;
104
+ for (let t = 1; t < twrIndex.length; t++) {
105
+ if (twrIndex[t] > peak) {
106
+ peak = twrIndex[t];
107
+ peakDate = dates[t];
108
+ }
109
+ const dd = twrIndex[t] / peak - 1;
110
+ if (dd < maxDd) {
111
+ maxDd = dd;
112
+ ddPeakDate = peakDate;
113
+ troughDate = dates[t];
114
+ troughIdx = t;
115
+ }
116
+ }
117
+ // Recovery: first date after the trough where the index regains its peak.
118
+ let recoveryDate = null;
119
+ if (maxDd < 0) {
120
+ const peakLevel = twrIndex[dates.indexOf(ddPeakDate)];
121
+ for (let t = troughIdx + 1; t < twrIndex.length; t++) {
122
+ if (twrIndex[t] >= peakLevel) {
123
+ recoveryDate = dates[t];
124
+ break;
125
+ }
126
+ }
127
+ }
128
+ return { maxDrawdown: maxDd, peakDate: ddPeakDate, troughDate, recoveryDate };
129
+ }
130
+ /**
131
+ * Money-weighted annual return via bisection on the rate that zeroes the
132
+ * NPV of {initial investment, flows, final value}.
133
+ */
134
+ export function irr(dates, flows, initialAmount, finalValue) {
135
+ const t0 = Date.parse(dates[0]);
136
+ const horizon = yearsBetween(dates[0], dates[dates.length - 1]);
137
+ if (horizon <= 0)
138
+ return 0;
139
+ const npv = (rate) => {
140
+ let v = -initialAmount;
141
+ for (let t = 1; t < dates.length; t++) {
142
+ if (flows[t] !== 0) {
143
+ const yrs = (Date.parse(dates[t]) - t0) / DAY_MS / 365.25;
144
+ v -= flows[t] / (1 + rate) ** yrs;
145
+ }
146
+ }
147
+ v += finalValue / (1 + rate) ** horizon;
148
+ return v;
149
+ };
150
+ let lo = -0.9999;
151
+ let hi = 10;
152
+ // Short horizons annualize to enormous rates — expand the bracket as needed.
153
+ while (npv(hi) > 0 && hi < 1e12)
154
+ hi *= 10;
155
+ if (npv(lo) * npv(hi) > 0)
156
+ return NaN; // no sign change — IRR undefined
157
+ for (let i = 0; i < 200; i++) {
158
+ const mid = (lo + hi) / 2;
159
+ if (npv(lo) * npv(mid) <= 0)
160
+ hi = mid;
161
+ else
162
+ lo = mid;
163
+ }
164
+ return (lo + hi) / 2;
165
+ }
166
+ export function computeMetrics(dates, twrIndex, values, flows, riskFreeRate, rfByMonth) {
167
+ const totalReturn = twrIndex[twrIndex.length - 1] / twrIndex[0] - 1;
168
+ const horizon = yearsBetween(dates[0], dates[dates.length - 1]);
169
+ const cagr = horizon > 0 ? (1 + totalReturn) ** (1 / horizon) - 1 : 0;
170
+ const labeled = monthlyReturnsLabeled(dates, twrIndex);
171
+ const monthly = labeled.map((m) => m.ret);
172
+ const rfConst = (1 + riskFreeRate) ** (1 / 12) - 1;
173
+ // Prefer the contemporaneous monthly T-bill return when supplied.
174
+ const excess = labeled.map((m) => m.ret - (rfByMonth?.[m.month] ?? rfConst));
175
+ const monthlyStdev = stdev(monthly);
176
+ const volatility = monthlyStdev * Math.sqrt(12);
177
+ const sharpe = monthlyStdev > 0 ? (mean(excess) / monthlyStdev) * Math.sqrt(12) : 0;
178
+ // Downside deviation over ALL months (Portfolio Visualizer convention).
179
+ const downside = Math.sqrt(mean(excess.map((e) => Math.min(0, e) ** 2)));
180
+ const sortino = downside > 0 ? (mean(excess) / downside) * Math.sqrt(12) : 0;
181
+ const annual = annualReturns(dates, twrIndex);
182
+ // Partial first/last years still count for best/worst, matching PV.
183
+ const best = annual.length
184
+ ? annual.reduce((a, b) => (b.return > a.return ? b : a))
185
+ : null;
186
+ const worst = annual.length
187
+ ? annual.reduce((a, b) => (b.return < a.return ? b : a))
188
+ : null;
189
+ return {
190
+ totalReturn,
191
+ cagr,
192
+ volatility,
193
+ sharpe,
194
+ sortino,
195
+ drawdown: maxDrawdown(dates, twrIndex),
196
+ annualReturns: annual,
197
+ bestYear: best,
198
+ worstYear: worst,
199
+ irr: irr(dates, flows, values[0], values[values.length - 1]),
200
+ };
201
+ }
202
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../src/engine/metrics.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,GAAG,UAAU,CAAA;AAEzB,SAAS,YAAY,CAAC,KAAa,EAAE,GAAW;IAC9C,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,MAAM,GAAG,MAAM,CAAA;AAChE,CAAC;AAED,SAAS,IAAI,CAAC,EAAY;IACxB,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,MAAM,CAAA;AAClD,CAAC;AAED,iCAAiC;AACjC,SAAS,KAAK,CAAC,EAAY;IACzB,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,CAAC,CAAA;IAC3B,MAAM,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,CAAA;IAClB,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAA;AAC9E,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,cAAc,CAAC,KAAe,EAAE,QAAkB;IAChE,OAAO,qBAAqB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;AACjE,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,qBAAqB,CACnC,KAAe,EACf,QAAkB;IAElB,MAAM,GAAG,GAA0C,EAAE,CAAA;IACrD,IAAI,aAAa,GAAG,CAAC,CAAA;IACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;YACtD,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YACjG,aAAa,GAAG,CAAC,GAAG,CAAC,CAAA;QACvB,CAAC;IACH,CAAC;IACD,GAAG,CAAC,IAAI,CAAC;QACP,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;QAC1C,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC;KAC9D,CAAC,CAAA;IACF,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAe,EAAE,QAAkB;IAC/D,MAAM,GAAG,GAAiB,EAAE,CAAA;IAC5B,IAAI,YAAY,GAAG,CAAC,CAAA;IACpB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;YACtD,GAAG,CAAC,IAAI,CAAC;gBACP,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;gBACtC,MAAM,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,YAAY,CAAC,GAAG,CAAC;aACrD,CAAC,CAAA;YACF,YAAY,GAAG,CAAC,GAAG,CAAC,CAAA;QACtB,CAAC;IACH,CAAC;IACD,GAAG,CAAC,IAAI,CAAC;QACP,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACjD,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,YAAY,CAAC,GAAG,CAAC;KAChE,CAAC,CAAA;IACF,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,oFAAoF;AACpF,SAAS,eAAe,CAAC,KAAe;IACtC,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IACxE,CAAC;IACD,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAC1B,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,KAAe,EACf,QAAkB,EAClB,WAAmB;IAEnB,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,CAAC,CAAA;IACnC,MAAM,CAAC,GAAG,WAAW,GAAG,EAAE,CAAA;IAC1B,MAAM,GAAG,GAAmB,EAAE,CAAA;IAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;QACzB,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;QACnB,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAA;QACpD,IAAI,KAAK,IAAI,CAAC;YAAE,SAAQ;QACxB,GAAG,CAAC,IAAI,CAAC;YACP,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC;YAChB,KAAK,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC;SAC5D,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,YAAY,CAC1B,KAAe,EACf,cAAwB;IAExB,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAA;IACxC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC;YAAE,SAAQ;QACrC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;QACzC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAAA;IAC/D,CAAC;IACD,OAAO,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;SACzB,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;SAC3C,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAA;AACpC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAe,EAAE,QAAkB;IAC7D,IAAI,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;IACtB,IAAI,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;IACvB,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,IAAI,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;IACzB,IAAI,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;IACzB,IAAI,SAAS,GAAG,CAAC,CAAA;IAEjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,IAAI,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,EAAE,CAAC;YACvB,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;YAClB,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;QACrB,CAAC;QACD,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,CAAA;QACjC,IAAI,EAAE,GAAG,KAAK,EAAE,CAAC;YACf,KAAK,GAAG,EAAE,CAAA;YACV,UAAU,GAAG,QAAQ,CAAA;YACrB,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;YACrB,SAAS,GAAG,CAAC,CAAA;QACf,CAAC;IACH,CAAC;IAED,0EAA0E;IAC1E,IAAI,YAAY,GAAkB,IAAI,CAAA;IACtC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACd,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAA;QACrD,KAAK,IAAI,CAAC,GAAG,SAAS,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACrD,IAAI,QAAQ,CAAC,CAAC,CAAC,IAAI,SAAS,EAAE,CAAC;gBAC7B,YAAY,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;gBACvB,MAAK;YACP,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,CAAA;AAC/E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,GAAG,CACjB,KAAe,EACf,KAAe,EACf,aAAqB,EACrB,UAAkB;IAElB,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IAC/B,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAA;IAC/D,IAAI,OAAO,IAAI,CAAC;QAAE,OAAO,CAAC,CAAA;IAE1B,MAAM,GAAG,GAAG,CAAC,IAAY,EAAU,EAAE;QACnC,IAAI,CAAC,GAAG,CAAC,aAAa,CAAA;QACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;gBACnB,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,MAAM,CAAA;gBACzD,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,GAAG,CAAA;YACnC,CAAC;QACH,CAAC;QACD,CAAC,IAAI,UAAU,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,OAAO,CAAA;QACvC,OAAO,CAAC,CAAA;IACV,CAAC,CAAA;IAED,IAAI,EAAE,GAAG,CAAC,MAAM,CAAA;IAChB,IAAI,EAAE,GAAG,EAAE,CAAA;IACX,6EAA6E;IAC7E,OAAO,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,IAAI;QAAE,EAAE,IAAI,EAAE,CAAA;IACzC,IAAI,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAA,CAAC,iCAAiC;IACvE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAA;QACzB,IAAI,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,EAAE,GAAG,GAAG,CAAA;;YAChC,EAAE,GAAG,GAAG,CAAA;IACf,CAAC;IACD,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAA;AACtB,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,KAAe,EACf,QAAkB,EAClB,MAAgB,EAChB,KAAe,EACf,YAAoB,EACpB,SAAkC;IAElC,MAAM,WAAW,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IACnE,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAA;IAC/D,MAAM,IAAI,GAAG,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAErE,MAAM,OAAO,GAAG,qBAAqB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;IACtD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IACzC,MAAM,OAAO,GAAG,CAAC,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAA;IAClD,kEAAkE;IAClE,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,CAAC,CAAA;IAC5E,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC,CAAA;IACnC,MAAM,UAAU,GAAG,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAC/C,MAAM,MAAM,GAAG,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,YAAY,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACnF,wEAAwE;IACxE,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IACxE,MAAM,OAAO,GAAG,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAE5E,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;IAC7C,oEAAoE;IACpE,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM;QACxB,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACxD,CAAC,CAAC,IAAI,CAAA;IACR,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM;QACzB,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACxD,CAAC,CAAC,IAAI,CAAA;IAER,OAAO;QACL,WAAW;QACX,IAAI;QACJ,UAAU;QACV,MAAM;QACN,OAAO;QACP,QAAQ,EAAE,WAAW,CAAC,KAAK,EAAE,QAAQ,CAAC;QACtC,aAAa,EAAE,MAAM;QACrB,QAAQ,EAAE,IAAI;QACd,SAAS,EAAE,KAAK;QAChB,GAAG,EAAE,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;KAC7D,CAAA;AACH,CAAC"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Canonical daily record, matching data/tickers/<TICKER>.json from the
3
+ * Tiingo fetch layer. `close` is unadjusted; `adjClose` is split- and
4
+ * dividend-adjusted as of fetch time.
5
+ */
6
+ export interface DailyRecord {
7
+ date: string;
8
+ close: number;
9
+ adjClose: number;
10
+ divCash: number;
11
+ splitFactor: number;
12
+ }
13
+ export interface TickerSeries {
14
+ ticker: string;
15
+ name?: string;
16
+ records: DailyRecord[];
17
+ }
18
+ export type RebalanceFrequency = 'none' | 'annual' | 'quarterly' | 'monthly';
19
+ export interface Allocation {
20
+ ticker: string;
21
+ /** Target weight in percent, e.g. 60 for 60%. Must sum to 100 across the portfolio. */
22
+ weight: number;
23
+ }
24
+ export interface PortfolioSpec {
25
+ name: string;
26
+ allocations: Allocation[];
27
+ }
28
+ export interface BacktestConfig {
29
+ /** yyyy-mm-dd inclusive bounds. Clamped to the common history of all tickers. */
30
+ start?: string;
31
+ end?: string;
32
+ initialAmount: number;
33
+ /** Cash added (+) or withdrawn (−) at the start of each calendar month, invested at target weights. */
34
+ monthlyContribution: number;
35
+ rebalance: RebalanceFrequency;
36
+ /** true = dividends reinvested same day (total return). false = dividends accrue as uninvested cash. */
37
+ reinvestDividends: boolean;
38
+ /** Constant annual risk-free rate for Sharpe/Sortino, e.g. 0.03. Defaults to 0. */
39
+ riskFreeRate?: number;
40
+ /**
41
+ * Historical MONTHLY risk-free returns keyed by 'yyyy-mm' (e.g. from the
42
+ * Fama-French RF series). When provided, Sharpe/Sortino use the actual
43
+ * contemporaneous T-bill return each month instead of the constant.
44
+ */
45
+ rfByMonth?: Record<string, number>;
46
+ }
47
+ /** Semantics of the simulation, documented once:
48
+ * - Calendar = intersection of all tickers' trading days within [start, end].
49
+ * - Day 0: initial amount buys in at the close. Returns accrue from day 1.
50
+ * - Monthly cashflows and rebalancing execute at the START of the first
51
+ * trading day of the period, at the prior day's closing values.
52
+ * - Reinvested dividends compound via adjClose ratios (reinvest at close on
53
+ * ex-date). Non-reinvested dividends accrue to a cash bucket earning 0%.
54
+ */
55
+ export interface BacktestResult {
56
+ portfolio: PortfolioSpec;
57
+ dates: string[];
58
+ /** Total portfolio market value per day (holdings + dividend cash), after flows. */
59
+ values: number[];
60
+ /** Time-weighted return index, 1.0 at day 0. Flows are excluded — use for all metrics. */
61
+ twrIndex: number[];
62
+ /** External flow applied at the start of each day (contributions/withdrawals). */
63
+ flows: number[];
64
+ /**
65
+ * Cash dividends received each day (before any reinvestment). When
66
+ * reinvesting, this money bought shares the same day — it is still income.
67
+ */
68
+ dividendIncome: number[];
69
+ endingCash: number;
70
+ totalContributions: number;
71
+ /** Per-ticker breakdown at the end of the period. */
72
+ holdings: HoldingBreakdown[];
73
+ metrics: MetricSet;
74
+ }
75
+ export interface HoldingBreakdown {
76
+ ticker: string;
77
+ /** Target weight in percent, from the portfolio spec. */
78
+ targetWeight: number;
79
+ endValue: number;
80
+ /** Share of final portfolio value (incl. dividend cash), percent. */
81
+ endWeight: number;
82
+ /** The asset's own cumulative total return over the period (standalone). */
83
+ assetTotalReturn: number;
84
+ /** Dividends this holding paid into the portfolio over the period. */
85
+ income: number;
86
+ }
87
+ export interface DrawdownInfo {
88
+ maxDrawdown: number;
89
+ peakDate: string;
90
+ troughDate: string;
91
+ /** Date the previous peak was regained, or null if not yet recovered. */
92
+ recoveryDate: string | null;
93
+ }
94
+ export interface YearReturn {
95
+ year: number;
96
+ return: number;
97
+ }
98
+ /** One rolling-window observation, keyed by the window's end date. */
99
+ export interface RollingPoint {
100
+ date: string;
101
+ /** Annualized return over the trailing window. */
102
+ value: number;
103
+ }
104
+ export interface MetricSet {
105
+ totalReturn: number;
106
+ cagr: number;
107
+ /** Annualized from monthly return stdev (×√12), Portfolio Visualizer convention. */
108
+ volatility: number;
109
+ sharpe: number;
110
+ sortino: number;
111
+ drawdown: DrawdownInfo;
112
+ annualReturns: YearReturn[];
113
+ bestYear: YearReturn | null;
114
+ worstYear: YearReturn | null;
115
+ /** Money-weighted annual return (IRR). Equals CAGR when there are no flows. */
116
+ irr: number;
117
+ }
118
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/engine/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/engine/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @calculator53295/backtest-engine
3
+ *
4
+ * Pure TypeScript backtest and retirement-simulation engine.
5
+ * Zero runtime dependencies — this is a hard constraint of the engine.
6
+ *
7
+ * Lifted verbatim from Fathom's fixture-guarded engine (app/src/engine +
8
+ * app/src/montecarlo). One engine, three consumers: retirement-sim, Fathom,
9
+ * finance-master. This repo must never contain personal financial data —
10
+ * synthetic or public-market fixtures only.
11
+ */
12
+ /** Package version. Kept in sync with package.json at release time. */
13
+ export declare const VERSION = "0.1.0";
14
+ export * from './engine/index.js';
15
+ export { mulberry32, trialMonths, runHistoricalSequence, runBootstrap, maxSafeWithdrawal, type WithdrawalStrategy, type SimParams, type RealReturnSeries, type SimResult, } from './montecarlo/simulate.js';
16
+ export { buildRealReturns, type AllocationWeight, type AssetData, } from './montecarlo/data.js';
17
+ /**
18
+ * Canonical type vocabulary + adapters reconciling the Fathom and
19
+ * retirement-sim dialects. Namespaced to avoid colliding with the engines'
20
+ * flat exports (both define an `Allocation`, a `WithdrawalStrategy`, etc.).
21
+ */
22
+ export * as canonical from './canonical/index.js';
23
+ /** Historical retirement simulation engine and withdrawal strategy registry. */
24
+ export * as retirement from './retirement/index.js';
25
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,uEAAuE;AACvE,eAAO,MAAM,OAAO,UAAU,CAAC;AAE/B,cAAc,mBAAmB,CAAA;AACjC,OAAO,EACL,UAAU,EACV,WAAW,EACX,qBAAqB,EACrB,YAAY,EACZ,iBAAiB,EACjB,KAAK,kBAAkB,EACvB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,SAAS,GACf,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,KAAK,SAAS,GACf,MAAM,sBAAsB,CAAA;AAE7B;;;;GAIG;AACH,OAAO,KAAK,SAAS,MAAM,sBAAsB,CAAA;AAEjD,gFAAgF;AAChF,OAAO,KAAK,UAAU,MAAM,uBAAuB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @calculator53295/backtest-engine
3
+ *
4
+ * Pure TypeScript backtest and retirement-simulation engine.
5
+ * Zero runtime dependencies — this is a hard constraint of the engine.
6
+ *
7
+ * Lifted verbatim from Fathom's fixture-guarded engine (app/src/engine +
8
+ * app/src/montecarlo). One engine, three consumers: retirement-sim, Fathom,
9
+ * finance-master. This repo must never contain personal financial data —
10
+ * synthetic or public-market fixtures only.
11
+ */
12
+ /** Package version. Kept in sync with package.json at release time. */
13
+ export const VERSION = "0.1.0";
14
+ export * from './engine/index.js';
15
+ export { mulberry32, trialMonths, runHistoricalSequence, runBootstrap, maxSafeWithdrawal, } from './montecarlo/simulate.js';
16
+ export { buildRealReturns, } from './montecarlo/data.js';
17
+ /**
18
+ * Canonical type vocabulary + adapters reconciling the Fathom and
19
+ * retirement-sim dialects. Namespaced to avoid colliding with the engines'
20
+ * flat exports (both define an `Allocation`, a `WithdrawalStrategy`, etc.).
21
+ */
22
+ export * as canonical from './canonical/index.js';
23
+ /** Historical retirement simulation engine and withdrawal strategy registry. */
24
+ export * as retirement from './retirement/index.js';
25
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,uEAAuE;AACvE,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAE/B,cAAc,mBAAmB,CAAA;AACjC,OAAO,EACL,UAAU,EACV,WAAW,EACX,qBAAqB,EACrB,YAAY,EACZ,iBAAiB,GAKlB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,gBAAgB,GAGjB,MAAM,sBAAsB,CAAA;AAE7B;;;;GAIG;AACH,OAAO,KAAK,SAAS,MAAM,sBAAsB,CAAA;AAEjD,gFAAgF;AAChF,OAAO,KAAK,UAAU,MAAM,uBAAuB,CAAA"}
@@ -0,0 +1,17 @@
1
+ import type { RealReturnSeries } from './simulate.js';
2
+ export interface AllocationWeight {
3
+ assetId: string;
4
+ weight: number;
5
+ }
6
+ /** The asset-class data shape returned by loadAssetClassData(). */
7
+ export interface AssetData {
8
+ returns: Map<string, Map<string, number>>;
9
+ cpi: Map<string, number>;
10
+ }
11
+ /**
12
+ * Build a blended REAL monthly return series for an allocation, over the range
13
+ * all its assets (and CPI) share. Monthly rebalancing (a weighted average of
14
+ * each asset's monthly return); nominal is deflated to real via CPI.
15
+ */
16
+ export declare function buildRealReturns(allocation: AllocationWeight[], data: AssetData): RealReturnSeries;
17
+ //# sourceMappingURL=data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../../src/montecarlo/data.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAErD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;CACf;AAED,mEAAmE;AACnE,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACzC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACzB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,gBAAgB,EAAE,EAC9B,IAAI,EAAE,SAAS,GACd,gBAAgB,CA8BlB"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Build a blended REAL monthly return series for an allocation, over the range
3
+ * all its assets (and CPI) share. Monthly rebalancing (a weighted average of
4
+ * each asset's monthly return); nominal is deflated to real via CPI.
5
+ */
6
+ export function buildRealReturns(allocation, data) {
7
+ const active = allocation.filter((a) => a.weight > 0);
8
+ if (active.length === 0)
9
+ return { dates: [], returns: [] };
10
+ const totalWeight = active.reduce((s, a) => s + a.weight, 0);
11
+ // Common months present in every asset AND cpi.
12
+ const maps = active.map((a) => data.returns.get(a.assetId));
13
+ if (maps.some((m) => !m))
14
+ return { dates: [], returns: [] };
15
+ const first = maps[0];
16
+ const common = [];
17
+ for (const ym of first.keys()) {
18
+ if (data.cpi.has(ym) && maps.every((m) => m.has(ym)))
19
+ common.push(ym);
20
+ }
21
+ common.sort();
22
+ const dates = [];
23
+ const returns = [];
24
+ for (let i = 1; i < common.length; i++) {
25
+ const ym = common[i];
26
+ const prevCpi = data.cpi.get(common[i - 1]);
27
+ const curCpi = data.cpi.get(ym);
28
+ let nominal = 0;
29
+ for (let a = 0; a < active.length; a++) {
30
+ nominal += (active[a].weight / totalWeight) * maps[a].get(ym);
31
+ }
32
+ const inflation = curCpi / prevCpi;
33
+ dates.push(ym);
34
+ returns.push((1 + nominal) / inflation - 1);
35
+ }
36
+ return { dates, returns };
37
+ }
38
+ //# sourceMappingURL=data.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data.js","sourceRoot":"","sources":["../../src/montecarlo/data.ts"],"names":[],"mappings":"AAaA;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,UAA8B,EAC9B,IAAe;IAEf,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IACrD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAA;IAC1D,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;IAE5D,gDAAgD;IAChD,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAA;IAC3D,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAA;IAC3D,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAE,CAAA;IACtB,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;QAC9B,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACxE,CAAC;IACD,MAAM,CAAC,IAAI,EAAE,CAAA;IAEb,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;QACpB,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAE,CAAA;QAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAE,CAAA;QAChC,IAAI,OAAO,GAAG,CAAC,CAAA;QACf,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,WAAW,CAAC,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC,GAAG,CAAC,EAAE,CAAE,CAAA;QACjE,CAAC;QACD,MAAM,SAAS,GAAG,MAAM,GAAG,OAAO,CAAA;QAClC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACd,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,SAAS,GAAG,CAAC,CAAC,CAAA;IAC7C,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAA;AAC3B,CAAC"}