@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.
- package/LICENSE +21 -0
- package/README.md +161 -0
- package/dist/canonical/allocation.d.ts +64 -0
- package/dist/canonical/allocation.d.ts.map +1 -0
- package/dist/canonical/allocation.js +91 -0
- package/dist/canonical/allocation.js.map +1 -0
- package/dist/canonical/dates.d.ts +45 -0
- package/dist/canonical/dates.d.ts.map +1 -0
- package/dist/canonical/dates.js +86 -0
- package/dist/canonical/dates.js.map +1 -0
- package/dist/canonical/index.d.ts +12 -0
- package/dist/canonical/index.d.ts.map +1 -0
- package/dist/canonical/index.js +12 -0
- package/dist/canonical/index.js.map +1 -0
- package/dist/canonical/rebalance.d.ts +35 -0
- package/dist/canonical/rebalance.d.ts.map +1 -0
- package/dist/canonical/rebalance.js +56 -0
- package/dist/canonical/rebalance.js.map +1 -0
- package/dist/canonical/returns.d.ts +61 -0
- package/dist/canonical/returns.d.ts.map +1 -0
- package/dist/canonical/returns.js +65 -0
- package/dist/canonical/returns.js.map +1 -0
- package/dist/canonical/strategy.d.ts +62 -0
- package/dist/canonical/strategy.d.ts.map +1 -0
- package/dist/canonical/strategy.js +103 -0
- package/dist/canonical/strategy.js.map +1 -0
- package/dist/engine/align.d.ts +34 -0
- package/dist/engine/align.d.ts.map +1 -0
- package/dist/engine/align.js +97 -0
- package/dist/engine/align.js.map +1 -0
- package/dist/engine/backtest.d.ts +7 -0
- package/dist/engine/backtest.d.ts.map +1 -0
- package/dist/engine/backtest.js +95 -0
- package/dist/engine/backtest.js.map +1 -0
- package/dist/engine/frontier.d.ts +25 -0
- package/dist/engine/frontier.d.ts.map +1 -0
- package/dist/engine/frontier.js +40 -0
- package/dist/engine/frontier.js.map +1 -0
- package/dist/engine/index.d.ts +6 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +5 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/metrics.d.ts +27 -0
- package/dist/engine/metrics.d.ts.map +1 -0
- package/dist/engine/metrics.js +202 -0
- package/dist/engine/metrics.js.map +1 -0
- package/dist/engine/types.d.ts +118 -0
- package/dist/engine/types.d.ts.map +1 -0
- package/dist/engine/types.js +2 -0
- package/dist/engine/types.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/montecarlo/data.d.ts +17 -0
- package/dist/montecarlo/data.d.ts.map +1 -0
- package/dist/montecarlo/data.js +38 -0
- package/dist/montecarlo/data.js.map +1 -0
- package/dist/montecarlo/simulate.d.ts +106 -0
- package/dist/montecarlo/simulate.d.ts.map +1 -0
- package/dist/montecarlo/simulate.js +264 -0
- package/dist/montecarlo/simulate.js.map +1 -0
- package/dist/retirement/bucketRules.d.ts +3 -0
- package/dist/retirement/bucketRules.d.ts.map +1 -0
- package/dist/retirement/bucketRules.js +37 -0
- package/dist/retirement/bucketRules.js.map +1 -0
- package/dist/retirement/engine.d.ts +7 -0
- package/dist/retirement/engine.d.ts.map +1 -0
- package/dist/retirement/engine.js +297 -0
- package/dist/retirement/engine.js.map +1 -0
- package/dist/retirement/index.d.ts +6 -0
- package/dist/retirement/index.d.ts.map +1 -0
- package/dist/retirement/index.js +6 -0
- package/dist/retirement/index.js.map +1 -0
- package/dist/retirement/runner.d.ts +15 -0
- package/dist/retirement/runner.d.ts.map +1 -0
- package/dist/retirement/runner.js +178 -0
- package/dist/retirement/runner.js.map +1 -0
- package/dist/retirement/strategies/index.d.ts +30 -0
- package/dist/retirement/strategies/index.d.ts.map +1 -0
- package/dist/retirement/strategies/index.js +270 -0
- package/dist/retirement/strategies/index.js.map +1 -0
- package/dist/retirement/types.d.ts +206 -0
- package/dist/retirement/types.d.ts.map +1 -0
- package/dist/retirement/types.js +2 -0
- package/dist/retirement/types.js.map +1 -0
- 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"}
|