@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 finance-kit contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # @calculator53295/backtest-engine
2
+
3
+ A **pure-TypeScript, zero-runtime-dependency** engine for portfolio backtesting and
4
+ retirement simulation. Everything ships as plain functions over plain data — no
5
+ network, no filesystem, no globals — so the same inputs produce the same numbers
6
+ in a browser, a worker, or Node.
7
+
8
+ Three surfaces in one package:
9
+
10
+ - **Daily backtest engine** — `runBacktest` walks a portfolio over the
11
+ intersection of its tickers' trading days, handling dividends (reinvested or
12
+ accrued as cash), splits, monthly contributions, and rebalancing. It reports a
13
+ time-weighted-return index (flows stripped out), a money-weighted **IRR**, plus
14
+ drawdown, volatility, annual/monthly/rolling returns, and per-holding
15
+ breakdowns. A two-asset efficient-frontier helper (`twoAssetFrontier`,
16
+ `minVarianceIndex`, `maxSharpeIndex`) rounds it out.
17
+ - **Monte Carlo retirement simulator** — `runHistoricalSequence`,
18
+ `runBootstrap`, and `maxSafeWithdrawal` run withdrawal plans over historical
19
+ or block-bootstrapped real-return sequences and summarize success rate, balance
20
+ and income percentiles, and pay-cut risk. Everything is in **real
21
+ (inflation-adjusted)** dollars.
22
+ - **Historical cohort retirement engine** (`retirement.*`) — a bucketed
23
+ stocks/bonds/cash simulator with glide paths, income and extra-withdrawal
24
+ streams, fees, and a **registry of nine withdrawal strategies** (constant
25
+ dollar, percent-of-portfolio, 1/N, VPW, Guyton-Klinger, CAPE-based, 95%-rule,
26
+ endowment, Vanguard dynamic — plus legacy adapters). `runAllCohorts` sweeps
27
+ every historical start date; `solveSwr` back-solves the safe withdrawal rate.
28
+
29
+ A namespaced **canonical** module (`canonical.*`) provides a forward-compatible
30
+ type vocabulary and pure adapters that reconcile the two engine dialects lifted
31
+ into this repo, so consumers can share one vocabulary as the surfaces converge.
32
+
33
+ ## How this engine is tested
34
+
35
+ This package exists to be trusted by three production apps, so its behavior is
36
+ pinned by tests at three levels — hand-computed synthetic fixtures for exact
37
+ correctness, real-market regression bands for realism, and a golden parity
38
+ fixture that locks results to the app the code was extracted from. The engine
39
+ itself carries **no personal financial data**: only synthetic tuples and public
40
+ market/Shiller data are used as fixtures.
41
+
42
+ | Fixture class | What it pins | Concrete examples (from the test suite) |
43
+ | --- | --- | --- |
44
+ | **Hand-computed synthetic fixtures** | Exact arithmetic of the daily engine — every value is derived by hand in the test, no tolerance beyond float epsilon. | A flat/linear price path `100 → 110 → 99` on a single asset must yield portfolio values `10 000 → 11 000 → 9 900` and `totalReturn = −0.01` (`engine/__tests__/backtest.test.ts`). A `$2` dividend where unadjusted close drops `100 → 99` yields `10 100` reinvested and `10 100` with `$200` accrued cash when not. A 2:1 split (`100 → 51`, real `+2%`) must neutralize to `10 200`. Two-asset 60/40 monthly rebalancing lands on `11 024`; TWR strips a `$1 000` monthly contribution so the index matches the no-contribution run exactly. |
45
+ | **Real-data regression bands** | The engine reproduces known market history within approximate ranges (vendor adjustment methods differ slightly). Runs against gitignored Tiingo data; auto-skips when absent. | SPY 1994–2023 CAGR ∈ (9%, 11%); GFC max drawdown ∈ (−58%, −50%) troughing in **2009-03** with a non-null recovery; 2008 annual return ∈ (−40%, −34%) (`≈ −37%`); AAPL 2020 **4:1 split** continuity — no daily move outside (0.85×, 1.15×); a 60/40 SPY/BND mix shows lower volatility and shallower drawdown than pure SPY and clamps its range to BND's 2007 inception (`engine/__tests__/realdata.test.ts`). |
46
+ | **Golden Shiller cohort parity** | The retirement cohort engine matches the app it was ported from, bit-for-bit. | The first 48 rows of retirement-sim's public `shiller.csv` (1871-01…1874-12) run through `runAllCohorts` must reproduce captured summary quantiles (real and nominal end balances) to 10 decimal places, with `count = 3`, `successRate = 1`, start dates `1871/1872/1873-01`, worst cohort `1872-01`, best `1871-01`, and every year-1 real withdrawal `≈ $4 000` (`retirement/__tests__/golden-cohort.test.ts`). |
47
+
48
+ ## Consumers
49
+
50
+ One engine, three production apps by the same author — extracting the shared math
51
+ here means simulation results are identical everywhere the same inputs appear,
52
+ with no per-app drift:
53
+
54
+ - **retirement-sim** — the cohort retirement engine and withdrawal-strategy
55
+ registry were ported from it.
56
+ - **Fathom** stock-analysis suite — the daily backtest and Monte Carlo engines
57
+ were lifted verbatim from its fixture-guarded `src/engine` + `src/montecarlo`.
58
+ - **finance-master** — shares the same engine.
59
+
60
+ ## Quickstart
61
+
62
+ ### Daily backtest — `runBacktest`
63
+
64
+ ```ts
65
+ import { runBacktest } from "@calculator53295/backtest-engine";
66
+
67
+ const spy /* : TickerSeries */ = {
68
+ ticker: "SPY",
69
+ records: [
70
+ { date: "2020-01-02", close: 100, adjClose: 100, divCash: 0, splitFactor: 1 },
71
+ { date: "2020-01-03", close: 110, adjClose: 110, divCash: 0, splitFactor: 1 },
72
+ { date: "2020-01-06", close: 99, adjClose: 99, divCash: 0, splitFactor: 1 },
73
+ ],
74
+ };
75
+
76
+ const result = runBacktest(
77
+ [spy],
78
+ { name: "All SPY", allocations: [{ ticker: "SPY", weight: 100 }] },
79
+ { initialAmount: 10_000, monthlyContribution: 0, rebalance: "none", reinvestDividends: true },
80
+ );
81
+
82
+ result.values; // portfolio value per trading day
83
+ result.metrics.cagr; // + totalReturn, irr, volatility, drawdown, annualReturns…
84
+ result.twrIndex; // flow-stripped time-weighted-return index
85
+ ```
86
+
87
+ ### Retirement Monte Carlo — `runHistoricalSequence` / `maxSafeWithdrawal`
88
+
89
+ ```ts
90
+ import {
91
+ runHistoricalSequence,
92
+ maxSafeWithdrawal,
93
+ type RealReturnSeries,
94
+ type SimParams,
95
+ } from "@calculator53295/backtest-engine";
96
+
97
+ // Real (inflation-adjusted) monthly total returns, oldest first.
98
+ const series: RealReturnSeries = { dates: ["1994-01", /* … */], returns: [0.003, /* … */] };
99
+
100
+ const params: Omit<SimParams, "withdrawalRate"> = {
101
+ initialBalance: 1_000_000,
102
+ strategy: "fixedReal", // or "fixedPercent" | "vpw" | "guardrails"
103
+ horizonYears: 30,
104
+ feeRate: 0.001,
105
+ };
106
+
107
+ const sim = runHistoricalSequence(series, { ...params, withdrawalRate: 0.04 });
108
+ sim.successRate; // fraction of historical cohorts that survived
109
+ sim.percentiles.p50; // median balance path, today's dollars
110
+
111
+ // Or back-solve the highest rate meeting a 95% success target:
112
+ const swr = maxSafeWithdrawal(series, params, 0.95);
113
+ ```
114
+
115
+ ### Historical cohort engine — `retirement.simulate`
116
+
117
+ ```ts
118
+ import { retirement } from "@calculator53295/backtest-engine";
119
+ // or: import { simulate, runAllCohorts } from "@calculator53295/backtest-engine/retirement";
120
+
121
+ // [date, spReturn, bondReturn, cashReturn, cpi] → HistoricalRow[]
122
+ const rows: retirement.HistoricalRow[] = [
123
+ { date: "1871-01", spReturn: 0.0184, bondReturn: 0.0042, cashReturn: 0.0011, cpi: 12.4641 },
124
+ // …monthly public Shiller / asset-class rows…
125
+ ];
126
+
127
+ const trial = retirement.simulate(
128
+ {
129
+ startDate: "1871-01",
130
+ horizonYears: 30,
131
+ stepFreq: "monthly",
132
+ initialBalance: 100_000,
133
+ initialAllocation: { stocks: 0.6, bonds: 0.3, cash: 0.1 },
134
+ strategy: { kind: "constant-dollar", annualAmount: 40_000, inflationAdjusted: true, sourcing: "proportional" },
135
+ rebalance: "yearly",
136
+ },
137
+ rows,
138
+ );
139
+
140
+ trial.endBalanceReal; // ending balance, today's dollars
141
+ trial.ranOutAtPeriod; // null if the plan survived
142
+
143
+ // Sweep every start date in the dataset:
144
+ const { summary } = retirement.runAllCohorts({ /* config without startDate */ }, rows);
145
+ summary.successRate;
146
+ ```
147
+
148
+ The cohort engine takes an injectable random source (`RandomSource`) for its
149
+ sampling helpers (e.g. `pickRandomStart`), so simulations stay deterministic and
150
+ testable — pass a seeded RNG like the exported `mulberry32`.
151
+
152
+ ## API stability
153
+
154
+ This package is **0.x**: minor releases may include breaking changes to the two
155
+ flat engine dialects while the surfaces converge. The **`canonical`** module is
156
+ the forward-compatible vocabulary — prefer it for long-lived integrations. The
157
+ exported `VERSION` constant tracks the package version.
158
+
159
+ ## License
160
+
161
+ MIT
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Canonical allocation vocabulary.
3
+ *
4
+ * The two engine dialects disagree on how a portfolio mix is spelled:
5
+ * - Fathom (`src/engine/types.ts`): `Allocation[]` of `{ ticker, weight }`
6
+ * where `weight` is a PERCENT and the weights sum to 100.
7
+ * - retirement-sim (`staging/retirement-sim/types.ts`): `Buckets` of
8
+ * `{ stocks, bonds, cash }` FRACTIONS that sum to 1.0.
9
+ *
10
+ * Canonical decision: allocations are a list of named sleeves whose
11
+ * `fraction` values are in 0..1 and sum to 1.0. Fractions are the honest
12
+ * unit — they compose (a 60/40 of a 50/50 is just multiplication) and never
13
+ * leave you guessing whether "40" meant 40% or 0.40. `assertAllocationSum`
14
+ * enforces the invariant at the boundary; the adapters below translate to and
15
+ * from each dialect without ever assuming the invariant silently.
16
+ */
17
+ import type { PortfolioSpec } from '../engine/types.js';
18
+ /** One named sleeve of a portfolio, as a fraction of the whole (0..1). */
19
+ export interface AllocationSleeve {
20
+ /** Sleeve identifier — a ticker ('SPY') or asset-class key ('stocks'). */
21
+ id: string;
22
+ /** Target weight as a fraction in 0..1. */
23
+ fraction: number;
24
+ }
25
+ /** Canonical allocation: sleeves whose fractions sum to 1.0. */
26
+ export type CanonicalAllocation = AllocationSleeve[];
27
+ /** retirement-sim's three-bucket allocation (fractions summing to 1.0). */
28
+ export interface Buckets {
29
+ stocks: number;
30
+ bonds: number;
31
+ cash: number;
32
+ }
33
+ /** The three canonical bucket ids, in a stable order. */
34
+ export declare const BUCKET_IDS: readonly ["stocks", "bonds", "cash"];
35
+ export type BucketId = (typeof BUCKET_IDS)[number];
36
+ /** Default tolerance for the sum-to-one check (accommodates float drift). */
37
+ export declare const ALLOCATION_SUM_TOLERANCE = 0.000001;
38
+ /**
39
+ * Throw unless `alloc` is a valid canonical allocation: every fraction is a
40
+ * finite number in [0, 1] and the fractions sum to 1.0 within `tolerance`.
41
+ */
42
+ export declare function assertAllocationSum(alloc: CanonicalAllocation, tolerance?: number): void;
43
+ /**
44
+ * Fathom spec → canonical. Percent weights become fractions (÷100). Does NOT
45
+ * assert the sum — callers validate with `assertAllocationSum` if they need
46
+ * the invariant (a spec might legitimately be under construction).
47
+ */
48
+ export declare function fromFathomSpec(spec: PortfolioSpec): CanonicalAllocation;
49
+ /**
50
+ * Canonical → Fathom spec. Fractions become percent weights (×100). `name`
51
+ * is required because a `PortfolioSpec` carries one and the canonical form
52
+ * does not.
53
+ */
54
+ export declare function toFathomSpec(alloc: CanonicalAllocation, name: string): PortfolioSpec;
55
+ /** retirement-sim buckets → canonical (always emits all three sleeves). */
56
+ export declare function fromBuckets(buckets: Buckets): CanonicalAllocation;
57
+ /**
58
+ * Canonical → retirement-sim buckets. Every sleeve id must be one of
59
+ * 'stocks' | 'bonds' | 'cash'; an unrecognized id throws rather than being
60
+ * silently dropped (a lossy mapping must never guess). Missing buckets
61
+ * default to 0. Repeated ids accumulate.
62
+ */
63
+ export declare function toBuckets(alloc: CanonicalAllocation): Buckets;
64
+ //# sourceMappingURL=allocation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"allocation.d.ts","sourceRoot":"","sources":["../../src/canonical/allocation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAkC,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAEvF,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,EAAE,EAAE,MAAM,CAAA;IACV,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,gEAAgE;AAChE,MAAM,MAAM,mBAAmB,GAAG,gBAAgB,EAAE,CAAA;AAEpD,2EAA2E;AAC3E,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb;AAED,yDAAyD;AACzD,eAAO,MAAM,UAAU,sCAAuC,CAAA;AAC9D,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA;AAElD,6EAA6E;AAC7E,eAAO,MAAM,wBAAwB,WAAO,CAAA;AAE5C;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,mBAAmB,EAC1B,SAAS,GAAE,MAAiC,GAC3C,IAAI,CAgBN;AAID;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,aAAa,GAAG,mBAAmB,CAEvE;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,mBAAmB,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAMpF;AAID,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,mBAAmB,CAMjE;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAe7D"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Canonical allocation vocabulary.
3
+ *
4
+ * The two engine dialects disagree on how a portfolio mix is spelled:
5
+ * - Fathom (`src/engine/types.ts`): `Allocation[]` of `{ ticker, weight }`
6
+ * where `weight` is a PERCENT and the weights sum to 100.
7
+ * - retirement-sim (`staging/retirement-sim/types.ts`): `Buckets` of
8
+ * `{ stocks, bonds, cash }` FRACTIONS that sum to 1.0.
9
+ *
10
+ * Canonical decision: allocations are a list of named sleeves whose
11
+ * `fraction` values are in 0..1 and sum to 1.0. Fractions are the honest
12
+ * unit — they compose (a 60/40 of a 50/50 is just multiplication) and never
13
+ * leave you guessing whether "40" meant 40% or 0.40. `assertAllocationSum`
14
+ * enforces the invariant at the boundary; the adapters below translate to and
15
+ * from each dialect without ever assuming the invariant silently.
16
+ */
17
+ /** The three canonical bucket ids, in a stable order. */
18
+ export const BUCKET_IDS = ['stocks', 'bonds', 'cash'];
19
+ /** Default tolerance for the sum-to-one check (accommodates float drift). */
20
+ export const ALLOCATION_SUM_TOLERANCE = 1e-6;
21
+ /**
22
+ * Throw unless `alloc` is a valid canonical allocation: every fraction is a
23
+ * finite number in [0, 1] and the fractions sum to 1.0 within `tolerance`.
24
+ */
25
+ export function assertAllocationSum(alloc, tolerance = ALLOCATION_SUM_TOLERANCE) {
26
+ let sum = 0;
27
+ for (const sleeve of alloc) {
28
+ if (!Number.isFinite(sleeve.fraction)) {
29
+ throw new Error(`Allocation sleeve '${sleeve.id}' has a non-finite fraction`);
30
+ }
31
+ if (sleeve.fraction < 0 || sleeve.fraction > 1) {
32
+ throw new Error(`Allocation sleeve '${sleeve.id}' fraction ${sleeve.fraction} is outside [0, 1]`);
33
+ }
34
+ sum += sleeve.fraction;
35
+ }
36
+ if (Math.abs(sum - 1) > tolerance) {
37
+ throw new Error(`Allocation fractions sum to ${sum}, expected 1.0 (±${tolerance})`);
38
+ }
39
+ }
40
+ // ---- Fathom PortfolioSpec <-> canonical -------------------------------------
41
+ /**
42
+ * Fathom spec → canonical. Percent weights become fractions (÷100). Does NOT
43
+ * assert the sum — callers validate with `assertAllocationSum` if they need
44
+ * the invariant (a spec might legitimately be under construction).
45
+ */
46
+ export function fromFathomSpec(spec) {
47
+ return spec.allocations.map((a) => ({ id: a.ticker, fraction: a.weight / 100 }));
48
+ }
49
+ /**
50
+ * Canonical → Fathom spec. Fractions become percent weights (×100). `name`
51
+ * is required because a `PortfolioSpec` carries one and the canonical form
52
+ * does not.
53
+ */
54
+ export function toFathomSpec(alloc, name) {
55
+ const allocations = alloc.map((s) => ({
56
+ ticker: s.id,
57
+ weight: s.fraction * 100,
58
+ }));
59
+ return { name, allocations };
60
+ }
61
+ // ---- retirement-sim Buckets <-> canonical -----------------------------------
62
+ /** retirement-sim buckets → canonical (always emits all three sleeves). */
63
+ export function fromBuckets(buckets) {
64
+ return [
65
+ { id: 'stocks', fraction: buckets.stocks },
66
+ { id: 'bonds', fraction: buckets.bonds },
67
+ { id: 'cash', fraction: buckets.cash },
68
+ ];
69
+ }
70
+ /**
71
+ * Canonical → retirement-sim buckets. Every sleeve id must be one of
72
+ * 'stocks' | 'bonds' | 'cash'; an unrecognized id throws rather than being
73
+ * silently dropped (a lossy mapping must never guess). Missing buckets
74
+ * default to 0. Repeated ids accumulate.
75
+ */
76
+ export function toBuckets(alloc) {
77
+ const out = { stocks: 0, bonds: 0, cash: 0 };
78
+ for (const sleeve of alloc) {
79
+ const id = sleeve.id;
80
+ // Positive narrowing (id === literal) refines `string` to the bucket union,
81
+ // which negative narrowing would not — so `out[id]` type-checks cleanly.
82
+ if (id === 'stocks' || id === 'bonds' || id === 'cash') {
83
+ out[id] += sleeve.fraction;
84
+ }
85
+ else {
86
+ throw new Error(`Cannot map sleeve '${id}' to a bucket; expected one of ${BUCKET_IDS.join(', ')}`);
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+ //# sourceMappingURL=allocation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"allocation.js","sourceRoot":"","sources":["../../src/canonical/allocation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Canonical date-granularity helpers.
3
+ *
4
+ * Fathom works in daily 'yyyy-mm-dd' keys (`DailyRecord.date`); retirement-sim
5
+ * and the Monte Carlo `RealReturnSeries` work in monthly 'YYYY-MM' keys. The
6
+ * two are easy to confuse because both are dash-delimited strings. Branded
7
+ * types make the granularity part of the static type, so a `DayKey` can never
8
+ * be passed where a `MonthKey` is wanted (or vice-versa) without an explicit,
9
+ * validating conversion.
10
+ */
11
+ /** A calendar month, 'YYYY-MM'. Construct via `parseMonthKey`/`formatMonthKey`. */
12
+ export type MonthKey = string & {
13
+ readonly __brand: 'MonthKey';
14
+ };
15
+ /** A calendar day, 'YYYY-MM-DD'. Construct via `parseDayKey`/`formatDayKey`. */
16
+ export type DayKey = string & {
17
+ readonly __brand: 'DayKey';
18
+ };
19
+ /** Type guard: is `s` a well-formed 'YYYY-MM' month key? */
20
+ export declare function isMonthKey(s: string): s is MonthKey;
21
+ /** Type guard: is `s` a well-formed 'YYYY-MM-DD' day key? */
22
+ export declare function isDayKey(s: string): s is DayKey;
23
+ /** Parse a 'YYYY-MM' string into a `MonthKey`, throwing on malformed input. */
24
+ export declare function parseMonthKey(s: string): MonthKey;
25
+ /** Parse a 'YYYY-MM-DD' string into a `DayKey`, throwing on malformed input. */
26
+ export declare function parseDayKey(s: string): DayKey;
27
+ /** Build a `MonthKey` from a numeric year and month (1..12). */
28
+ export declare function formatMonthKey(year: number, month: number): MonthKey;
29
+ /** Build a `DayKey` from numeric year, month and calendar-valid day. */
30
+ export declare function formatDayKey(year: number, month: number, day: number): DayKey;
31
+ /** The year component of a month key, as a number. */
32
+ export declare function monthKeyYear(key: MonthKey): number;
33
+ /** The month component (1..12) of a month key, as a number. */
34
+ export declare function monthKeyMonth(key: MonthKey): number;
35
+ /** Truncate a day key to its month ('2020-03-17' → '2020-03'). */
36
+ export declare function dayToMonth(day: DayKey): MonthKey;
37
+ /** The first day of a month ('2020-03' → '2020-03-01'). */
38
+ export declare function monthStartDay(month: MonthKey): DayKey;
39
+ /**
40
+ * Normalize any recognized date key to its month. Accepts either a 'YYYY-MM'
41
+ * or 'YYYY-MM-DD' string; throws on anything else. Handy when a series may
42
+ * carry either granularity.
43
+ */
44
+ export declare function toMonthKey(date: string): MonthKey;
45
+ //# sourceMappingURL=dates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dates.d.ts","sourceRoot":"","sources":["../../src/canonical/dates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,mFAAmF;AACnF,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;CAAE,CAAA;AAEhE,gFAAgF;AAChF,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAA;AAK5D,4DAA4D;AAC5D,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,IAAI,QAAQ,CAEnD;AAED,6DAA6D;AAC7D,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,IAAI,MAAM,CAW/C;AAED,+EAA+E;AAC/E,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,QAAQ,CAGjD;AAED,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAG7C;AAED,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,CAQpE;AAED,wEAAwE;AACxE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAO7E;AAED,sDAAsD;AACtD,wBAAgB,YAAY,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,CAElD;AAED,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,CAEnD;AAED,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CAEhD;AAED,2DAA2D;AAC3D,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAErD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAGjD"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Canonical date-granularity helpers.
3
+ *
4
+ * Fathom works in daily 'yyyy-mm-dd' keys (`DailyRecord.date`); retirement-sim
5
+ * and the Monte Carlo `RealReturnSeries` work in monthly 'YYYY-MM' keys. The
6
+ * two are easy to confuse because both are dash-delimited strings. Branded
7
+ * types make the granularity part of the static type, so a `DayKey` can never
8
+ * be passed where a `MonthKey` is wanted (or vice-versa) without an explicit,
9
+ * validating conversion.
10
+ */
11
+ const MONTH_RE = /^\d{4}-(0[1-9]|1[0-2])$/;
12
+ const DAY_RE = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
13
+ /** Type guard: is `s` a well-formed 'YYYY-MM' month key? */
14
+ export function isMonthKey(s) {
15
+ return MONTH_RE.test(s);
16
+ }
17
+ /** Type guard: is `s` a well-formed 'YYYY-MM-DD' day key? */
18
+ export function isDayKey(s) {
19
+ if (!DAY_RE.test(s))
20
+ return false;
21
+ const year = Number(s.slice(0, 4));
22
+ const month = Number(s.slice(5, 7));
23
+ const day = Number(s.slice(8, 10));
24
+ const date = new Date(Date.UTC(year, month - 1, day));
25
+ return (date.getUTCFullYear() === year &&
26
+ date.getUTCMonth() === month - 1 &&
27
+ date.getUTCDate() === day);
28
+ }
29
+ /** Parse a 'YYYY-MM' string into a `MonthKey`, throwing on malformed input. */
30
+ export function parseMonthKey(s) {
31
+ if (!isMonthKey(s))
32
+ throw new Error(`Invalid month key '${s}'; expected 'YYYY-MM'`);
33
+ return s;
34
+ }
35
+ /** Parse a 'YYYY-MM-DD' string into a `DayKey`, throwing on malformed input. */
36
+ export function parseDayKey(s) {
37
+ if (!isDayKey(s))
38
+ throw new Error(`Invalid day key '${s}'; expected 'YYYY-MM-DD'`);
39
+ return s;
40
+ }
41
+ /** Build a `MonthKey` from a numeric year and month (1..12). */
42
+ export function formatMonthKey(year, month) {
43
+ if (!Number.isInteger(month) || month < 1 || month > 12) {
44
+ throw new Error(`Month ${month} out of range 1..12`);
45
+ }
46
+ if (!Number.isInteger(year) || year < 0) {
47
+ throw new Error(`Year ${year} is not a non-negative integer`);
48
+ }
49
+ return `${String(year).padStart(4, '0')}-${String(month).padStart(2, '0')}`;
50
+ }
51
+ /** Build a `DayKey` from numeric year, month and calendar-valid day. */
52
+ export function formatDayKey(year, month, day) {
53
+ if (!Number.isInteger(day) || day < 1 || day > 31) {
54
+ throw new Error(`Day ${day} out of range 1..31`);
55
+ }
56
+ // Reuse the month validation, then validate the composed calendar date.
57
+ const ym = formatMonthKey(year, month);
58
+ return parseDayKey(`${ym}-${String(day).padStart(2, '0')}`);
59
+ }
60
+ /** The year component of a month key, as a number. */
61
+ export function monthKeyYear(key) {
62
+ return Number(key.slice(0, 4));
63
+ }
64
+ /** The month component (1..12) of a month key, as a number. */
65
+ export function monthKeyMonth(key) {
66
+ return Number(key.slice(5, 7));
67
+ }
68
+ /** Truncate a day key to its month ('2020-03-17' → '2020-03'). */
69
+ export function dayToMonth(day) {
70
+ return day.slice(0, 7);
71
+ }
72
+ /** The first day of a month ('2020-03' → '2020-03-01'). */
73
+ export function monthStartDay(month) {
74
+ return `${month}-01`;
75
+ }
76
+ /**
77
+ * Normalize any recognized date key to its month. Accepts either a 'YYYY-MM'
78
+ * or 'YYYY-MM-DD' string; throws on anything else. Handy when a series may
79
+ * carry either granularity.
80
+ */
81
+ export function toMonthKey(date) {
82
+ if (isDayKey(date))
83
+ return dayToMonth(date);
84
+ return parseMonthKey(date);
85
+ }
86
+ //# sourceMappingURL=dates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dates.js","sourceRoot":"","sources":["../../src/canonical/dates.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Canonical type vocabulary + pure conversion adapters bridging the two engine
3
+ * dialects lifted into this repo (Fathom's `src/engine` and the retirement-sim
4
+ * sources under `staging/`). Additive only — nothing here modifies either
5
+ * engine; the adapters translate at the boundary.
6
+ */
7
+ export * from './allocation.js';
8
+ export * from './rebalance.js';
9
+ export * from './dates.js';
10
+ export * from './strategy.js';
11
+ export * from './returns.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/canonical/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Canonical type vocabulary + pure conversion adapters bridging the two engine
3
+ * dialects lifted into this repo (Fathom's `src/engine` and the retirement-sim
4
+ * sources under `staging/`). Additive only — nothing here modifies either
5
+ * engine; the adapters translate at the boundary.
6
+ */
7
+ export * from './allocation.js';
8
+ export * from './rebalance.js';
9
+ export * from './dates.js';
10
+ export * from './strategy.js';
11
+ export * from './returns.js';
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/canonical/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Canonical rebalance-frequency vocabulary.
3
+ *
4
+ * The dialects overlap only partially:
5
+ * - Fathom (`RebalanceFrequency`): 'none' | 'annual' | 'quarterly' | 'monthly'
6
+ * — calendar cadences.
7
+ * - retirement-sim (`RebalancePolicy`): 'none' | 'yearly' | 'every-n-years' |
8
+ * 'on-withdrawal' | 'threshold' — policies, some event- or drift-driven.
9
+ *
10
+ * Canonical union is the superset, with the one genuine synonym collapsed:
11
+ * Fathom's 'annual' and retirement-sim's 'yearly' are the same cadence, spelled
12
+ * 'annual' canonically. Mappings that would lose information (a canonical
13
+ * cadence a target dialect can't express) THROW rather than guess a nearest
14
+ * neighbour.
15
+ */
16
+ import type { RebalanceFrequency } from '../engine/types.js';
17
+ /** retirement-sim's rebalance policy (mirrored structurally; staging is read-only). */
18
+ export type RetirementRebalancePolicy = 'none' | 'yearly' | 'every-n-years' | 'on-withdrawal' | 'threshold';
19
+ /** Superset covering both dialects; 'annual' subsumes retirement-sim 'yearly'. */
20
+ export type CanonicalRebalance = 'none' | 'monthly' | 'quarterly' | 'annual' | 'every-n-years' | 'on-withdrawal' | 'threshold';
21
+ /** Fathom → canonical. Total (every Fathom value is a valid canonical value). */
22
+ export declare function fromFathomRebalance(freq: RebalanceFrequency): CanonicalRebalance;
23
+ /**
24
+ * Canonical → Fathom. Fathom has no notion of the event/drift policies, so
25
+ * 'every-n-years' | 'on-withdrawal' | 'threshold' throw.
26
+ */
27
+ export declare function toFathomRebalance(rebalance: CanonicalRebalance): RebalanceFrequency;
28
+ /** retirement-sim → canonical. Only 'yearly' is renamed (→ 'annual'). */
29
+ export declare function fromRetirementRebalance(policy: RetirementRebalancePolicy): CanonicalRebalance;
30
+ /**
31
+ * Canonical → retirement-sim. retirement-sim has no sub-annual calendar
32
+ * cadence, so 'monthly' | 'quarterly' throw; 'annual' becomes 'yearly'.
33
+ */
34
+ export declare function toRetirementRebalance(rebalance: CanonicalRebalance): RetirementRebalancePolicy;
35
+ //# sourceMappingURL=rebalance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rebalance.d.ts","sourceRoot":"","sources":["../../src/canonical/rebalance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAE5D,uFAAuF;AACvF,MAAM,MAAM,yBAAyB,GACjC,MAAM,GACN,QAAQ,GACR,eAAe,GACf,eAAe,GACf,WAAW,CAAA;AAEf,kFAAkF;AAClF,MAAM,MAAM,kBAAkB,GAC1B,MAAM,GACN,SAAS,GACT,WAAW,GACX,QAAQ,GACR,eAAe,GACf,eAAe,GACf,WAAW,CAAA;AAEf,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,kBAAkB,GAAG,kBAAkB,CAEhF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,kBAAkB,GAAG,kBAAkB,CAYnF;AAED,yEAAyE;AACzE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,yBAAyB,GAAG,kBAAkB,CAE7F;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,kBAAkB,GAC5B,yBAAyB,CAc3B"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Canonical rebalance-frequency vocabulary.
3
+ *
4
+ * The dialects overlap only partially:
5
+ * - Fathom (`RebalanceFrequency`): 'none' | 'annual' | 'quarterly' | 'monthly'
6
+ * — calendar cadences.
7
+ * - retirement-sim (`RebalancePolicy`): 'none' | 'yearly' | 'every-n-years' |
8
+ * 'on-withdrawal' | 'threshold' — policies, some event- or drift-driven.
9
+ *
10
+ * Canonical union is the superset, with the one genuine synonym collapsed:
11
+ * Fathom's 'annual' and retirement-sim's 'yearly' are the same cadence, spelled
12
+ * 'annual' canonically. Mappings that would lose information (a canonical
13
+ * cadence a target dialect can't express) THROW rather than guess a nearest
14
+ * neighbour.
15
+ */
16
+ /** Fathom → canonical. Total (every Fathom value is a valid canonical value). */
17
+ export function fromFathomRebalance(freq) {
18
+ return freq;
19
+ }
20
+ /**
21
+ * Canonical → Fathom. Fathom has no notion of the event/drift policies, so
22
+ * 'every-n-years' | 'on-withdrawal' | 'threshold' throw.
23
+ */
24
+ export function toFathomRebalance(rebalance) {
25
+ switch (rebalance) {
26
+ case 'none':
27
+ case 'monthly':
28
+ case 'quarterly':
29
+ case 'annual':
30
+ return rebalance;
31
+ default:
32
+ throw new Error(`Canonical rebalance '${rebalance}' has no Fathom RebalanceFrequency equivalent`);
33
+ }
34
+ }
35
+ /** retirement-sim → canonical. Only 'yearly' is renamed (→ 'annual'). */
36
+ export function fromRetirementRebalance(policy) {
37
+ return policy === 'yearly' ? 'annual' : policy;
38
+ }
39
+ /**
40
+ * Canonical → retirement-sim. retirement-sim has no sub-annual calendar
41
+ * cadence, so 'monthly' | 'quarterly' throw; 'annual' becomes 'yearly'.
42
+ */
43
+ export function toRetirementRebalance(rebalance) {
44
+ switch (rebalance) {
45
+ case 'annual':
46
+ return 'yearly';
47
+ case 'none':
48
+ case 'every-n-years':
49
+ case 'on-withdrawal':
50
+ case 'threshold':
51
+ return rebalance;
52
+ default:
53
+ throw new Error(`Canonical rebalance '${rebalance}' has no retirement-sim RebalancePolicy equivalent`);
54
+ }
55
+ }
56
+ //# sourceMappingURL=rebalance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rebalance.js","sourceRoot":"","sources":["../../src/canonical/rebalance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAsBH,iFAAiF;AACjF,MAAM,UAAU,mBAAmB,CAAC,IAAwB;IAC1D,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAA6B;IAC7D,QAAQ,SAAS,EAAE,CAAC;QAClB,KAAK,MAAM,CAAC;QACZ,KAAK,SAAS,CAAC;QACf,KAAK,WAAW,CAAC;QACjB,KAAK,QAAQ;YACX,OAAO,SAAS,CAAA;QAClB;YACE,MAAM,IAAI,KAAK,CACb,wBAAwB,SAAS,+CAA+C,CACjF,CAAA;IACL,CAAC;AACH,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,uBAAuB,CAAC,MAAiC;IACvE,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAA;AAChD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACnC,SAA6B;IAE7B,QAAQ,SAAS,EAAE,CAAC;QAClB,KAAK,QAAQ;YACX,OAAO,QAAQ,CAAA;QACjB,KAAK,MAAM,CAAC;QACZ,KAAK,eAAe,CAAC;QACrB,KAAK,eAAe,CAAC;QACrB,KAAK,WAAW;YACd,OAAO,SAAS,CAAA;QAClB;YACE,MAAM,IAAI,KAAK,CACb,wBAAwB,SAAS,oDAAoD,CACtF,CAAA;IACL,CAAC;AACH,CAAC"}