@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
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # synthetic-leverage-engine
2
+ Engine and tests for a synthetic leverage simulator
3
+
4
+ ## Handoff note for UI builders
5
+ - The engine is the single source of truth for financial logic.
6
+ - UI integrations should import from `src/public-api.ts` (or package root exports that re-export it) and call engine functions only.
7
+ - UI integrations should avoid importing low-level internal engine files directly unless the public engine surface is intentionally extended first.
8
+ - Do not duplicate backtest, Monte Carlo, benchmark, validation, metrics, diagnostics, or cost logic in UI code.
9
+ - Cash yield behavior is explicit and configurable (`cashYieldDaily`); zero-yield is a supported/default mode, not the only mode.
10
+ - If UI needs new output shapes, extend the engine first and keep locked accounting order unchanged.
11
+
12
+ The repository now includes a deterministic research workflow on top of the trusted fixed-path engine:
13
+
14
+ - Strategy diagnostics explaining exposure, transitions, cost mix, drawdown profile, and benchmark-relative behavior (`buildStrategyDiagnostics` via run summaries)
15
+ - Sensitivity analysis utilities:
16
+ - one-way sensitivity (`runOneWaySensitivity`)
17
+ - two-way sensitivity with grid-friendly cells (`runTwoWaySensitivity`)
18
+ - deterministic ranking helper (`rankStrategyRuns`)
19
+ - Structured analysis warnings (`AnalysisWarning`) surfaced directly in strategy/sweep/scenario outputs
20
+ - Rule-based research summary builders (`buildResearchSummaries`) for decision-ready but descriptive recommendations
21
+ - Export-ready flat rows for downstream CSV pipelines:
22
+ - backtest summary rows
23
+ - strategy comparison rows
24
+ - parameter sweep rows
25
+ - scenario rows
26
+ - one-way/two-way sensitivity rows
27
+ - diagnostics rows
28
+
29
+ Core files:
30
+ - `src/analysis.ts`
31
+ - `src/reporting.ts`
32
+ - `src/analysis-cli.ts` (minimal script entry point)
33
+
34
+ ### Minimal CLI usage
35
+
36
+ Build first:
37
+
38
+ ```bash
39
+ npm run build
40
+ ```
41
+
42
+ Run modes:
43
+
44
+ ```bash
45
+ node dist/src/analysis-cli.js comparison <days.json> <config.json> <payload.json>
46
+ node dist/src/analysis-cli.js sweep <days.json> <config.json> <payload.json>
47
+ node dist/src/analysis-cli.js scenario <days.json> <config.json> <payload.json>
48
+ node dist/src/analysis-cli.js backtest <days.json> <config.json> [payload.json]
49
+ ```
50
+
51
+ Payload JSON provides typed strategy variants, sweep grids, or scenario definitions.
52
+
53
+ ## Model validation harness
54
+
55
+ Run the automated model validation suite:
56
+
57
+ ```bash
58
+ npm run validate:model
59
+ ```
60
+
61
+ The harness prints a human-readable PASS/FAIL/WARN report for each validation case, including actual values and expected conditions. It also prints a summary with total passed, total failed, and warnings. The command exits non-zero when any core validation fails.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /* eslint-disable no-console */
4
+ const node_fs_1 = require("node:fs");
5
+ const analysis_1 = require("./analysis");
6
+ const reporting_1 = require("./reporting");
7
+ function loadJson(path) {
8
+ return JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
9
+ }
10
+ function main() {
11
+ const [, , mode, daysPath, configPath, payloadPath] = process.argv;
12
+ if (!mode || !daysPath || !configPath) {
13
+ console.error('Usage: node dist/src/analysis-cli.js <backtest|comparison|sweep|scenario> <days.json> <config.json> [payload.json]');
14
+ process.exit(1);
15
+ }
16
+ const days = loadJson(daysPath);
17
+ const config = loadJson(configPath);
18
+ if (mode === 'backtest') {
19
+ const payload = payloadPath ? loadJson(payloadPath) : { variants: [] };
20
+ const comparison = (0, analysis_1.runStrategyComparison)(days, config, payload.variants ?? []);
21
+ console.log(JSON.stringify((0, reporting_1.toStrategyComparisonRows)(comparison), null, 2));
22
+ return;
23
+ }
24
+ if (!payloadPath) {
25
+ console.error(`Mode ${mode} requires payload.json`);
26
+ process.exit(1);
27
+ }
28
+ if (mode === 'comparison') {
29
+ const payload = loadJson(payloadPath);
30
+ console.log(JSON.stringify((0, analysis_1.runStrategyComparison)(days, config, payload.variants), null, 2));
31
+ return;
32
+ }
33
+ if (mode === 'sweep') {
34
+ const payload = loadJson(payloadPath);
35
+ console.log(JSON.stringify((0, analysis_1.runParameterSweep)(days, config, payload.grid, payload.options), null, 2));
36
+ return;
37
+ }
38
+ if (mode === 'scenario') {
39
+ const payload = loadJson(payloadPath);
40
+ const scenarios = payload.rolling
41
+ ? (0, analysis_1.buildRollingWindowScenarios)(days, payload.rolling.windowLengthDays, payload.rolling.stepDays)
42
+ : payload.scenarios ?? [];
43
+ const scenarioResult = (0, analysis_1.runScenarioAnalysis)(days, config, payload.variant, scenarios);
44
+ const comparison = (0, analysis_1.runStrategyComparison)(days, config, [payload.variant]);
45
+ const sweep = (0, analysis_1.runParameterSweep)(days, config, {
46
+ vixThresholds: [payload.variant.signal?.vixThreshold ?? 20],
47
+ leverageMultiples: [payload.variant.signal?.leverageMultiple ?? 1],
48
+ tradeSpreads: [payload.variant.tradeSpread ?? 0],
49
+ expenseRatiosAnnual: [payload.variant.expenseRatioAnnual ?? 0],
50
+ cashModes: [payload.variant.cashMode ?? 'zero_yield'],
51
+ cashYieldDailyValues: [payload.variant.cashYieldDaily ?? 0],
52
+ });
53
+ console.log(JSON.stringify((0, reporting_1.buildAnalysisReport)(comparison, sweep, scenarioResult), null, 2));
54
+ return;
55
+ }
56
+ console.error(`Unknown mode: ${mode}`);
57
+ process.exit(1);
58
+ }
59
+ main();
@@ -0,0 +1,214 @@
1
+ import { BacktestResult, DayInput, EngineConfig, MonteCarloConfig } from './types';
2
+ export type CashModeOption = 'zero_yield' | 'configured_yield';
3
+ export type SweepRankMetric = 'twrCagr' | 'xirr' | 'maxDrawdown' | 'volatility' | 'excessTwrCagr' | 'returnToDrawdown';
4
+ export type AnalysisWarningCode = 'BENCHMARK_UNAVAILABLE' | 'BENCHMARK_PARTIAL_COVERAGE' | 'SCENARIO_TOO_FEW_OBSERVATIONS' | 'MONTE_CARLO_BLOCK_TOO_LONG' | 'UNUSUAL_ASSUMPTION' | 'XIRR_UNAVAILABLE' | 'SWEEP_VALIDATION_FAILED';
5
+ export interface AnalysisWarning {
6
+ code: AnalysisWarningCode;
7
+ message: string;
8
+ context?: Record<string, string | number | boolean | null>;
9
+ }
10
+ export interface SignalThresholdConfig {
11
+ vixThreshold: number;
12
+ leverageMultiple: number;
13
+ cashExposure?: number;
14
+ /**
15
+ * When true, treats signalInput as a boolean-style indicator where 1 means
16
+ * "price above SMA" and 0 means "price below SMA".
17
+ * Comparison becomes signalInput > vixThreshold (set vixThreshold: 0).
18
+ * Without this flag the default VIX-scale comparison is signalInput <= vixThreshold,
19
+ * which silently suppresses all SMA-based switching because SMA distance values
20
+ * (~0.05 magnitude) are always below any VIX-scale threshold like 20.
21
+ */
22
+ smaMode?: boolean;
23
+ }
24
+ export interface StrategyVariant {
25
+ name: string;
26
+ signal?: SignalThresholdConfig;
27
+ tradeSpread?: number;
28
+ expenseRatioAnnual?: number;
29
+ cashMode?: CashModeOption;
30
+ cashYieldDaily?: number;
31
+ monteCarloBlockLength?: number;
32
+ engineConfigOverrides?: Partial<EngineConfig>;
33
+ }
34
+ export interface ExposureDiagnostics {
35
+ pctDaysCash: number;
36
+ pctDaysOneX: number;
37
+ pctDaysLeveraged: number;
38
+ averageAppliedLeverage: number;
39
+ leverageDistribution: Record<string, number>;
40
+ }
41
+ export interface TransitionDiagnostics {
42
+ totalSwitches: number;
43
+ transitionCounts: Record<string, number>;
44
+ averageHoldingDurationByRegime: Record<string, number>;
45
+ }
46
+ export interface CostDiagnostics {
47
+ totalTradeCost: number;
48
+ totalBorrowCost: number;
49
+ totalErCost: number;
50
+ totalCost: number;
51
+ tradeCostShare: number;
52
+ borrowCostShare: number;
53
+ erCostShare: number;
54
+ totalCostPctFinalEquity: number | null;
55
+ totalCostPctInvestedCapital: number | null;
56
+ }
57
+ export interface DrawdownDiagnostics {
58
+ maxDrawdown: number;
59
+ maxDrawdownDurationDays: number;
60
+ episodesAbove10Pct: number;
61
+ episodesAbove20Pct: number;
62
+ episodesAbove30Pct: number;
63
+ }
64
+ export interface BenchmarkRelativeDiagnostics {
65
+ matchedPeriods: number;
66
+ outperformPeriods: number;
67
+ outperformPct: number | null;
68
+ averageExcessReturn: number | null;
69
+ medianExcessReturn: number | null;
70
+ excessTwrCagr: number | null;
71
+ }
72
+ export interface StrategyDiagnostics {
73
+ exposure: ExposureDiagnostics;
74
+ transitions: TransitionDiagnostics;
75
+ costs: CostDiagnostics;
76
+ drawdown: DrawdownDiagnostics;
77
+ benchmarkRelative?: BenchmarkRelativeDiagnostics;
78
+ }
79
+ export interface AnalysisRunSummary {
80
+ strategyName: string;
81
+ finalEquity: number;
82
+ totalInvested: number;
83
+ twrCagr: number | null;
84
+ xirr: number | null;
85
+ maxDrawdown: number;
86
+ volatility: number | null;
87
+ totalTradeCost: number;
88
+ totalBorrowCost: number;
89
+ totalErCost: number;
90
+ totalSwitches: number;
91
+ diagnostics: StrategyDiagnostics;
92
+ benchmark?: AnalysisBenchmarkSummary;
93
+ }
94
+ export interface AnalysisBenchmarkSummary {
95
+ benchmarkTotalReturn: number;
96
+ benchmarkTwrCagr: number | null;
97
+ benchmarkVolatility: number | null;
98
+ benchmarkMaxDrawdown: number;
99
+ excessTotalReturn: number | null;
100
+ excessTwrCagr: number | null;
101
+ trackingError: number | null;
102
+ relativeMaxDrawdown: number;
103
+ }
104
+ export interface StrategyRunSuccess {
105
+ ok: true;
106
+ strategy: StrategyVariant;
107
+ summary: AnalysisRunSummary;
108
+ warnings: AnalysisWarning[];
109
+ }
110
+ export interface StrategyRunFailure {
111
+ ok: false;
112
+ strategy: StrategyVariant;
113
+ error: string;
114
+ warnings: AnalysisWarning[];
115
+ }
116
+ export type StrategyRunResult = StrategyRunSuccess | StrategyRunFailure;
117
+ export interface StrategyConfigComparison {
118
+ runs: StrategyRunResult[];
119
+ ranking: StrategyRankingEntry[];
120
+ }
121
+ export interface StrategyRankingEntry {
122
+ strategyName: string;
123
+ metric: SweepRankMetric;
124
+ rank: number;
125
+ value: number | null;
126
+ }
127
+ export interface ParameterSweepGrid {
128
+ vixThresholds: number[];
129
+ leverageMultiples: number[];
130
+ tradeSpreads: number[];
131
+ expenseRatiosAnnual: number[];
132
+ cashModes: CashModeOption[];
133
+ cashYieldDailyValues: number[];
134
+ monteCarloBlockLengths?: number[];
135
+ }
136
+ export interface ParameterSweepConfig {
137
+ baseVariantName?: string;
138
+ rankBy?: SweepRankMetric;
139
+ monteCarloConfig?: Pick<MonteCarloConfig, 'horizonDays' | 'paths' | 'seed'>;
140
+ }
141
+ export interface SweepResultRow {
142
+ strategyName: string;
143
+ vixThreshold: number;
144
+ leverageMultiple: number;
145
+ tradeSpread: number;
146
+ expenseRatioAnnual: number;
147
+ cashMode: CashModeOption;
148
+ cashYieldDaily: number;
149
+ monteCarloBlockLength?: number;
150
+ run: StrategyRunResult;
151
+ }
152
+ export interface ParameterSweepResult {
153
+ rows: SweepResultRow[];
154
+ ranking: StrategyRankingEntry[];
155
+ }
156
+ export interface ScenarioDefinition {
157
+ name: string;
158
+ startDate: string;
159
+ endDate: string;
160
+ }
161
+ export interface ScenarioResultRow {
162
+ scenarioName: string;
163
+ startDate: string;
164
+ endDate: string;
165
+ run: StrategyRunResult;
166
+ warnings: AnalysisWarning[];
167
+ }
168
+ export interface ScenarioAnalysisResult {
169
+ rows: ScenarioResultRow[];
170
+ }
171
+ export type SensitivityParameter = 'vixThreshold' | 'leverageMultiple' | 'tradeSpread' | 'expenseRatioAnnual' | 'cashYieldDaily' | 'cashMode';
172
+ export interface OneWaySensitivityRow {
173
+ parameter: SensitivityParameter;
174
+ parameterValue: number | string;
175
+ run: StrategyRunResult;
176
+ }
177
+ export interface OneWaySensitivityResult {
178
+ parameter: SensitivityParameter;
179
+ rows: OneWaySensitivityRow[];
180
+ ranking: StrategyRankingEntry[];
181
+ }
182
+ export interface TwoWaySensitivityCell {
183
+ xParameter: SensitivityParameter;
184
+ xValue: number | string;
185
+ yParameter: SensitivityParameter;
186
+ yValue: number | string;
187
+ run: StrategyRunResult;
188
+ }
189
+ export interface TwoWaySensitivityResult {
190
+ xParameter: SensitivityParameter;
191
+ yParameter: SensitivityParameter;
192
+ xValues: Array<number | string>;
193
+ yValues: Array<number | string>;
194
+ cells: TwoWaySensitivityCell[];
195
+ ranking: StrategyRankingEntry[];
196
+ }
197
+ export type ResearchSummaryKind = 'best_growth_drawdown_tradeoff' | 'highest_twr_under_drawdown_constraint' | 'lowest_cost' | 'most_benchmark_outperforming' | 'most_robust_across_scenarios';
198
+ export interface ResearchSummaryRecommendation {
199
+ kind: ResearchSummaryKind;
200
+ strategyName: string;
201
+ rationale: string;
202
+ metrics: Record<string, number | string | null>;
203
+ }
204
+ export declare function runStrategyComparison(days: DayInput[], baseConfig: EngineConfig, variants: StrategyVariant[], rankBy?: SweepRankMetric): StrategyConfigComparison;
205
+ export declare function runParameterSweep(days: DayInput[], baseConfig: EngineConfig, grid: ParameterSweepGrid, config?: ParameterSweepConfig): ParameterSweepResult;
206
+ export declare function runScenarioAnalysis(days: DayInput[], baseConfig: EngineConfig, variant: StrategyVariant, scenarios: ScenarioDefinition[], includeFullPeriod?: boolean): ScenarioAnalysisResult;
207
+ export declare function buildRollingWindowScenarios(days: DayInput[], windowLengthDays: number, stepDays: number): ScenarioDefinition[];
208
+ export declare function runOneWaySensitivity(days: DayInput[], baseConfig: EngineConfig, baseVariant: StrategyVariant, parameter: SensitivityParameter, values: Array<number | string>, rankBy?: SweepRankMetric): OneWaySensitivityResult;
209
+ export declare function runTwoWaySensitivity(days: DayInput[], baseConfig: EngineConfig, baseVariant: StrategyVariant, xParameter: SensitivityParameter, xValues: Array<number | string>, yParameter: SensitivityParameter, yValues: Array<number | string>, rankBy?: SweepRankMetric): TwoWaySensitivityResult;
210
+ export declare function buildResearchSummaries(comparison: StrategyConfigComparison, scenarioResult?: ScenarioAnalysisResult, options?: {
211
+ maxDrawdownConstraint?: number;
212
+ }): ResearchSummaryRecommendation[];
213
+ export declare function rankStrategyRuns(runs: StrategyRunResult[], metric: SweepRankMetric): StrategyRankingEntry[];
214
+ export declare function buildStrategyDiagnostics(backtest: BacktestResult, totalInvested?: number): StrategyDiagnostics;