@samsmith2121/synthetic-leverage-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 (44) hide show
  1. package/README.md +61 -0
  2. package/dist/src/analysis-cli.d.ts +1 -0
  3. package/dist/src/analysis-cli.js +59 -0
  4. package/dist/src/analysis.d.ts +214 -0
  5. package/dist/src/analysis.js +843 -0
  6. package/dist/src/backtest.d.ts +3 -0
  7. package/dist/src/backtest.js +172 -0
  8. package/dist/src/benchmark.d.ts +11 -0
  9. package/dist/src/benchmark.js +85 -0
  10. package/dist/src/contributions.d.ts +4 -0
  11. package/dist/src/contributions.js +11 -0
  12. package/dist/src/costs.d.ts +8 -0
  13. package/dist/src/costs.js +29 -0
  14. package/dist/src/index.d.ts +18 -0
  15. package/dist/src/index.js +34 -0
  16. package/dist/src/metrics.d.ts +15 -0
  17. package/dist/src/metrics.js +232 -0
  18. package/dist/src/public-api.d.ts +89 -0
  19. package/dist/src/public-api.js +103 -0
  20. package/dist/src/reporting.d.ts +112 -0
  21. package/dist/src/reporting.js +156 -0
  22. package/dist/src/signal.d.ts +34 -0
  23. package/dist/src/signal.js +80 -0
  24. package/dist/src/simulator/adapters.d.ts +9 -0
  25. package/dist/src/simulator/adapters.js +99 -0
  26. package/dist/src/simulator/index.d.ts +4 -0
  27. package/dist/src/simulator/index.js +31 -0
  28. package/dist/src/simulator/monte-carlo.d.ts +8 -0
  29. package/dist/src/simulator/monte-carlo.js +94 -0
  30. package/dist/src/simulator/series.d.ts +6 -0
  31. package/dist/src/simulator/series.js +152 -0
  32. package/dist/src/simulator/types.d.ts +111 -0
  33. package/dist/src/simulator/types.js +2 -0
  34. package/dist/src/step.d.ts +24 -0
  35. package/dist/src/step.js +75 -0
  36. package/dist/src/types.d.ts +145 -0
  37. package/dist/src/types.js +2 -0
  38. package/dist/src/validation-cases.d.ts +21 -0
  39. package/dist/src/validation-cases.js +930 -0
  40. package/dist/src/validation-harness.d.ts +1 -0
  41. package/dist/src/validation-harness.js +26 -0
  42. package/dist/src/validation.d.ts +8 -0
  43. package/dist/src/validation.js +244 -0
  44. package/package.json +36 -0
