@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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical monthly real-return series + adapters into it.
|
|
3
|
+
*
|
|
4
|
+
* The canonical series is the Monte Carlo `RealReturnSeries` (monthly, real,
|
|
5
|
+
* fractional) — it is what `runHistoricalSequence`/`runBootstrap` consume. The
|
|
6
|
+
* two upstream shapes are:
|
|
7
|
+
* - Fathom `TickerSeries`: daily nominal price records (handled elsewhere by
|
|
8
|
+
* the engine's alignment layer, not here).
|
|
9
|
+
* - retirement-sim `HistoricalRow`: monthly nominal returns per asset class
|
|
10
|
+
* plus a CPI level.
|
|
11
|
+
*
|
|
12
|
+
* `historicalRowsToRealReturns` converts the monthly rows using the same
|
|
13
|
+
* real-return math as `montecarlo/data.ts`: blend the nominal per-asset returns
|
|
14
|
+
* by allocation, then deflate by month-over-month CPI growth. The bond/cash
|
|
15
|
+
* composition is unambiguous here (each row carries a separate bond and cash
|
|
16
|
+
* return), so the default blend is the allocation-weighted average — but the
|
|
17
|
+
* blend is exposed as an explicit parameter for callers whose asset set or
|
|
18
|
+
* weighting differs.
|
|
19
|
+
*/
|
|
20
|
+
import type { RealReturnSeries } from '../montecarlo/simulate.js';
|
|
21
|
+
import type { Buckets } from './allocation.js';
|
|
22
|
+
/**
|
|
23
|
+
* retirement-sim's monthly historical row (mirrored structurally; staging is
|
|
24
|
+
* read-only). Returns are NOMINAL monthly fractions; `cpi` is an index level.
|
|
25
|
+
*/
|
|
26
|
+
export interface HistoricalRow {
|
|
27
|
+
/** 'YYYY-MM' (or 'YYYY-MM-DD', normalized to month on output). */
|
|
28
|
+
date: string;
|
|
29
|
+
/** Nominal monthly stock (S&P) return. */
|
|
30
|
+
spReturn: number;
|
|
31
|
+
/** Nominal monthly bond return. */
|
|
32
|
+
bondReturn: number;
|
|
33
|
+
/** Nominal monthly cash return. */
|
|
34
|
+
cashReturn: number;
|
|
35
|
+
/** CPI index level for the month. */
|
|
36
|
+
cpi: number;
|
|
37
|
+
/** Optional Shiller CAPE for the month. */
|
|
38
|
+
cape?: number;
|
|
39
|
+
}
|
|
40
|
+
/** Blends a row's per-asset nominal returns into one portfolio nominal return. */
|
|
41
|
+
export type ReturnBlend = (row: HistoricalRow, allocation: Buckets) => number;
|
|
42
|
+
/**
|
|
43
|
+
* Default blend: allocation-weighted average of the three bucket returns,
|
|
44
|
+
* normalized by the total weight so unnormalized buckets still behave. This
|
|
45
|
+
* matches `buildRealReturns`'s weighting convention.
|
|
46
|
+
*/
|
|
47
|
+
export declare const weightedBucketBlend: ReturnBlend;
|
|
48
|
+
export interface HistoricalRowsOptions {
|
|
49
|
+
/** Override the per-row nominal blend (default: `weightedBucketBlend`). */
|
|
50
|
+
blend?: ReturnBlend;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Convert monthly `HistoricalRow`s into a canonical `RealReturnSeries`.
|
|
54
|
+
*
|
|
55
|
+
* For each month i ≥ 1: inflation = cpi[i] / cpi[i-1]; nominal = blend(row[i]);
|
|
56
|
+
* real = (1 + nominal) / inflation − 1. The label emitted is month i (the
|
|
57
|
+
* month the return occurs in), matching `buildRealReturns`. The first row seeds
|
|
58
|
+
* the CPI base and produces no return, so a series of N rows yields N−1 points.
|
|
59
|
+
*/
|
|
60
|
+
export declare function historicalRowsToRealReturns(rows: HistoricalRow[], allocation: Buckets, options?: HistoricalRowsOptions): RealReturnSeries;
|
|
61
|
+
//# sourceMappingURL=returns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"returns.d.ts","sourceRoot":"","sources":["../../src/canonical/returns.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AACjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAG9C;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAA;IACZ,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAA;IAChB,mCAAmC;IACnC,UAAU,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,UAAU,EAAE,MAAM,CAAA;IAClB,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAA;IACX,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,kFAAkF;AAClF,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,KAAK,MAAM,CAAA;AAE7E;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,WAQjC,CAAA;AAED,MAAM,WAAW,qBAAqB;IACpC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,WAAW,CAAA;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,aAAa,EAAE,EACrB,UAAU,EAAE,OAAO,EACnB,OAAO,GAAE,qBAA0B,GAClC,gBAAgB,CAoBlB"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical monthly real-return series + adapters into it.
|
|
3
|
+
*
|
|
4
|
+
* The canonical series is the Monte Carlo `RealReturnSeries` (monthly, real,
|
|
5
|
+
* fractional) — it is what `runHistoricalSequence`/`runBootstrap` consume. The
|
|
6
|
+
* two upstream shapes are:
|
|
7
|
+
* - Fathom `TickerSeries`: daily nominal price records (handled elsewhere by
|
|
8
|
+
* the engine's alignment layer, not here).
|
|
9
|
+
* - retirement-sim `HistoricalRow`: monthly nominal returns per asset class
|
|
10
|
+
* plus a CPI level.
|
|
11
|
+
*
|
|
12
|
+
* `historicalRowsToRealReturns` converts the monthly rows using the same
|
|
13
|
+
* real-return math as `montecarlo/data.ts`: blend the nominal per-asset returns
|
|
14
|
+
* by allocation, then deflate by month-over-month CPI growth. The bond/cash
|
|
15
|
+
* composition is unambiguous here (each row carries a separate bond and cash
|
|
16
|
+
* return), so the default blend is the allocation-weighted average — but the
|
|
17
|
+
* blend is exposed as an explicit parameter for callers whose asset set or
|
|
18
|
+
* weighting differs.
|
|
19
|
+
*/
|
|
20
|
+
import { toMonthKey } from './dates.js';
|
|
21
|
+
/**
|
|
22
|
+
* Default blend: allocation-weighted average of the three bucket returns,
|
|
23
|
+
* normalized by the total weight so unnormalized buckets still behave. This
|
|
24
|
+
* matches `buildRealReturns`'s weighting convention.
|
|
25
|
+
*/
|
|
26
|
+
export const weightedBucketBlend = (row, allocation) => {
|
|
27
|
+
const total = allocation.stocks + allocation.bonds + allocation.cash;
|
|
28
|
+
if (total <= 0)
|
|
29
|
+
return 0;
|
|
30
|
+
const weighted = allocation.stocks * row.spReturn +
|
|
31
|
+
allocation.bonds * row.bondReturn +
|
|
32
|
+
allocation.cash * row.cashReturn;
|
|
33
|
+
return weighted / total;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Convert monthly `HistoricalRow`s into a canonical `RealReturnSeries`.
|
|
37
|
+
*
|
|
38
|
+
* For each month i ≥ 1: inflation = cpi[i] / cpi[i-1]; nominal = blend(row[i]);
|
|
39
|
+
* real = (1 + nominal) / inflation − 1. The label emitted is month i (the
|
|
40
|
+
* month the return occurs in), matching `buildRealReturns`. The first row seeds
|
|
41
|
+
* the CPI base and produces no return, so a series of N rows yields N−1 points.
|
|
42
|
+
*/
|
|
43
|
+
export function historicalRowsToRealReturns(rows, allocation, options = {}) {
|
|
44
|
+
const blend = options.blend ?? weightedBucketBlend;
|
|
45
|
+
const dates = [];
|
|
46
|
+
const returns = [];
|
|
47
|
+
for (let i = 1; i < rows.length; i++) {
|
|
48
|
+
const prev = rows[i - 1];
|
|
49
|
+
const cur = rows[i];
|
|
50
|
+
if (!prev || !cur)
|
|
51
|
+
continue;
|
|
52
|
+
if (!Number.isFinite(prev.cpi) || prev.cpi <= 0) {
|
|
53
|
+
throw new Error(`Invalid CPI at '${prev.date}': expected a positive finite value`);
|
|
54
|
+
}
|
|
55
|
+
if (!Number.isFinite(cur.cpi) || cur.cpi <= 0) {
|
|
56
|
+
throw new Error(`Invalid CPI at '${cur.date}': expected a positive finite value`);
|
|
57
|
+
}
|
|
58
|
+
const inflation = cur.cpi / prev.cpi;
|
|
59
|
+
const nominal = blend(cur, allocation);
|
|
60
|
+
dates.push(toMonthKey(cur.date));
|
|
61
|
+
returns.push((1 + nominal) / inflation - 1);
|
|
62
|
+
}
|
|
63
|
+
return { dates, returns };
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=returns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"returns.js","sourceRoot":"","sources":["../../src/canonical/returns.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAwBvC;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAgB,CAAC,GAAG,EAAE,UAAU,EAAE,EAAE;IAClE,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,GAAG,UAAU,CAAC,KAAK,GAAG,UAAU,CAAC,IAAI,CAAA;IACpE,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,CAAC,CAAA;IACxB,MAAM,QAAQ,GACZ,UAAU,CAAC,MAAM,GAAG,GAAG,CAAC,QAAQ;QAChC,UAAU,CAAC,KAAK,GAAG,GAAG,CAAC,UAAU;QACjC,UAAU,CAAC,IAAI,GAAG,GAAG,CAAC,UAAU,CAAA;IAClC,OAAO,QAAQ,GAAG,KAAK,CAAA;AACzB,CAAC,CAAA;AAOD;;;;;;;GAOG;AACH,MAAM,UAAU,2BAA2B,CACzC,IAAqB,EACrB,UAAmB,EACnB,UAAiC,EAAE;IAEnC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,mBAAmB,CAAA;IAClD,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;QACxB,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;QACnB,IAAI,CAAC,IAAI,IAAI,CAAC,GAAG;YAAE,SAAQ;QAC3B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;YAChD,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,CAAC,IAAI,qCAAqC,CAAC,CAAA;QACpF,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;YAC9C,MAAM,IAAI,KAAK,CAAC,mBAAmB,GAAG,CAAC,IAAI,qCAAqC,CAAC,CAAA;QACnF,CAAC;QACD,MAAM,SAAS,GAAG,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAA;QACpC,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,EAAE,UAAU,CAAC,CAAA;QACtC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;QAChC,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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical withdrawal-strategy ids.
|
|
3
|
+
*
|
|
4
|
+
* Three vocabularies name the same family of rules differently:
|
|
5
|
+
* - Monte Carlo (`src/montecarlo/simulate.ts`): 'fixedReal' | 'fixedPercent' |
|
|
6
|
+
* 'vpw' | 'guardrails' — a camelCase four.
|
|
7
|
+
* - retirement-sim registry (`staging/retirement-sim/strategies/index.ts`):
|
|
8
|
+
* a kebab-case superset ('constant-dollar', 'percent-of-portfolio',
|
|
9
|
+
* 'one-over-n', 'vpw', 'guyton-klinger', 'cape-based', '95-percent',
|
|
10
|
+
* 'endowment', 'vanguard-dynamic', plus legacy aliases).
|
|
11
|
+
*
|
|
12
|
+
* Canonical ids are the kebab-case superset (they already read as the shared
|
|
13
|
+
* vocabulary). Mappings carry an `approximate` flag and a `note` whenever two
|
|
14
|
+
* ids name rules that are close but NOT identical — so the difference is
|
|
15
|
+
* documented at the boundary instead of being silently equated. The Monte
|
|
16
|
+
* Carlo 'guardrails' rule is a simplified 20%/10% band; it is mapped to
|
|
17
|
+
* 'guyton-klinger' as an APPROXIMATION, never as an equality.
|
|
18
|
+
*/
|
|
19
|
+
import type { WithdrawalStrategy as MontecarloStrategy } from '../montecarlo/simulate.js';
|
|
20
|
+
/** Monte Carlo strategy id (re-export of the camelCase union). */
|
|
21
|
+
export type MontecarloStrategyId = MontecarloStrategy;
|
|
22
|
+
/** Canonical, kebab-case strategy ids (the retirement-sim "new kinds" superset). */
|
|
23
|
+
export type CanonicalStrategyId = 'constant-dollar' | 'percent-of-portfolio' | 'one-over-n' | 'vpw' | 'guyton-klinger' | 'cape-based' | '95-percent' | 'endowment' | 'vanguard-dynamic';
|
|
24
|
+
export declare const CANONICAL_STRATEGY_IDS: readonly CanonicalStrategyId[];
|
|
25
|
+
/**
|
|
26
|
+
* retirement-sim `StrategyKind`: the canonical superset plus legacy aliases
|
|
27
|
+
* and the non-rule 'interactive' mode (mirrored structurally; staging is
|
|
28
|
+
* read-only).
|
|
29
|
+
*/
|
|
30
|
+
export type RetirementStrategyKind = CanonicalStrategyId | 'fixed-real' | 'fixed-pct' | 'guardrails' | 'interactive';
|
|
31
|
+
/** Result of a strategy-id mapping: the canonical id + whether it's exact. */
|
|
32
|
+
export interface StrategyMapping<Id extends string = CanonicalStrategyId> {
|
|
33
|
+
id: Id;
|
|
34
|
+
/** True when the source rule is only approximately the mapped rule. */
|
|
35
|
+
approximate: boolean;
|
|
36
|
+
/** Present only for approximate mappings: what the approximation glosses over. */
|
|
37
|
+
note?: string;
|
|
38
|
+
}
|
|
39
|
+
/** Monte Carlo id → canonical id. Exact except 'guardrails' (≈ Guyton-Klinger). */
|
|
40
|
+
export declare const MONTECARLO_TO_CANONICAL: Record<MontecarloStrategyId, StrategyMapping>;
|
|
41
|
+
/** Map a Monte Carlo strategy id to its canonical id. */
|
|
42
|
+
export declare function montecarloToCanonicalStrategy(id: MontecarloStrategyId): StrategyMapping;
|
|
43
|
+
/**
|
|
44
|
+
* Map a canonical id back to a Monte Carlo id. The Monte Carlo engine only
|
|
45
|
+
* implements four rules, so canonical ids outside that set (one-over-n,
|
|
46
|
+
* cape-based, 95-percent, endowment, vanguard-dynamic) have no equivalent and
|
|
47
|
+
* throw rather than being coerced to a nearest neighbour.
|
|
48
|
+
*/
|
|
49
|
+
export declare function canonicalToMontecarloStrategy(id: CanonicalStrategyId): StrategyMapping<MontecarloStrategyId>;
|
|
50
|
+
/**
|
|
51
|
+
* Map a retirement-sim `StrategyKind` to its canonical id. The "new kinds" are
|
|
52
|
+
* already canonical (identity); legacy aliases fold onto their modern id, and
|
|
53
|
+
* the non-rule 'interactive' mode throws (it has no withdrawal-rule id).
|
|
54
|
+
*/
|
|
55
|
+
export declare function retirementToCanonicalStrategy(kind: RetirementStrategyKind): StrategyMapping;
|
|
56
|
+
/**
|
|
57
|
+
* Map a canonical id to retirement-sim's modern strategy id. Canonical ids
|
|
58
|
+
* were deliberately chosen from the modern retirement-sim vocabulary, so
|
|
59
|
+
* this direction is exact and total.
|
|
60
|
+
*/
|
|
61
|
+
export declare function canonicalToRetirementStrategy(id: CanonicalStrategyId): StrategyMapping<CanonicalStrategyId>;
|
|
62
|
+
//# sourceMappingURL=strategy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strategy.d.ts","sourceRoot":"","sources":["../../src/canonical/strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,kBAAkB,IAAI,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAEzF,kEAAkE;AAClE,MAAM,MAAM,oBAAoB,GAAG,kBAAkB,CAAA;AAErD,oFAAoF;AACpF,MAAM,MAAM,mBAAmB,GAC3B,iBAAiB,GACjB,sBAAsB,GACtB,YAAY,GACZ,KAAK,GACL,gBAAgB,GAChB,YAAY,GACZ,YAAY,GACZ,WAAW,GACX,kBAAkB,CAAA;AAEtB,eAAO,MAAM,sBAAsB,EAAE,SAAS,mBAAmB,EAUhE,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAC9B,mBAAmB,GACnB,YAAY,GACZ,WAAW,GACX,YAAY,GACZ,aAAa,CAAA;AAEjB,8EAA8E;AAC9E,MAAM,WAAW,eAAe,CAAC,EAAE,SAAS,MAAM,GAAG,mBAAmB;IACtE,EAAE,EAAE,EAAE,CAAA;IACN,uEAAuE;IACvE,WAAW,EAAE,OAAO,CAAA;IACpB,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AASD,mFAAmF;AACnF,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,oBAAoB,EAAE,eAAe,CAKjF,CAAA;AAED,yDAAyD;AACzD,wBAAgB,6BAA6B,CAAC,EAAE,EAAE,oBAAoB,GAAG,eAAe,CAIvF;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,EAAE,EAAE,mBAAmB,GACtB,eAAe,CAAC,oBAAoB,CAAC,CAavC;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,sBAAsB,GAAG,eAAe,CAoB3F;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,EAAE,EAAE,mBAAmB,GACtB,eAAe,CAAC,mBAAmB,CAAC,CAEtC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical withdrawal-strategy ids.
|
|
3
|
+
*
|
|
4
|
+
* Three vocabularies name the same family of rules differently:
|
|
5
|
+
* - Monte Carlo (`src/montecarlo/simulate.ts`): 'fixedReal' | 'fixedPercent' |
|
|
6
|
+
* 'vpw' | 'guardrails' — a camelCase four.
|
|
7
|
+
* - retirement-sim registry (`staging/retirement-sim/strategies/index.ts`):
|
|
8
|
+
* a kebab-case superset ('constant-dollar', 'percent-of-portfolio',
|
|
9
|
+
* 'one-over-n', 'vpw', 'guyton-klinger', 'cape-based', '95-percent',
|
|
10
|
+
* 'endowment', 'vanguard-dynamic', plus legacy aliases).
|
|
11
|
+
*
|
|
12
|
+
* Canonical ids are the kebab-case superset (they already read as the shared
|
|
13
|
+
* vocabulary). Mappings carry an `approximate` flag and a `note` whenever two
|
|
14
|
+
* ids name rules that are close but NOT identical — so the difference is
|
|
15
|
+
* documented at the boundary instead of being silently equated. The Monte
|
|
16
|
+
* Carlo 'guardrails' rule is a simplified 20%/10% band; it is mapped to
|
|
17
|
+
* 'guyton-klinger' as an APPROXIMATION, never as an equality.
|
|
18
|
+
*/
|
|
19
|
+
export const CANONICAL_STRATEGY_IDS = [
|
|
20
|
+
'constant-dollar',
|
|
21
|
+
'percent-of-portfolio',
|
|
22
|
+
'one-over-n',
|
|
23
|
+
'vpw',
|
|
24
|
+
'guyton-klinger',
|
|
25
|
+
'cape-based',
|
|
26
|
+
'95-percent',
|
|
27
|
+
'endowment',
|
|
28
|
+
'vanguard-dynamic',
|
|
29
|
+
];
|
|
30
|
+
const GUARDRAILS_NOTE = "Monte Carlo 'guardrails' is a simplified Guyton-Klinger: it cuts the real " +
|
|
31
|
+
'withdrawal 10% when the current rate drifts 20% above the initial rate and ' +
|
|
32
|
+
'raises it 10% when 20% below. Full Guyton-Klinger additionally applies an ' +
|
|
33
|
+
'inflation cap, a post-loss inflation skip, and a preservation/prosperity ' +
|
|
34
|
+
'timing window. Treat as an approximation, not an equivalence.';
|
|
35
|
+
/** Monte Carlo id → canonical id. Exact except 'guardrails' (≈ Guyton-Klinger). */
|
|
36
|
+
export const MONTECARLO_TO_CANONICAL = {
|
|
37
|
+
fixedReal: { id: 'constant-dollar', approximate: false },
|
|
38
|
+
fixedPercent: { id: 'percent-of-portfolio', approximate: false },
|
|
39
|
+
vpw: { id: 'vpw', approximate: false },
|
|
40
|
+
guardrails: { id: 'guyton-klinger', approximate: true, note: GUARDRAILS_NOTE },
|
|
41
|
+
};
|
|
42
|
+
/** Map a Monte Carlo strategy id to its canonical id. */
|
|
43
|
+
export function montecarloToCanonicalStrategy(id) {
|
|
44
|
+
const mapping = MONTECARLO_TO_CANONICAL[id];
|
|
45
|
+
if (!mapping)
|
|
46
|
+
throw new Error(`Unknown Monte Carlo strategy '${id}'`);
|
|
47
|
+
return mapping;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Map a canonical id back to a Monte Carlo id. The Monte Carlo engine only
|
|
51
|
+
* implements four rules, so canonical ids outside that set (one-over-n,
|
|
52
|
+
* cape-based, 95-percent, endowment, vanguard-dynamic) have no equivalent and
|
|
53
|
+
* throw rather than being coerced to a nearest neighbour.
|
|
54
|
+
*/
|
|
55
|
+
export function canonicalToMontecarloStrategy(id) {
|
|
56
|
+
switch (id) {
|
|
57
|
+
case 'constant-dollar':
|
|
58
|
+
return { id: 'fixedReal', approximate: false };
|
|
59
|
+
case 'percent-of-portfolio':
|
|
60
|
+
return { id: 'fixedPercent', approximate: false };
|
|
61
|
+
case 'vpw':
|
|
62
|
+
return { id: 'vpw', approximate: false };
|
|
63
|
+
case 'guyton-klinger':
|
|
64
|
+
return { id: 'guardrails', approximate: true, note: GUARDRAILS_NOTE };
|
|
65
|
+
default:
|
|
66
|
+
throw new Error(`Canonical strategy '${id}' has no Monte Carlo equivalent`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Map a retirement-sim `StrategyKind` to its canonical id. The "new kinds" are
|
|
71
|
+
* already canonical (identity); legacy aliases fold onto their modern id, and
|
|
72
|
+
* the non-rule 'interactive' mode throws (it has no withdrawal-rule id).
|
|
73
|
+
*/
|
|
74
|
+
export function retirementToCanonicalStrategy(kind) {
|
|
75
|
+
if (CANONICAL_STRATEGY_IDS.includes(kind)) {
|
|
76
|
+
return { id: kind, approximate: false };
|
|
77
|
+
}
|
|
78
|
+
switch (kind) {
|
|
79
|
+
case 'fixed-real':
|
|
80
|
+
return { id: 'constant-dollar', approximate: false };
|
|
81
|
+
case 'fixed-pct':
|
|
82
|
+
return { id: 'percent-of-portfolio', approximate: false };
|
|
83
|
+
case 'guardrails':
|
|
84
|
+
return {
|
|
85
|
+
id: 'guyton-klinger',
|
|
86
|
+
approximate: true,
|
|
87
|
+
note: "Legacy 'guardrails' is a simplified guardrail rule; Guyton-Klinger is the full ruleset.",
|
|
88
|
+
};
|
|
89
|
+
case 'interactive':
|
|
90
|
+
throw new Error("retirement-sim 'interactive' has no canonical withdrawal-rule id");
|
|
91
|
+
default:
|
|
92
|
+
throw new Error(`Unknown retirement-sim strategy kind '${kind}'`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Map a canonical id to retirement-sim's modern strategy id. Canonical ids
|
|
97
|
+
* were deliberately chosen from the modern retirement-sim vocabulary, so
|
|
98
|
+
* this direction is exact and total.
|
|
99
|
+
*/
|
|
100
|
+
export function canonicalToRetirementStrategy(id) {
|
|
101
|
+
return { id, approximate: false };
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=strategy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strategy.js","sourceRoot":"","sources":["../../src/canonical/strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAmBH,MAAM,CAAC,MAAM,sBAAsB,GAAmC;IACpE,iBAAiB;IACjB,sBAAsB;IACtB,YAAY;IACZ,KAAK;IACL,gBAAgB;IAChB,YAAY;IACZ,YAAY;IACZ,WAAW;IACX,kBAAkB;CACnB,CAAA;AAuBD,MAAM,eAAe,GACnB,4EAA4E;IAC5E,6EAA6E;IAC7E,4EAA4E;IAC5E,2EAA2E;IAC3E,+DAA+D,CAAA;AAEjE,mFAAmF;AACnF,MAAM,CAAC,MAAM,uBAAuB,GAAkD;IACpF,SAAS,EAAE,EAAE,EAAE,EAAE,iBAAiB,EAAE,WAAW,EAAE,KAAK,EAAE;IACxD,YAAY,EAAE,EAAE,EAAE,EAAE,sBAAsB,EAAE,WAAW,EAAE,KAAK,EAAE;IAChE,GAAG,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE;IACtC,UAAU,EAAE,EAAE,EAAE,EAAE,gBAAgB,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,eAAe,EAAE;CAC/E,CAAA;AAED,yDAAyD;AACzD,MAAM,UAAU,6BAA6B,CAAC,EAAwB;IACpE,MAAM,OAAO,GAAG,uBAAuB,CAAC,EAAE,CAAC,CAAA;IAC3C,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,EAAE,GAAG,CAAC,CAAA;IACrE,OAAO,OAAO,CAAA;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,6BAA6B,CAC3C,EAAuB;IAEvB,QAAQ,EAAE,EAAE,CAAC;QACX,KAAK,iBAAiB;YACpB,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,CAAA;QAChD,KAAK,sBAAsB;YACzB,OAAO,EAAE,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,KAAK,EAAE,CAAA;QACnD,KAAK,KAAK;YACR,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,CAAA;QAC1C,KAAK,gBAAgB;YACnB,OAAO,EAAE,EAAE,EAAE,YAAY,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,eAAe,EAAE,CAAA;QACvE;YACE,MAAM,IAAI,KAAK,CAAC,uBAAuB,EAAE,iCAAiC,CAAC,CAAA;IAC/E,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAAC,IAA4B;IACxE,IAAK,sBAA4C,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACjE,OAAO,EAAE,EAAE,EAAE,IAA2B,EAAE,WAAW,EAAE,KAAK,EAAE,CAAA;IAChE,CAAC;IACD,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,YAAY;YACf,OAAO,EAAE,EAAE,EAAE,iBAAiB,EAAE,WAAW,EAAE,KAAK,EAAE,CAAA;QACtD,KAAK,WAAW;YACd,OAAO,EAAE,EAAE,EAAE,sBAAsB,EAAE,WAAW,EAAE,KAAK,EAAE,CAAA;QAC3D,KAAK,YAAY;YACf,OAAO;gBACL,EAAE,EAAE,gBAAgB;gBACpB,WAAW,EAAE,IAAI;gBACjB,IAAI,EAAE,yFAAyF;aAChG,CAAA;QACH,KAAK,aAAa;YAChB,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAA;QACrF;YACE,MAAM,IAAI,KAAK,CAAC,yCAAyC,IAAI,GAAG,CAAC,CAAA;IACrE,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAC3C,EAAuB;IAEvB,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,CAAA;AACnC,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DailyRecord, TickerSeries } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Per-ticker daily return components on a shared calendar.
|
|
4
|
+
* All arrays have length = dates.length; index 0 is the buy-in day
|
|
5
|
+
* (returns there are identity: 1, 1, 0).
|
|
6
|
+
*/
|
|
7
|
+
export interface PreparedAsset {
|
|
8
|
+
ticker: string;
|
|
9
|
+
/** totalReturn[t] = adjClose[t] / adjClose[t-1] — dividends reinvested. */
|
|
10
|
+
totalReturn: number[];
|
|
11
|
+
/** priceReturn[t] = close[t] * splitFactor[t] / close[t-1] — splits neutralized, dividends dropped. */
|
|
12
|
+
priceReturn: number[];
|
|
13
|
+
/** divYield[t] = divCash[t] * (shares growth to t-1) / close[t-1] — cash dividend as fraction of prior value. */
|
|
14
|
+
divYield: number[];
|
|
15
|
+
}
|
|
16
|
+
export interface AlignedData {
|
|
17
|
+
dates: string[];
|
|
18
|
+
assets: Map<string, PreparedAsset>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build the shared calendar (intersection of trading days across all series,
|
|
22
|
+
* clamped to [start, end]) and per-ticker return components on it.
|
|
23
|
+
*
|
|
24
|
+
* Intersection can skip days a ticker traded (e.g. one fund had a holiday).
|
|
25
|
+
* Returns are computed from each ticker's own consecutive records BETWEEN
|
|
26
|
+
* calendar days, so a skipped day's move is never lost — it compounds into
|
|
27
|
+
* the next shared day.
|
|
28
|
+
*/
|
|
29
|
+
export declare function alignSeries(seriesList: TickerSeries[], start?: string, end?: string): AlignedData;
|
|
30
|
+
/** True when `date` starts a new period vs `prevDate` for the given frequency. */
|
|
31
|
+
export declare function isPeriodStart(prevDate: string, date: string, freq: 'annual' | 'quarterly' | 'monthly'): boolean;
|
|
32
|
+
export declare function isNewMonth(prevDate: string, date: string): boolean;
|
|
33
|
+
export type { DailyRecord };
|
|
34
|
+
//# sourceMappingURL=align.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"align.d.ts","sourceRoot":"","sources":["../../src/engine/align.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE3D;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,2EAA2E;IAC3E,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,uGAAuG;IACvG,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,iHAAiH;IACjH,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;CACnC;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,UAAU,EAAE,YAAY,EAAE,EAC1B,KAAK,CAAC,EAAE,MAAM,EACd,GAAG,CAAC,EAAE,MAAM,GACX,WAAW,CAmCb;AAuCD,kFAAkF;AAClF,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,QAAQ,GAAG,WAAW,GAAG,SAAS,GACvC,OAAO,CAQT;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAElE;AAED,YAAY,EAAE,WAAW,EAAE,CAAA"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the shared calendar (intersection of trading days across all series,
|
|
3
|
+
* clamped to [start, end]) and per-ticker return components on it.
|
|
4
|
+
*
|
|
5
|
+
* Intersection can skip days a ticker traded (e.g. one fund had a holiday).
|
|
6
|
+
* Returns are computed from each ticker's own consecutive records BETWEEN
|
|
7
|
+
* calendar days, so a skipped day's move is never lost — it compounds into
|
|
8
|
+
* the next shared day.
|
|
9
|
+
*/
|
|
10
|
+
export function alignSeries(seriesList, start, end) {
|
|
11
|
+
if (seriesList.length === 0)
|
|
12
|
+
throw new Error('No ticker series provided');
|
|
13
|
+
// Effective range: caller's range clamped to common history.
|
|
14
|
+
let lo = start ?? '0000-00-00';
|
|
15
|
+
let hi = end ?? '9999-99-99';
|
|
16
|
+
for (const s of seriesList) {
|
|
17
|
+
if (s.records.length === 0)
|
|
18
|
+
throw new Error(`Empty series for ${s.ticker}`);
|
|
19
|
+
const first = s.records[0].date;
|
|
20
|
+
const last = s.records[s.records.length - 1].date;
|
|
21
|
+
if (first > lo)
|
|
22
|
+
lo = first;
|
|
23
|
+
if (last < hi)
|
|
24
|
+
hi = last;
|
|
25
|
+
}
|
|
26
|
+
if (lo > hi)
|
|
27
|
+
throw new Error(`No overlapping history in range ${lo}..${hi}`);
|
|
28
|
+
// Intersection of trading days within [lo, hi].
|
|
29
|
+
const counts = new Map();
|
|
30
|
+
for (const s of seriesList) {
|
|
31
|
+
for (const r of s.records) {
|
|
32
|
+
if (r.date >= lo && r.date <= hi) {
|
|
33
|
+
counts.set(r.date, (counts.get(r.date) ?? 0) + 1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const dates = [...counts.entries()]
|
|
38
|
+
.filter(([, n]) => n === seriesList.length)
|
|
39
|
+
.map(([d]) => d)
|
|
40
|
+
.sort();
|
|
41
|
+
if (dates.length < 2)
|
|
42
|
+
throw new Error(`Fewer than 2 shared trading days in ${lo}..${hi}`);
|
|
43
|
+
const assets = new Map();
|
|
44
|
+
for (const s of seriesList) {
|
|
45
|
+
assets.set(s.ticker, prepareAsset(s, dates));
|
|
46
|
+
}
|
|
47
|
+
return { dates, assets };
|
|
48
|
+
}
|
|
49
|
+
function prepareAsset(series, dates) {
|
|
50
|
+
const byDate = new Map();
|
|
51
|
+
series.records.forEach((r, i) => byDate.set(r.date, i));
|
|
52
|
+
const totalReturn = [1];
|
|
53
|
+
const priceReturn = [1];
|
|
54
|
+
const divYield = [0];
|
|
55
|
+
for (let t = 1; t < dates.length; t++) {
|
|
56
|
+
const from = byDate.get(dates[t - 1]);
|
|
57
|
+
const to = byDate.get(dates[t]);
|
|
58
|
+
if (from === undefined || to === undefined) {
|
|
59
|
+
throw new Error(`${series.ticker} missing shared date ${dates[t - 1]} or ${dates[t]}`);
|
|
60
|
+
}
|
|
61
|
+
// Compound this ticker's own daily records across (from, to].
|
|
62
|
+
let tr = 1;
|
|
63
|
+
let pr = 1;
|
|
64
|
+
let dy = 0;
|
|
65
|
+
for (let i = from + 1; i <= to; i++) {
|
|
66
|
+
const prev = series.records[i - 1];
|
|
67
|
+
const cur = series.records[i];
|
|
68
|
+
tr *= cur.adjClose / prev.adjClose;
|
|
69
|
+
// A splitFactor of k means 1 share became k shares; neutralize the price drop.
|
|
70
|
+
const dayPr = (cur.close * cur.splitFactor) / prev.close;
|
|
71
|
+
// Dividend cash per unit of value held at `prev` close. Scale by shares
|
|
72
|
+
// accumulated through intermediate splits within this window (pr so far).
|
|
73
|
+
dy += (pr * (cur.divCash * cur.splitFactor)) / prev.close;
|
|
74
|
+
pr *= dayPr;
|
|
75
|
+
}
|
|
76
|
+
totalReturn.push(tr);
|
|
77
|
+
priceReturn.push(pr);
|
|
78
|
+
divYield.push(dy);
|
|
79
|
+
}
|
|
80
|
+
return { ticker: series.ticker, totalReturn, priceReturn, divYield };
|
|
81
|
+
}
|
|
82
|
+
/** True when `date` starts a new period vs `prevDate` for the given frequency. */
|
|
83
|
+
export function isPeriodStart(prevDate, date, freq) {
|
|
84
|
+
const py = Number(prevDate.slice(0, 4));
|
|
85
|
+
const pm = Number(prevDate.slice(5, 7));
|
|
86
|
+
const cy = Number(date.slice(0, 4));
|
|
87
|
+
const cm = Number(date.slice(5, 7));
|
|
88
|
+
if (freq === 'annual')
|
|
89
|
+
return cy !== py;
|
|
90
|
+
if (freq === 'monthly')
|
|
91
|
+
return cy !== py || cm !== pm;
|
|
92
|
+
return cy !== py || Math.floor((cm - 1) / 3) !== Math.floor((pm - 1) / 3);
|
|
93
|
+
}
|
|
94
|
+
export function isNewMonth(prevDate, date) {
|
|
95
|
+
return prevDate.slice(0, 7) !== date.slice(0, 7);
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=align.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"align.js","sourceRoot":"","sources":["../../src/engine/align.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BacktestConfig, BacktestResult, PortfolioSpec, TickerSeries } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run a portfolio backtest. See BacktestResult in types.ts for the exact
|
|
4
|
+
* simulation semantics (flow timing, rebalancing, dividend handling).
|
|
5
|
+
*/
|
|
6
|
+
export declare function runBacktest(seriesList: TickerSeries[], portfolio: PortfolioSpec, config: BacktestConfig): BacktestResult;
|
|
7
|
+
//# sourceMappingURL=backtest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backtest.d.ts","sourceRoot":"","sources":["../../src/engine/backtest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,aAAa,EACb,YAAY,EACb,MAAM,YAAY,CAAA;AAEnB;;;GAGG;AACH,wBAAgB,WAAW,CACzB,UAAU,EAAE,YAAY,EAAE,EAC1B,SAAS,EAAE,aAAa,EACxB,MAAM,EAAE,cAAc,GACrB,cAAc,CAuGhB"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { alignSeries, isNewMonth, isPeriodStart } from './align.js';
|
|
2
|
+
import { computeMetrics } from './metrics.js';
|
|
3
|
+
/**
|
|
4
|
+
* Run a portfolio backtest. See BacktestResult in types.ts for the exact
|
|
5
|
+
* simulation semantics (flow timing, rebalancing, dividend handling).
|
|
6
|
+
*/
|
|
7
|
+
export function runBacktest(seriesList, portfolio, config) {
|
|
8
|
+
const weightSum = portfolio.allocations.reduce((s, a) => s + a.weight, 0);
|
|
9
|
+
if (Math.abs(weightSum - 100) > 1e-6) {
|
|
10
|
+
throw new Error(`Weights must sum to 100, got ${weightSum}`);
|
|
11
|
+
}
|
|
12
|
+
const bySymbol = new Map(seriesList.map((s) => [s.ticker, s]));
|
|
13
|
+
const used = portfolio.allocations.map((a) => {
|
|
14
|
+
const s = bySymbol.get(a.ticker);
|
|
15
|
+
if (!s)
|
|
16
|
+
throw new Error(`Missing series for ${a.ticker}`);
|
|
17
|
+
return s;
|
|
18
|
+
});
|
|
19
|
+
const { dates, assets } = alignSeries(used, config.start, config.end);
|
|
20
|
+
const n = dates.length;
|
|
21
|
+
const weights = portfolio.allocations.map((a) => a.weight / 100);
|
|
22
|
+
const prepared = portfolio.allocations.map((a) => assets.get(a.ticker));
|
|
23
|
+
// State: market value per holding, plus uninvested dividend cash.
|
|
24
|
+
const holdings = weights.map((w) => config.initialAmount * w);
|
|
25
|
+
let cash = 0;
|
|
26
|
+
const values = new Array(n);
|
|
27
|
+
const twrIndex = new Array(n);
|
|
28
|
+
const flows = new Array(n).fill(0);
|
|
29
|
+
const dividendIncome = new Array(n).fill(0);
|
|
30
|
+
const incomeByHolding = new Array(holdings.length).fill(0);
|
|
31
|
+
values[0] = config.initialAmount;
|
|
32
|
+
twrIndex[0] = 1;
|
|
33
|
+
let totalContributions = 0;
|
|
34
|
+
for (let t = 1; t < n; t++) {
|
|
35
|
+
const prevValue = holdings.reduce((s, h) => s + h, cash);
|
|
36
|
+
// 1. Start-of-day external flow (contribution/withdrawal at prior close values).
|
|
37
|
+
let flow = 0;
|
|
38
|
+
if (config.monthlyContribution !== 0 && isNewMonth(dates[t - 1], dates[t])) {
|
|
39
|
+
flow = config.monthlyContribution;
|
|
40
|
+
for (let i = 0; i < holdings.length; i++)
|
|
41
|
+
holdings[i] += flow * weights[i];
|
|
42
|
+
totalContributions += flow;
|
|
43
|
+
flows[t] = flow;
|
|
44
|
+
}
|
|
45
|
+
// 2. Start-of-day rebalance to target weights (holdings only; dividend cash stays cash).
|
|
46
|
+
if (config.rebalance !== 'none' &&
|
|
47
|
+
isPeriodStart(dates[t - 1], dates[t], config.rebalance)) {
|
|
48
|
+
const invested = holdings.reduce((s, h) => s + h, 0);
|
|
49
|
+
for (let i = 0; i < holdings.length; i++)
|
|
50
|
+
holdings[i] = invested * weights[i];
|
|
51
|
+
}
|
|
52
|
+
// 3. Apply the day's returns.
|
|
53
|
+
for (let i = 0; i < holdings.length; i++) {
|
|
54
|
+
const asset = prepared[i];
|
|
55
|
+
const income = holdings[i] * asset.divYield[t];
|
|
56
|
+
dividendIncome[t] += income;
|
|
57
|
+
incomeByHolding[i] += income;
|
|
58
|
+
if (config.reinvestDividends) {
|
|
59
|
+
holdings[i] *= asset.totalReturn[t];
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
cash += income;
|
|
63
|
+
holdings[i] *= asset.priceReturn[t];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const value = holdings.reduce((s, h) => s + h, cash);
|
|
67
|
+
values[t] = value;
|
|
68
|
+
// Time-weighted return: the flow entered at prior-close values, so the
|
|
69
|
+
// day's growth acted on (prevValue + flow) — divide it out.
|
|
70
|
+
twrIndex[t] = twrIndex[t - 1] * (value / (prevValue + flow));
|
|
71
|
+
}
|
|
72
|
+
const metrics = computeMetrics(dates, twrIndex, values, flows, config.riskFreeRate ?? 0, config.rfByMonth);
|
|
73
|
+
const finalValue = values[n - 1];
|
|
74
|
+
const holdingBreakdowns = portfolio.allocations.map((a, i) => ({
|
|
75
|
+
ticker: a.ticker,
|
|
76
|
+
targetWeight: a.weight,
|
|
77
|
+
endValue: holdings[i],
|
|
78
|
+
endWeight: finalValue > 0 ? (holdings[i] / finalValue) * 100 : 0,
|
|
79
|
+
assetTotalReturn: prepared[i].totalReturn.reduce((p, r) => p * r, 1) - 1,
|
|
80
|
+
income: incomeByHolding[i],
|
|
81
|
+
}));
|
|
82
|
+
return {
|
|
83
|
+
portfolio,
|
|
84
|
+
dates,
|
|
85
|
+
values,
|
|
86
|
+
twrIndex,
|
|
87
|
+
flows,
|
|
88
|
+
dividendIncome,
|
|
89
|
+
endingCash: cash,
|
|
90
|
+
totalContributions,
|
|
91
|
+
holdings: holdingBreakdowns,
|
|
92
|
+
metrics,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=backtest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backtest.js","sourceRoot":"","sources":["../../src/engine/backtest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BacktestConfig, TickerSeries } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A point on the two-asset efficient frontier: the risk (annualized
|
|
4
|
+
* volatility) and reward (CAGR) of a specific A/B weight mix, plus its
|
|
5
|
+
* Sharpe ratio, all computed by the real backtest engine over the shared
|
|
6
|
+
* window — no mean/covariance shortcuts, so it matches every other number
|
|
7
|
+
* the tool reports.
|
|
8
|
+
*/
|
|
9
|
+
export interface FrontierPoint {
|
|
10
|
+
weightA: number;
|
|
11
|
+
volatility: number;
|
|
12
|
+
cagr: number;
|
|
13
|
+
sharpe: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Sweep the A/B mix from 0→100% A in `steps` increments. Two assets is the
|
|
17
|
+
* classic, honest frontier: the whole curve is enumerable, so there's no
|
|
18
|
+
* optimizer to trust — every point is a portfolio you could actually hold.
|
|
19
|
+
*/
|
|
20
|
+
export declare function twoAssetFrontier(a: TickerSeries, b: TickerSeries, config: BacktestConfig, steps?: number): FrontierPoint[];
|
|
21
|
+
/** Index of the minimum-variance point. */
|
|
22
|
+
export declare function minVarianceIndex(points: FrontierPoint[]): number;
|
|
23
|
+
/** Index of the maximum-Sharpe (tangency) point. */
|
|
24
|
+
export declare function maxSharpeIndex(points: FrontierPoint[]): number;
|
|
25
|
+
//# sourceMappingURL=frontier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontier.d.ts","sourceRoot":"","sources":["../../src/engine/frontier.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAG9D;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,CAAC,EAAE,YAAY,EACf,CAAC,EAAE,YAAY,EACf,MAAM,EAAE,cAAc,EACtB,KAAK,SAAK,GACT,aAAa,EAAE,CAgBjB;AAED,2CAA2C;AAC3C,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,CAIhE;AAED,oDAAoD;AACpD,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,CAI9D"}
|