@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.
- package/README.md +61 -0
- package/dist/src/analysis-cli.d.ts +1 -0
- package/dist/src/analysis-cli.js +59 -0
- package/dist/src/analysis.d.ts +214 -0
- package/dist/src/analysis.js +843 -0
- package/dist/src/backtest.d.ts +3 -0
- package/dist/src/backtest.js +172 -0
- package/dist/src/benchmark.d.ts +11 -0
- package/dist/src/benchmark.js +85 -0
- package/dist/src/contributions.d.ts +4 -0
- package/dist/src/contributions.js +11 -0
- package/dist/src/costs.d.ts +8 -0
- package/dist/src/costs.js +29 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.js +34 -0
- package/dist/src/metrics.d.ts +15 -0
- package/dist/src/metrics.js +232 -0
- package/dist/src/public-api.d.ts +89 -0
- package/dist/src/public-api.js +103 -0
- package/dist/src/reporting.d.ts +112 -0
- package/dist/src/reporting.js +156 -0
- package/dist/src/signal.d.ts +34 -0
- package/dist/src/signal.js +80 -0
- package/dist/src/simulator/adapters.d.ts +9 -0
- package/dist/src/simulator/adapters.js +99 -0
- package/dist/src/simulator/index.d.ts +4 -0
- package/dist/src/simulator/index.js +31 -0
- package/dist/src/simulator/monte-carlo.d.ts +8 -0
- package/dist/src/simulator/monte-carlo.js +94 -0
- package/dist/src/simulator/series.d.ts +6 -0
- package/dist/src/simulator/series.js +152 -0
- package/dist/src/simulator/types.d.ts +111 -0
- package/dist/src/simulator/types.js +2 -0
- package/dist/src/step.d.ts +24 -0
- package/dist/src/step.js +75 -0
- package/dist/src/types.d.ts +145 -0
- package/dist/src/types.js +2 -0
- package/dist/src/validation-cases.d.ts +21 -0
- package/dist/src/validation-cases.js +930 -0
- package/dist/src/validation-harness.d.ts +1 -0
- package/dist/src/validation-harness.js +26 -0
- package/dist/src/validation.d.ts +8 -0
- package/dist/src/validation.js +244 -0
- 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;
|