@@ -0,0 +1,4 @@
1
+ import type { SimulatorBacktestRequest, SimulatorBacktestResponse, SimulatorMonteCarloRequest, SimulatorMonteCarloResponse } from './types';
2
+ export type { SimulatorAnnualReturnPoint, SimulatorBacktestRequest, SimulatorBacktestResponse, SimulatorBacktestSeries, SimulatorBacktestSummary, SimulatorConfig, SimulatorDay, SimulatorMonteCarloPoint, SimulatorMonteCarloRequest, SimulatorMonteCarloResponse, SimulatorStrategy, SimulatorWarning, } from './types';
3
+ export declare function runSingleBacktest(request: SimulatorBacktestRequest): SimulatorBacktestResponse;
4
+ export declare function runMonteCarlo(request: SimulatorMonteCarloRequest): SimulatorMonteCarloResponse;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runSingleBacktest = runSingleBacktest;
4
+ exports.runMonteCarlo = runMonteCarlo;
5
+ const backtest_1 = require("../backtest");
6
+ const adapters_1 = require("./adapters");
7
+ const monte_carlo_1 = require("./monte-carlo");
8
+ const series_1 = require("./series");
9
+ const TRADING_DAYS_PER_YEAR = 252;
10
+ function runSingleBacktest(request) {
11
+ const prepared = (0, adapters_1.prepareSimulatorBacktest)(request);
12
+ const result = (0, backtest_1.runFixedBacktest)(prepared.engineDays, prepared.engineConfig);
13
+ return (0, series_1.buildSimulatorBacktestResponse)({
14
+ result,
15
+ initialEquity: request.config.initialEquity,
16
+ });
17
+ }
18
+ function runMonteCarlo(request) {
19
+ const prepared = (0, adapters_1.prepareSimulatorMonteCarlo)(request);
20
+ const result = (0, backtest_1.runMonteCarloBacktest)(prepared.engineDays, prepared.engineConfig, {
21
+ paths: request.simulations,
22
+ horizonDays: Math.max(1, request.years * TRADING_DAYS_PER_YEAR),
23
+ seed: request.randomSeed,
24
+ });
25
+ return (0, monte_carlo_1.buildSimulatorMonteCarloResponse)({
26
+ result,
27
+ years: request.years,
28
+ startDate: prepared.sourceDays[0]?.date ?? request.config.startDate ?? '1970-01-01',
29
+ initialEquity: request.config.initialEquity,
30
+ });
31
+ }
@@ -0,0 +1,8 @@
1
+ import { MonteCarloResult } from '../types';
2
+ import { SimulatorMonteCarloResponse } from './types';
3
+ export declare function buildSimulatorMonteCarloResponse(params: {
4
+ result: MonteCarloResult;
5
+ years: number;
6
+ startDate: string;
7
+ initialEquity: number;
8
+ }): SimulatorMonteCarloResponse;
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildSimulatorMonteCarloResponse = buildSimulatorMonteCarloResponse;
4
+ const metrics_1 = require("../metrics");
5
+ const TRADING_DAYS_PER_YEAR = 252;
6
+ function buildSimulatorMonteCarloResponse(params) {
7
+ const { result, years, startDate, initialEquity } = params;
8
+ const points = [];
9
+ const startYear = new Date(`${startDate}T00:00:00Z`).getUTCFullYear();
10
+ for (let step = 0; step <= years; step += 1) {
11
+ const equityValues = result.paths.map((path) => checkpointEquity(path.result, step, initialEquity));
12
+ const benchmarkValues = result.paths
13
+ .map((path) => checkpointBenchmarkEquity(path.result, step, initialEquity))
14
+ .filter((value) => value !== undefined);
15
+ points.push({
16
+ step,
17
+ label: String(startYear + step),
18
+ p10: (0, metrics_1.percentile)(equityValues, 0.1),
19
+ p25: (0, metrics_1.percentile)(equityValues, 0.25),
20
+ p50: (0, metrics_1.percentile)(equityValues, 0.5),
21
+ p75: (0, metrics_1.percentile)(equityValues, 0.75),
22
+ p90: (0, metrics_1.percentile)(equityValues, 0.9),
23
+ ...(benchmarkValues.length > 0 ? { benchmarkP50: (0, metrics_1.percentile)(benchmarkValues, 0.5) } : {}),
24
+ });
25
+ }
26
+ return {
27
+ monteCarlo: {
28
+ points,
29
+ warnings: mapMonteCarloWarnings(result.invariants),
30
+ },
31
+ };
32
+ }
33
+ function checkpointEquity(result, step, initialEquity) {
34
+ if (step === 0) {
35
+ return initialEquity;
36
+ }
37
+ const idx = Math.min(step * TRADING_DAYS_PER_YEAR - 1, result.daily.length - 1);
38
+ return result.daily[idx]?.equityEnd ?? result.finalEquity;
39
+ }
40
+ function checkpointBenchmarkEquity(result, step, initialEquity) {
41
+ if (step === 0) {
42
+ return initialEquity;
43
+ }
44
+ const benchmark = result.benchmark;
45
+ if (!benchmark) {
46
+ return undefined;
47
+ }
48
+ const targetDate = result.daily[Math.min(step * TRADING_DAYS_PER_YEAR - 1, result.daily.length - 1)]?.date;
49
+ if (!targetDate) {
50
+ return undefined;
51
+ }
52
+ const benchmarkIndex = benchmark.dates.findIndex((date) => date === targetDate);
53
+ if (benchmarkIndex === -1) {
54
+ return undefined;
55
+ }
56
+ return benchmark.benchmarkEquity[benchmarkIndex];
57
+ }
58
+ function mapMonteCarloWarnings(invariants) {
59
+ if (!invariants) {
60
+ return [];
61
+ }
62
+ const warnings = [];
63
+ if (!invariants.hasFiniteValues) {
64
+ warnings.push({
65
+ code: 'MC_HAS_NON_FINITE_VALUES',
66
+ message: 'Monte Carlo invariants found non-finite values in path outputs.',
67
+ });
68
+ }
69
+ if (!invariants.sameSeedReproducible) {
70
+ warnings.push({
71
+ code: 'MC_SEED_NOT_REPRODUCIBLE',
72
+ message: 'Monte Carlo same-seed reproducibility invariant failed.',
73
+ });
74
+ }
75
+ if (!invariants.differentSeedsCanDiffer) {
76
+ warnings.push({
77
+ code: 'MC_DIFFERENT_SEEDS_IDENTICAL',
78
+ message: 'Monte Carlo different-seed divergence invariant failed.',
79
+ });
80
+ }
81
+ if (!invariants.pathDatesStrictlyIncreasing) {
82
+ warnings.push({
83
+ code: 'MC_PATH_DATES_NOT_STRICTLY_INCREASING',
84
+ message: 'Monte Carlo path date ordering invariant failed.',
85
+ });
86
+ }
87
+ if (!invariants.percentileSummariesOrdered) {
88
+ warnings.push({
89
+ code: 'MC_PERCENTILES_NOT_ORDERED',
90
+ message: 'Monte Carlo percentile ordering invariant failed.',
91
+ });
92
+ }
93
+ return warnings;
94
+ }
@@ -0,0 +1,6 @@
1
+ import { BacktestResult } from '../types';
2
+ import { SimulatorBacktestResponse } from './types';
3
+ export declare function buildSimulatorBacktestResponse(params: {
4
+ result: BacktestResult;
5
+ initialEquity: number;
6
+ }): SimulatorBacktestResponse;
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildSimulatorBacktestResponse = buildSimulatorBacktestResponse;
4
+ const metrics_1 = require("../metrics");
5
+ function buildSimulatorBacktestResponse(params) {
6
+ const { result, initialEquity } = params;
7
+ const xirr = computeBacktestXirr(result.daily, initialEquity);
8
+ const volatility = computeBacktestVolatility(result.daily);
9
+ const summary = buildSummary(result, initialEquity, xirr, volatility);
10
+ return {
11
+ run: {
12
+ ok: true,
13
+ summary,
14
+ series: buildSeries(result, initialEquity),
15
+ warnings: collectWarnings(result, xirr, volatility),
16
+ },
17
+ };
18
+ }
19
+ function buildSummary(result, initialEquity, xirr, volatility) {
20
+ const totalInvested = initialEquity + result.daily.reduce((sum, day) => sum + day.contribution, 0);
21
+ return {
22
+ finalEquity: result.finalEquity,
23
+ totalInvested,
24
+ twrCagr: result.twrCagr,
25
+ xirr,
26
+ maxDrawdown: result.maxDrawdown,
27
+ volatility,
28
+ totalTradeCost: result.daily.reduce((sum, day) => sum + day.tradeCost, 0),
29
+ totalBorrowCost: result.daily.reduce((sum, day) => sum + day.borrowDrag, 0),
30
+ totalErCost: result.daily.reduce((sum, day) => sum + day.expenseDrag, 0),
31
+ totalSwitches: countSwitches(result.daily),
32
+ benchmarkTotalReturn: result.benchmark?.benchmarkTotalReturn ?? null,
33
+ benchmarkTwrCagr: result.benchmark?.benchmarkTwrCagr ?? null,
34
+ benchmarkVolatility: result.benchmark?.benchmarkVolatility ?? null,
35
+ benchmarkMaxDrawdown: result.benchmark?.benchmarkMaxDrawdown ?? null,
36
+ excessTotalReturn: result.benchmark?.excessTotalReturn ?? null,
37
+ excessTwrCagr: result.benchmark?.excessTwrCagr ?? null,
38
+ trackingError: result.benchmark?.trackingError ?? null,
39
+ };
40
+ }
41
+ function buildSeries(result, initialEquity) {
42
+ return {
43
+ equityCurve: result.daily.map((state) => point(state.date, state.equityEnd)),
44
+ benchmarkCurve: result.benchmark
45
+ ? result.benchmark.dates.map((date, index) => point(date, result.benchmark?.benchmarkEquity[index] ?? 0))
46
+ : undefined,
47
+ drawdownCurve: buildDrawdownCurve(result.daily, initialEquity),
48
+ leverageCurve: result.daily.map((state) => point(state.date, state.appliedPosition)),
49
+ returnSeries: result.daily.map((state) => point(state.date, state.capitalBaseBeforeReturn === 0 ? 0 : state.equityEnd / state.capitalBaseBeforeReturn - 1)),
50
+ annualReturns: buildAnnualReturns(result.daily, initialEquity),
51
+ };
52
+ }
53
+ function buildDrawdownCurve(daily, initialEquity) {
54
+ const out = [];
55
+ let peak = initialEquity;
56
+ for (const state of daily) {
57
+ peak = Math.max(peak, state.equityEnd);
58
+ const drawdown = peak > 0 ? (state.equityEnd - peak) / peak : 0;
59
+ out.push(point(state.date, drawdown));
60
+ }
61
+ return out;
62
+ }
63
+ function buildAnnualReturns(daily, initialEquity) {
64
+ const buckets = new Map();
65
+ let priorYearEnd = initialEquity;
66
+ for (const state of daily) {
67
+ const year = new Date(`${state.date}T00:00:00Z`).getUTCFullYear();
68
+ const existing = buckets.get(year);
69
+ if (!existing) {
70
+ buckets.set(year, { start: priorYearEnd, end: state.equityEnd });
71
+ }
72
+ else {
73
+ existing.end = state.equityEnd;
74
+ }
75
+ priorYearEnd = state.equityEnd;
76
+ }
77
+ return [...buckets.entries()].map(([year, values]) => ({
78
+ year,
79
+ value: values.start === 0 ? 0 : values.end / values.start - 1,
80
+ }));
81
+ }
82
+ function computeBacktestXirr(daily, initialEquity) {
83
+ return (0, metrics_1.computeXirr)(buildCashFlows(daily, initialEquity));
84
+ }
85
+ function computeBacktestVolatility(daily) {
86
+ return (0, metrics_1.computeVolatilityFromReturns)(daily.map((state) => state.capitalBaseBeforeReturn === 0 ? 0 : state.equityEnd / state.capitalBaseBeforeReturn - 1));
87
+ }
88
+ function buildCashFlows(daily, initialEquity) {
89
+ if (daily.length === 0) {
90
+ return [];
91
+ }
92
+ const flows = [{ date: daily[0].date, amount: -initialEquity }];
93
+ for (const state of daily) {
94
+ if (state.contribution !== 0) {
95
+ flows.push({ date: state.date, amount: -state.contribution });
96
+ }
97
+ }
98
+ flows.push({ date: daily[daily.length - 1].date, amount: daily[daily.length - 1].equityEnd });
99
+ return flows;
100
+ }
101
+ function countSwitches(daily) {
102
+ let switches = 0;
103
+ for (const state of daily) {
104
+ if (Math.abs(state.appliedPosition - state.priorAppliedPosition) > 1e-12) {
105
+ switches += 1;
106
+ }
107
+ }
108
+ return switches;
109
+ }
110
+ function collectWarnings(result, xirr, volatility) {
111
+ const warnings = [];
112
+ const benchmarkDates = result.benchmark?.dates.length ?? 0;
113
+ if (benchmarkDates === 0) {
114
+ warnings.push({
115
+ code: 'BENCHMARK_UNAVAILABLE',
116
+ message: 'No benchmark overlap was available for matched-date benchmark analytics.',
117
+ });
118
+ }
119
+ else if (benchmarkDates < result.daily.length) {
120
+ warnings.push({
121
+ code: 'BENCHMARK_PARTIAL_COVERAGE',
122
+ message: 'Benchmark analytics use matched dates only because benchmark coverage is partial.',
123
+ context: {
124
+ benchmarkDays: benchmarkDates,
125
+ totalDays: result.daily.length,
126
+ coveragePct: benchmarkDates / result.daily.length,
127
+ },
128
+ });
129
+ }
130
+ if (result.twrCagr === null) {
131
+ warnings.push({
132
+ code: 'TWR_CAGR_UNAVAILABLE',
133
+ message: 'TWR CAGR is unavailable for the provided backtest window.',
134
+ });
135
+ }
136
+ if (xirr === null) {
137
+ warnings.push({
138
+ code: 'XIRR_UNAVAILABLE',
139
+ message: 'XIRR is unavailable for the provided cash-flow schedule.',
140
+ });
141
+ }
142
+ if (volatility === null) {
143
+ warnings.push({
144
+ code: 'VOLATILITY_UNAVAILABLE',
145
+ message: 'Volatility is unavailable for the provided backtest window.',
146
+ });
147
+ }
148
+ return warnings;
149
+ }
150
+ function point(date, value) {
151
+ return { date, value };
152
+ }
@@ -0,0 +1,111 @@
1
+ export interface SimulatorDay {
2
+ date: string;
3
+ assetClose: number;
4
+ benchmarkClose?: number;
5
+ indicatorClose?: number;
6
+ financingRateAnnual?: number;
7
+ }
8
+ export interface SimulatorConfig {
9
+ initialEquity: number;
10
+ monthlyContribution: number;
11
+ tradeSpreadPct: number;
12
+ borrowSpreadPct: number;
13
+ expenseRatioPct: number;
14
+ startDate?: string;
15
+ endDate?: string;
16
+ }
17
+ export interface SimulatorStrategy {
18
+ name: string;
19
+ leverageMultiple: number;
20
+ vixDefensiveExposure: number;
21
+ signal: {
22
+ useSma: boolean;
23
+ smaLength: number;
24
+ exposureAboveSignal?: number;
25
+ exposureBelowSignal?: number;
26
+ useVixThreshold: boolean;
27
+ vixThreshold: number;
28
+ };
29
+ rateSource: {
30
+ mode: 'builtin' | 'fixed' | 'series';
31
+ fixedRatePct?: number;
32
+ seriesId?: string;
33
+ };
34
+ }
35
+ export interface SimulatorBacktestRequest {
36
+ days: SimulatorDay[];
37
+ config: SimulatorConfig;
38
+ strategy: SimulatorStrategy;
39
+ }
40
+ export interface SimulatorWarning {
41
+ code: string;
42
+ message: string;
43
+ context?: Record<string, string | number | boolean | null>;
44
+ }
45
+ export interface SimulatorSeriesPoint {
46
+ date: string;
47
+ value: number;
48
+ }
49
+ export interface SimulatorAnnualReturnPoint {
50
+ year: number;
51
+ value: number;
52
+ }
53
+ export interface SimulatorBacktestSummary {
54
+ finalEquity: number;
55
+ totalInvested: number;
56
+ twrCagr: number | null;
57
+ xirr: number | null;
58
+ maxDrawdown: number;
59
+ volatility: number | null;
60
+ totalTradeCost: number;
61
+ totalBorrowCost: number;
62
+ totalErCost: number;
63
+ totalSwitches: number;
64
+ benchmarkTotalReturn?: number | null;
65
+ benchmarkTwrCagr?: number | null;
66
+ benchmarkVolatility?: number | null;
67
+ benchmarkMaxDrawdown?: number | null;
68
+ excessTotalReturn?: number | null;
69
+ excessTwrCagr?: number | null;
70
+ trackingError?: number | null;
71
+ }
72
+ export interface SimulatorBacktestSeries {
73
+ equityCurve: SimulatorSeriesPoint[];
74
+ benchmarkCurve?: SimulatorSeriesPoint[];
75
+ drawdownCurve: SimulatorSeriesPoint[];
76
+ leverageCurve: SimulatorSeriesPoint[];
77
+ returnSeries: SimulatorSeriesPoint[];
78
+ annualReturns: SimulatorAnnualReturnPoint[];
79
+ }
80
+ export interface SimulatorBacktestResponse {
81
+ run: {
82
+ ok: boolean;
83
+ summary: SimulatorBacktestSummary;
84
+ series: SimulatorBacktestSeries;
85
+ warnings: SimulatorWarning[];
86
+ };
87
+ }
88
+ export interface SimulatorMonteCarloRequest {
89
+ days: SimulatorDay[];
90
+ config: SimulatorConfig;
91
+ strategy: SimulatorStrategy;
92
+ years: number;
93
+ simulations: number;
94
+ randomSeed?: number;
95
+ }
96
+ export interface SimulatorMonteCarloPoint {
97
+ step: number;
98
+ label: string;
99
+ p10: number;
100
+ p25: number;
101
+ p50: number;
102
+ p75: number;
103
+ p90: number;
104
+ benchmarkP50?: number;
105
+ }
106
+ export interface SimulatorMonteCarloResponse {
107
+ monteCarlo: {
108
+ points: SimulatorMonteCarloPoint[];
109
+ warnings: SimulatorWarning[];
110
+ };
111
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,24 @@
1
+ import { BacktestResult, DailyState, DayInput, EngineConfig } from './types';
2
+ export interface StepInput {
3
+ day: DayInput;
4
+ previousDay?: DayInput;
5
+ previousState?: DailyState;
6
+ previousTargetPosition: number;
7
+ previousAppliedPosition: number;
8
+ previousBorrowRateDaily: number;
9
+ config: EngineConfig;
10
+ }
11
+ /**
12
+ * Canonical daily engine step using locked order:
13
+ * 1) read day t inputs
14
+ * 2) compute signal(t)
15
+ * 3) derive targetPosition(t)
16
+ * 4) appliedPosition(t)=targetPosition(t-1)
17
+ * 5) contribution before return
18
+ * 6) trade cost at start of day on turnover
19
+ * 7) apply return
20
+ * 8) apply borrow + ER drag
21
+ * 9) save end-of-day state
22
+ */
23
+ export declare function runDayStep(input: StepInput): DailyState;
24
+ export declare function summarizeResult(daily: DailyState[]): BacktestResult;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runDayStep = runDayStep;
4
+ exports.summarizeResult = summarizeResult;
5
+ const metrics_1 = require("./metrics");
6
+ const costs_1 = require("./costs");
7
+ const contributions_1 = require("./contributions");
8
+ const signal_1 = require("./signal");
9
+ /**
10
+ * Canonical daily engine step using locked order:
11
+ * 1) read day t inputs
12
+ * 2) compute signal(t)
13
+ * 3) derive targetPosition(t)
14
+ * 4) appliedPosition(t)=targetPosition(t-1)
15
+ * 5) contribution before return
16
+ * 6) trade cost at start of day on turnover
17
+ * 7) apply return
18
+ * 8) apply borrow + ER drag
19
+ * 9) save end-of-day state
20
+ */
21
+ function runDayStep(input) {
22
+ const { day, previousDay, previousState, previousTargetPosition, previousAppliedPosition, config } = input;
23
+ const equityStart = previousState?.equityEnd ?? config.initialEquity;
24
+ const signalContext = (0, signal_1.buildSignalContext)(day, previousDay);
25
+ const signal = (0, signal_1.computeSignal)(signalContext);
26
+ const targetPosition = (0, signal_1.deriveTargetPosition)(signal, config);
27
+ // T+1 fixed-path behavior: today's applied position is yesterday's target position.
28
+ const appliedPosition = previousTargetPosition;
29
+ const contribution = (0, contributions_1.getContributionForDay)(day);
30
+ const capitalBaseBeforeReturn = (0, contributions_1.applyContribution)(equityStart, contribution);
31
+ const turnoverNotional = (0, costs_1.calculateTurnoverNotional)(appliedPosition, previousAppliedPosition, capitalBaseBeforeReturn);
32
+ const tradeCost = (0, costs_1.calculateTradeCost)(turnoverNotional, config.tradeCostRate, day.tradeSpread ?? 0);
33
+ const equityAfterTradeCost = capitalBaseBeforeReturn - tradeCost;
34
+ const grossAssetPnl = equityAfterTradeCost * appliedPosition * day.assetReturn;
35
+ const cashYieldDaily = config.cashYieldDaily ?? 0;
36
+ const cashWeight = Math.max(0, 1 - Math.abs(appliedPosition));
37
+ const cashPnl = equityAfterTradeCost * cashWeight * cashYieldDaily;
38
+ const grossPnl = grossAssetPnl + cashPnl;
39
+ const equityAfterReturn = equityAfterTradeCost + grossPnl;
40
+ const borrowRateDaily = day.borrowRateDaily ?? input.previousBorrowRateDaily;
41
+ const expenseRatioDaily = (0, costs_1.annualToDailyRate)(day.expenseRatioAnnual ?? 0);
42
+ const borrowDrag = (0, costs_1.calculateBorrowDrag)(appliedPosition, equityAfterTradeCost, borrowRateDaily);
43
+ const expenseDrag = (0, costs_1.calculateExpenseDrag)(equityAfterTradeCost, expenseRatioDaily);
44
+ const equityEnd = equityAfterReturn - borrowDrag - expenseDrag;
45
+ const netPnl = equityEnd - equityStart - contribution;
46
+ return {
47
+ date: day.date,
48
+ equityStart,
49
+ contribution,
50
+ capitalBaseBeforeReturn,
51
+ equityAfterTradeCost,
52
+ equityEnd,
53
+ signal,
54
+ targetPosition,
55
+ appliedPosition,
56
+ priorAppliedPosition: previousAppliedPosition,
57
+ turnoverNotional,
58
+ tradeCost,
59
+ grossPnl,
60
+ borrowDrag,
61
+ expenseDrag,
62
+ netPnl,
63
+ borrowRateDaily,
64
+ expenseRatioDaily,
65
+ };
66
+ }
67
+ function summarizeResult(daily) {
68
+ const equityCurve = daily.map((state) => state.equityEnd);
69
+ return {
70
+ daily,
71
+ finalEquity: daily.length > 0 ? daily[daily.length - 1].equityEnd : 0,
72
+ twrCagr: (0, metrics_1.computeTwrCagr)(daily),
73
+ maxDrawdown: (0, metrics_1.computeMaxDrawdownFromEquity)(equityCurve),
74
+ };
75
+ }
@@ -0,0 +1,145 @@
1
+ export interface DayInput {
2
+ date: string;
3
+ assetReturn: number;
4
+ /** Optional benchmark return used in aligned benchmark comparison. */
5
+ benchmarkReturn?: number;
6
+ /** Signal source for day t. Signal is computed on day t and applied on day t+1. */
7
+ signalInput: number;
8
+ /** Optional scheduled contribution for day t (applied before return). */
9
+ contribution?: number;
10
+ /** Daily borrow rate override; missing values carry forward prior known rate. */
11
+ borrowRateDaily?: number;
12
+ /** Optional annualized expense ratio. Converted to daily inside the engine. */
13
+ expenseRatioAnnual?: number;
14
+ /** Optional day-level trading spread surcharge applied to turnover notional. */
15
+ tradeSpread?: number;
16
+ }
17
+ export interface EngineConfig {
18
+ initialEquity: number;
19
+ /** Target position used prior to first bar. */
20
+ initialTargetPosition?: number;
21
+ /** Applied position on the day before the first bar. */
22
+ initialAppliedPosition?: number;
23
+ /** Trade-cost rate charged on turnover notional at start of day. */
24
+ tradeCostRate: number;
25
+ /** Maximum absolute leverage allowed for target positions. */
26
+ maxLeverage: number;
27
+ /**
28
+ * Explicit initial borrow rate requirement.
29
+ * If day 1 has no borrowRateDaily, this field must be provided.
30
+ */
31
+ initialBorrowRateDaily?: number;
32
+ /**
33
+ * Cash mode assumption for this milestone: zero yield unless explicitly configured.
34
+ * Kept as configurable for future milestones.
35
+ */
36
+ cashYieldDaily?: number;
37
+ /** When true, invariant checks run after each backtest. */
38
+ enableInvariantChecks?: boolean;
39
+ }
40
+ export interface SignalContext {
41
+ day: DayInput;
42
+ previousDay?: DayInput;
43
+ }
44
+ export interface DailyState {
45
+ date: string;
46
+ equityStart: number;
47
+ contribution: number;
48
+ /** External-flow-adjusted capital base before internal performance effects. */
49
+ capitalBaseBeforeReturn: number;
50
+ equityAfterTradeCost: number;
51
+ equityEnd: number;
52
+ signal: number;
53
+ targetPosition: number;
54
+ appliedPosition: number;
55
+ priorAppliedPosition: number;
56
+ turnoverNotional: number;
57
+ tradeCost: number;
58
+ grossPnl: number;
59
+ borrowDrag: number;
60
+ expenseDrag: number;
61
+ netPnl: number;
62
+ borrowRateDaily: number;
63
+ expenseRatioDaily: number;
64
+ }
65
+ export interface BacktestResult {
66
+ daily: DailyState[];
67
+ finalEquity: number;
68
+ twrCagr: number | null;
69
+ maxDrawdown: number;
70
+ benchmark?: BenchmarkComparison | null;
71
+ invariants?: BacktestInvariantReport;
72
+ }
73
+ export interface BenchmarkComparison {
74
+ dates: string[];
75
+ strategyReturns: number[];
76
+ benchmarkReturns: number[];
77
+ strategyEquity: number[];
78
+ benchmarkEquity: number[];
79
+ benchmarkTotalReturn: number;
80
+ benchmarkTwrCagr: number | null;
81
+ benchmarkVolatility: number | null;
82
+ benchmarkMaxDrawdown: number;
83
+ excessTotalReturn: number | null;
84
+ excessTwrCagr: number | null;
85
+ trackingError: number | null;
86
+ }
87
+ export interface EquityPoint {
88
+ date: string;
89
+ equity: number;
90
+ }
91
+ export interface ReturnPoint {
92
+ date: string;
93
+ value: number;
94
+ }
95
+ export interface CashFlowPoint {
96
+ date: string;
97
+ amount: number;
98
+ }
99
+ export interface MonteCarloConfig {
100
+ paths: number;
101
+ horizonDays: number;
102
+ seed?: number;
103
+ blockLength?: number;
104
+ }
105
+ export interface MonteCarloPathResult {
106
+ index: number;
107
+ seed: number;
108
+ result: BacktestResult;
109
+ }
110
+ export interface MonteCarloResult {
111
+ config: MonteCarloConfig;
112
+ paths: MonteCarloPathResult[];
113
+ summary: MonteCarloSummary;
114
+ invariants?: MonteCarloInvariantReport;
115
+ }
116
+ export interface PercentileSummary {
117
+ p5: number;
118
+ p50: number;
119
+ p95: number;
120
+ }
121
+ export interface MonteCarloSummary {
122
+ endingEquity: PercentileSummary;
123
+ twrCagr: PercentileSummary;
124
+ maxDrawdown: PercentileSummary;
125
+ medianEndingEquity: number;
126
+ medianTwrCagr: number;
127
+ medianMaxDrawdown: number;
128
+ }
129
+ export interface BacktestInvariantReport {
130
+ hasFiniteValues: boolean;
131
+ noSameDaySignalLeakage: boolean;
132
+ contributionBeforeReturnOrdering: boolean;
133
+ tradeCostZeroWithoutPositionChange: boolean;
134
+ borrowCostZeroAtOrBelowOneX: boolean;
135
+ borrowCostZeroInCashMode: boolean;
136
+ benchmarkMatchedDatesOnly: boolean;
137
+ isDailyDateSequenceStrictlyIncreasing: boolean;
138
+ }
139
+ export interface MonteCarloInvariantReport {
140
+ hasFiniteValues: boolean;
141
+ sameSeedReproducible: boolean;
142
+ differentSeedsCanDiffer: boolean;
143
+ pathDatesStrictlyIncreasing: boolean;
144
+ percentileSummariesOrdered: boolean;
145
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,21 @@
1
+ export type ValidationStatus = 'PASS' | 'FAIL' | 'WARN';
2
+ export interface ValidationResult {
3
+ id: string;
4
+ name: string;
5
+ status: ValidationStatus;
6
+ actual: string;
7
+ expected: string;
8
+ core: boolean;
9
+ }
10
+ export interface ValidationSummary {
11
+ total: number;
12
+ passed: number;
13
+ failed: number;
14
+ warnings: number;
15
+ failedCore: number;
16
+ }
17
+ export interface ValidationRun {
18
+ results: ValidationResult[];
19
+ summary: ValidationSummary;
20
+ }
21
+ export declare function runModelValidationCases(): ValidationRun;