@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runModelValidationHarness(): number;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runModelValidationHarness = runModelValidationHarness;
|
|
4
|
+
const validation_cases_1 = require("./validation-cases");
|
|
5
|
+
function runModelValidationHarness() {
|
|
6
|
+
const run = (0, validation_cases_1.runModelValidationCases)();
|
|
7
|
+
for (const result of run.results) {
|
|
8
|
+
console.log(`[${result.status}] ${result.id} ${result.name}`);
|
|
9
|
+
console.log(` actual: ${result.actual}`);
|
|
10
|
+
console.log(` expected: ${result.expected}`);
|
|
11
|
+
}
|
|
12
|
+
console.log('');
|
|
13
|
+
console.log('Validation summary');
|
|
14
|
+
console.log(` total passed: ${run.summary.passed}`);
|
|
15
|
+
console.log(` total failed: ${run.summary.failed}`);
|
|
16
|
+
console.log(` warnings: ${run.summary.warnings}`);
|
|
17
|
+
if (run.summary.failedCore > 0) {
|
|
18
|
+
console.error(`Core validation failures: ${run.summary.failedCore}`);
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
if (require.main === module) {
|
|
24
|
+
const exitCode = runModelValidationHarness();
|
|
25
|
+
process.exit(exitCode);
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { BacktestInvariantReport, BenchmarkComparison, DailyState, DayInput, EngineConfig, MonteCarloConfig, MonteCarloInvariantReport, MonteCarloResult } from './types';
|
|
2
|
+
export declare function validateConfig(config: EngineConfig): void;
|
|
3
|
+
export declare function validateDayInputs(days: DayInput[], config: EngineConfig): void;
|
|
4
|
+
export declare function validateMonteCarloConfig(config: MonteCarloConfig): void;
|
|
5
|
+
export declare function assertBacktestInvariants(daily: DailyState[]): void;
|
|
6
|
+
export declare function evaluateBacktestInvariants(daily: DailyState[], benchmark: BenchmarkComparison | null): BacktestInvariantReport;
|
|
7
|
+
export declare function evaluateBenchmarkAlignmentInvariant(benchmark: BenchmarkComparison | null): boolean;
|
|
8
|
+
export declare function evaluateMonteCarloInvariants(first: MonteCarloResult, secondSameSeed: MonteCarloResult, thirdDifferentSeed: MonteCarloResult): MonteCarloInvariantReport;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateConfig = validateConfig;
|
|
4
|
+
exports.validateDayInputs = validateDayInputs;
|
|
5
|
+
exports.validateMonteCarloConfig = validateMonteCarloConfig;
|
|
6
|
+
exports.assertBacktestInvariants = assertBacktestInvariants;
|
|
7
|
+
exports.evaluateBacktestInvariants = evaluateBacktestInvariants;
|
|
8
|
+
exports.evaluateBenchmarkAlignmentInvariant = evaluateBenchmarkAlignmentInvariant;
|
|
9
|
+
exports.evaluateMonteCarloInvariants = evaluateMonteCarloInvariants;
|
|
10
|
+
function isValidIsoDate(date) {
|
|
11
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const d = new Date(`${date}T00:00:00Z`);
|
|
15
|
+
return !Number.isNaN(d.getTime()) && d.toISOString().startsWith(date);
|
|
16
|
+
}
|
|
17
|
+
function assertFinite(value, label) {
|
|
18
|
+
if (!Number.isFinite(value)) {
|
|
19
|
+
throw new Error(`${label} must be finite`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function validateConfig(config) {
|
|
23
|
+
assertFinite(config.initialEquity, 'initialEquity');
|
|
24
|
+
if (config.initialEquity <= 0) {
|
|
25
|
+
throw new Error('initialEquity must be positive');
|
|
26
|
+
}
|
|
27
|
+
assertFinite(config.tradeCostRate, 'tradeCostRate');
|
|
28
|
+
if (config.tradeCostRate < 0) {
|
|
29
|
+
throw new Error('tradeCostRate must be non-negative');
|
|
30
|
+
}
|
|
31
|
+
if (config.tradeCostRate > 0.1) {
|
|
32
|
+
throw new Error('tradeCostRate appears unrealistic (>10%)');
|
|
33
|
+
}
|
|
34
|
+
assertFinite(config.maxLeverage, 'maxLeverage');
|
|
35
|
+
if (config.maxLeverage <= 0) {
|
|
36
|
+
throw new Error('maxLeverage must be positive');
|
|
37
|
+
}
|
|
38
|
+
if (config.initialTargetPosition !== undefined) {
|
|
39
|
+
assertFinite(config.initialTargetPosition, 'initialTargetPosition');
|
|
40
|
+
if (Math.abs(config.initialTargetPosition) > config.maxLeverage) {
|
|
41
|
+
throw new Error('initialTargetPosition exceeds maxLeverage bounds');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (config.initialAppliedPosition !== undefined) {
|
|
45
|
+
assertFinite(config.initialAppliedPosition, 'initialAppliedPosition');
|
|
46
|
+
if (Math.abs(config.initialAppliedPosition) > config.maxLeverage) {
|
|
47
|
+
throw new Error('initialAppliedPosition exceeds maxLeverage bounds');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (config.initialBorrowRateDaily !== undefined) {
|
|
51
|
+
assertFinite(config.initialBorrowRateDaily, 'initialBorrowRateDaily');
|
|
52
|
+
if (config.initialBorrowRateDaily <= -1) {
|
|
53
|
+
throw new Error('initialBorrowRateDaily must be greater than -1');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (config.cashYieldDaily !== undefined) {
|
|
57
|
+
assertFinite(config.cashYieldDaily, 'cashYieldDaily');
|
|
58
|
+
if (config.cashYieldDaily <= -1) {
|
|
59
|
+
throw new Error('cashYieldDaily must be greater than -1');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function validateDayInputs(days, config) {
|
|
64
|
+
if (days.length === 0) {
|
|
65
|
+
throw new Error('days must not be empty');
|
|
66
|
+
}
|
|
67
|
+
let hasKnownBorrowRate = config.initialBorrowRateDaily !== undefined;
|
|
68
|
+
for (let i = 0; i < days.length; i += 1) {
|
|
69
|
+
const current = days[i];
|
|
70
|
+
if (!isValidIsoDate(current.date)) {
|
|
71
|
+
throw new Error(`invalid date format for ${current.date}; expected YYYY-MM-DD`);
|
|
72
|
+
}
|
|
73
|
+
assertFinite(current.assetReturn, `assetReturn for date ${current.date}`);
|
|
74
|
+
assertFinite(current.signalInput, `signalInput for date ${current.date}`);
|
|
75
|
+
if (current.contribution !== undefined) {
|
|
76
|
+
assertFinite(current.contribution, `contribution for date ${current.date}`);
|
|
77
|
+
if (current.contribution < 0) {
|
|
78
|
+
throw new Error(`negative contribution is not allowed for date ${current.date}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (current.borrowRateDaily !== undefined) {
|
|
82
|
+
assertFinite(current.borrowRateDaily, `borrowRateDaily for date ${current.date}`);
|
|
83
|
+
if (current.borrowRateDaily <= -1) {
|
|
84
|
+
throw new Error(`borrowRateDaily must be greater than -1 for date ${current.date}`);
|
|
85
|
+
}
|
|
86
|
+
hasKnownBorrowRate = true;
|
|
87
|
+
}
|
|
88
|
+
if (current.expenseRatioAnnual !== undefined) {
|
|
89
|
+
assertFinite(current.expenseRatioAnnual, `expenseRatioAnnual for date ${current.date}`);
|
|
90
|
+
if (current.expenseRatioAnnual <= -1) {
|
|
91
|
+
throw new Error(`expenseRatioAnnual must be greater than -1 for date ${current.date}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (current.tradeSpread !== undefined) {
|
|
95
|
+
assertFinite(current.tradeSpread, `tradeSpread for date ${current.date}`);
|
|
96
|
+
if (current.tradeSpread < 0) {
|
|
97
|
+
throw new Error(`tradeSpread must be non-negative for date ${current.date}`);
|
|
98
|
+
}
|
|
99
|
+
if (current.tradeSpread > 0.1) {
|
|
100
|
+
throw new Error(`tradeSpread appears unrealistic (>10%) for date ${current.date}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (current.benchmarkReturn !== undefined) {
|
|
104
|
+
assertFinite(current.benchmarkReturn, `benchmarkReturn for date ${current.date}`);
|
|
105
|
+
}
|
|
106
|
+
if (i > 0) {
|
|
107
|
+
const prev = days[i - 1];
|
|
108
|
+
if (prev.date >= current.date) {
|
|
109
|
+
throw new Error('dates must be strictly increasing (ISO YYYY-MM-DD)');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (i === 0 && !hasKnownBorrowRate) {
|
|
113
|
+
throw new Error('first borrow rate must be explicit: provide day[0].borrowRateDaily or config.initialBorrowRateDaily');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function validateMonteCarloConfig(config) {
|
|
118
|
+
assertFinite(config.paths, 'paths');
|
|
119
|
+
assertFinite(config.horizonDays, 'horizonDays');
|
|
120
|
+
if (!Number.isInteger(config.paths) || config.paths <= 0) {
|
|
121
|
+
throw new Error('paths must be a positive integer');
|
|
122
|
+
}
|
|
123
|
+
if (!Number.isInteger(config.horizonDays) || config.horizonDays <= 0) {
|
|
124
|
+
throw new Error('horizonDays must be a positive integer');
|
|
125
|
+
}
|
|
126
|
+
if (config.seed !== undefined) {
|
|
127
|
+
assertFinite(config.seed, 'seed');
|
|
128
|
+
if (!Number.isInteger(config.seed) || config.seed < 0) {
|
|
129
|
+
throw new Error('seed must be a non-negative integer');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (config.blockLength !== undefined) {
|
|
133
|
+
assertFinite(config.blockLength, 'blockLength');
|
|
134
|
+
if (!Number.isInteger(config.blockLength) || config.blockLength <= 0) {
|
|
135
|
+
throw new Error('blockLength must be a positive integer');
|
|
136
|
+
}
|
|
137
|
+
if (config.blockLength > config.horizonDays) {
|
|
138
|
+
throw new Error('blockLength must not exceed horizonDays');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function assertBacktestInvariants(daily) {
|
|
143
|
+
const report = evaluateBacktestInvariants(daily, null);
|
|
144
|
+
const failed = Object.entries(report).find(([, value]) => !value);
|
|
145
|
+
if (failed) {
|
|
146
|
+
throw new Error(`backtest invariant failed: ${failed[0]}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function evaluateBacktestInvariants(daily, benchmark) {
|
|
150
|
+
let hasFiniteValues = true;
|
|
151
|
+
let noSameDaySignalLeakage = true;
|
|
152
|
+
let contributionBeforeReturnOrdering = true;
|
|
153
|
+
let tradeCostZeroWithoutPositionChange = true;
|
|
154
|
+
let borrowCostZeroAtOrBelowOneX = true;
|
|
155
|
+
let borrowCostZeroInCashMode = true;
|
|
156
|
+
let isDailyDateSequenceStrictlyIncreasing = true;
|
|
157
|
+
for (let i = 0; i < daily.length; i += 1) {
|
|
158
|
+
const current = daily[i];
|
|
159
|
+
const numericValues = [
|
|
160
|
+
current.equityStart,
|
|
161
|
+
current.contribution,
|
|
162
|
+
current.capitalBaseBeforeReturn,
|
|
163
|
+
current.equityAfterTradeCost,
|
|
164
|
+
current.equityEnd,
|
|
165
|
+
current.signal,
|
|
166
|
+
current.targetPosition,
|
|
167
|
+
current.appliedPosition,
|
|
168
|
+
current.priorAppliedPosition,
|
|
169
|
+
current.turnoverNotional,
|
|
170
|
+
current.tradeCost,
|
|
171
|
+
current.grossPnl,
|
|
172
|
+
current.borrowDrag,
|
|
173
|
+
current.expenseDrag,
|
|
174
|
+
current.netPnl,
|
|
175
|
+
current.borrowRateDaily,
|
|
176
|
+
current.expenseRatioDaily,
|
|
177
|
+
];
|
|
178
|
+
if (numericValues.some((value) => !Number.isFinite(value))) {
|
|
179
|
+
hasFiniteValues = false;
|
|
180
|
+
}
|
|
181
|
+
if (current.capitalBaseBeforeReturn !== current.equityStart + current.contribution) {
|
|
182
|
+
contributionBeforeReturnOrdering = false;
|
|
183
|
+
}
|
|
184
|
+
if (current.appliedPosition === current.priorAppliedPosition && current.tradeCost !== 0) {
|
|
185
|
+
tradeCostZeroWithoutPositionChange = false;
|
|
186
|
+
}
|
|
187
|
+
if (Math.abs(current.appliedPosition) <= 1 && current.borrowDrag !== 0) {
|
|
188
|
+
borrowCostZeroAtOrBelowOneX = false;
|
|
189
|
+
}
|
|
190
|
+
if (current.appliedPosition === 0 && current.borrowDrag !== 0) {
|
|
191
|
+
borrowCostZeroInCashMode = false;
|
|
192
|
+
}
|
|
193
|
+
if (i > 0) {
|
|
194
|
+
const previous = daily[i - 1];
|
|
195
|
+
if (current.appliedPosition !== previous.targetPosition) {
|
|
196
|
+
noSameDaySignalLeakage = false;
|
|
197
|
+
}
|
|
198
|
+
if (previous.date >= current.date) {
|
|
199
|
+
isDailyDateSequenceStrictlyIncreasing = false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
hasFiniteValues,
|
|
205
|
+
noSameDaySignalLeakage,
|
|
206
|
+
contributionBeforeReturnOrdering,
|
|
207
|
+
tradeCostZeroWithoutPositionChange,
|
|
208
|
+
borrowCostZeroAtOrBelowOneX,
|
|
209
|
+
borrowCostZeroInCashMode,
|
|
210
|
+
benchmarkMatchedDatesOnly: evaluateBenchmarkAlignmentInvariant(benchmark),
|
|
211
|
+
isDailyDateSequenceStrictlyIncreasing,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function evaluateBenchmarkAlignmentInvariant(benchmark) {
|
|
215
|
+
if (!benchmark) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
const expectedLength = benchmark.dates.length;
|
|
219
|
+
return (benchmark.strategyEquity.length === expectedLength &&
|
|
220
|
+
benchmark.benchmarkEquity.length === expectedLength &&
|
|
221
|
+
benchmark.strategyReturns.length === expectedLength &&
|
|
222
|
+
benchmark.benchmarkReturns.length === expectedLength &&
|
|
223
|
+
benchmark.dates.every((date, idx) => idx === 0 || benchmark.dates[idx - 1] < date));
|
|
224
|
+
}
|
|
225
|
+
function evaluateMonteCarloInvariants(first, secondSameSeed, thirdDifferentSeed) {
|
|
226
|
+
const firstSerialized = JSON.stringify(first);
|
|
227
|
+
const secondSerialized = JSON.stringify(secondSameSeed);
|
|
228
|
+
const thirdSerialized = JSON.stringify(thirdDifferentSeed);
|
|
229
|
+
const pathDatesStrictlyIncreasing = first.paths.every((path) => path.result.daily.every((day, index) => index === 0 || path.result.daily[index - 1].date < day.date));
|
|
230
|
+
const hasFiniteValues = first.paths.every((path) => path.result.daily.every((day) => Object.values(day).every((value) => typeof value !== 'number' || Number.isFinite(value))));
|
|
231
|
+
const percentileSummariesOrdered = isOrderedPercentiles(first.summary.endingEquity)
|
|
232
|
+
&& isOrderedPercentiles(first.summary.twrCagr)
|
|
233
|
+
&& isOrderedPercentiles(first.summary.maxDrawdown);
|
|
234
|
+
return {
|
|
235
|
+
hasFiniteValues,
|
|
236
|
+
sameSeedReproducible: firstSerialized === secondSerialized,
|
|
237
|
+
differentSeedsCanDiffer: firstSerialized !== thirdSerialized,
|
|
238
|
+
pathDatesStrictlyIncreasing,
|
|
239
|
+
percentileSummariesOrdered,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function isOrderedPercentiles(summary) {
|
|
243
|
+
return summary.p5 <= summary.p50 && summary.p50 <= summary.p95;
|
|
244
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@samsmith2121/synthetic-leverage-engine",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "commonjs",
|
|
5
|
+
"main": "./dist/src/index.js",
|
|
6
|
+
"types": "./dist/src/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/src/**/*",
|
|
9
|
+
"package.json",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/src/index.d.ts",
|
|
15
|
+
"require": "./dist/src/index.js",
|
|
16
|
+
"default": "./dist/src/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./simulator": {
|
|
19
|
+
"types": "./dist/src/simulator/index.d.ts",
|
|
20
|
+
"require": "./dist/src/simulator/index.js",
|
|
21
|
+
"default": "./dist/src/simulator/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"clean": "node -e \"const fs=require('fs'); fs.rmSync('dist',{ recursive:true, force:true });\"",
|
|
26
|
+
"build": "npm run clean && tsc --project tsconfig.json",
|
|
27
|
+
"typecheck": "tsc --noEmit --project tsconfig.json",
|
|
28
|
+
"test": "npm run build && node --test dist/tests/*.test.js",
|
|
29
|
+
"validate:model": "npm run build && node dist/src/validation-harness.js",
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22.14.1",
|
|
34
|
+
"typescript": "^5.8.3"
|
|
35
|
+
}
|
|
36
|
+
}
|