@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,3 @@
|
|
|
1
|
+
import { BacktestResult, DayInput, EngineConfig, MonteCarloConfig, MonteCarloResult } from './types';
|
|
2
|
+
export declare function runFixedBacktest(days: DayInput[], config: EngineConfig): BacktestResult;
|
|
3
|
+
export declare function runMonteCarloBacktest(sourceDays: DayInput[], engineConfig: EngineConfig, mcConfig: MonteCarloConfig): MonteCarloResult;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runFixedBacktest = runFixedBacktest;
|
|
4
|
+
exports.runMonteCarloBacktest = runMonteCarloBacktest;
|
|
5
|
+
const benchmark_1 = require("./benchmark");
|
|
6
|
+
const step_1 = require("./step");
|
|
7
|
+
const metrics_1 = require("./metrics");
|
|
8
|
+
const validation_1 = require("./validation");
|
|
9
|
+
const DEFAULT_BLOCK_LENGTH = 5;
|
|
10
|
+
function runFixedBacktest(days, config) {
|
|
11
|
+
(0, validation_1.validateConfig)(config);
|
|
12
|
+
(0, validation_1.validateDayInputs)(days, config);
|
|
13
|
+
const daily = [];
|
|
14
|
+
let previousTargetPosition = config.initialTargetPosition ?? 0;
|
|
15
|
+
let previousAppliedPosition = config.initialAppliedPosition ?? previousTargetPosition;
|
|
16
|
+
let previousBorrowRateDaily = config.initialBorrowRateDaily ?? days[0].borrowRateDaily;
|
|
17
|
+
for (let i = 0; i < days.length; i += 1) {
|
|
18
|
+
const day = days[i];
|
|
19
|
+
const previousDay = i > 0 ? days[i - 1] : undefined;
|
|
20
|
+
const previousState = i > 0 ? daily[i - 1] : undefined;
|
|
21
|
+
const state = (0, step_1.runDayStep)({
|
|
22
|
+
day,
|
|
23
|
+
previousDay,
|
|
24
|
+
previousState,
|
|
25
|
+
previousTargetPosition,
|
|
26
|
+
previousAppliedPosition,
|
|
27
|
+
previousBorrowRateDaily,
|
|
28
|
+
config,
|
|
29
|
+
});
|
|
30
|
+
daily.push(state);
|
|
31
|
+
previousTargetPosition = state.targetPosition;
|
|
32
|
+
previousAppliedPosition = state.appliedPosition;
|
|
33
|
+
previousBorrowRateDaily = state.borrowRateDaily;
|
|
34
|
+
}
|
|
35
|
+
const result = (0, step_1.summarizeResult)(daily);
|
|
36
|
+
result.benchmark = buildBenchmarkResult(days, result);
|
|
37
|
+
result.invariants = (0, validation_1.evaluateBacktestInvariants)(result.daily, result.benchmark ?? null);
|
|
38
|
+
if (config.enableInvariantChecks ?? true) {
|
|
39
|
+
(0, validation_1.assertBacktestInvariants)(result.daily);
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
function runMonteCarloBacktest(sourceDays, engineConfig, mcConfig) {
|
|
44
|
+
(0, validation_1.validateConfig)(engineConfig);
|
|
45
|
+
(0, validation_1.validateDayInputs)(sourceDays, engineConfig);
|
|
46
|
+
(0, validation_1.validateMonteCarloConfig)(mcConfig);
|
|
47
|
+
const seedBase = mcConfig.seed ?? 123456789;
|
|
48
|
+
const blockLength = resolveBlockLength(mcConfig.blockLength, sourceDays.length);
|
|
49
|
+
const pathResults = [];
|
|
50
|
+
for (let i = 0; i < mcConfig.paths; i += 1) {
|
|
51
|
+
const seed = (seedBase + i) >>> 0;
|
|
52
|
+
const rng = mulberry32(seed);
|
|
53
|
+
const pathDays = buildSyntheticPath(sourceDays, mcConfig.horizonDays, blockLength, rng);
|
|
54
|
+
const result = runFixedBacktest(pathDays, engineConfig);
|
|
55
|
+
pathResults.push({ index: i, seed, result });
|
|
56
|
+
}
|
|
57
|
+
const monteCarloResult = {
|
|
58
|
+
config: {
|
|
59
|
+
...mcConfig,
|
|
60
|
+
blockLength,
|
|
61
|
+
},
|
|
62
|
+
paths: pathResults,
|
|
63
|
+
summary: buildMonteCarloSummary(pathResults.map((p) => p.result)),
|
|
64
|
+
};
|
|
65
|
+
const sameSeedCheck = rerunMonteCarloForInvariant(sourceDays, engineConfig, {
|
|
66
|
+
...mcConfig,
|
|
67
|
+
blockLength,
|
|
68
|
+
});
|
|
69
|
+
const differentSeedCheck = rerunMonteCarloForInvariant(sourceDays, engineConfig, {
|
|
70
|
+
...mcConfig,
|
|
71
|
+
seed: (seedBase + 1) >>> 0,
|
|
72
|
+
blockLength,
|
|
73
|
+
});
|
|
74
|
+
monteCarloResult.invariants = (0, validation_1.evaluateMonteCarloInvariants)(monteCarloResult, sameSeedCheck, differentSeedCheck);
|
|
75
|
+
return monteCarloResult;
|
|
76
|
+
}
|
|
77
|
+
function rerunMonteCarloForInvariant(sourceDays, engineConfig, mcConfig) {
|
|
78
|
+
const seedBase = mcConfig.seed ?? 123456789;
|
|
79
|
+
const blockLength = resolveBlockLength(mcConfig.blockLength, sourceDays.length);
|
|
80
|
+
const paths = [];
|
|
81
|
+
for (let i = 0; i < mcConfig.paths; i += 1) {
|
|
82
|
+
const seed = (seedBase + i) >>> 0;
|
|
83
|
+
const rng = mulberry32(seed);
|
|
84
|
+
const pathDays = buildSyntheticPath(sourceDays, mcConfig.horizonDays, blockLength, rng);
|
|
85
|
+
paths.push({ index: i, seed, result: runFixedBacktest(pathDays, engineConfig) });
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
config: mcConfig,
|
|
89
|
+
paths,
|
|
90
|
+
summary: buildMonteCarloSummary(paths.map((p) => p.result)),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function buildBenchmarkResult(days, result) {
|
|
94
|
+
const strategyReturns = result.daily.map((state) => ({
|
|
95
|
+
date: state.date,
|
|
96
|
+
value: state.capitalBaseBeforeReturn === 0 ? 0 : state.equityEnd / state.capitalBaseBeforeReturn - 1,
|
|
97
|
+
}));
|
|
98
|
+
const benchmarkReturns = days
|
|
99
|
+
.filter((d) => d.benchmarkReturn !== undefined)
|
|
100
|
+
.map((d) => ({ date: d.date, value: d.benchmarkReturn ?? 0 }));
|
|
101
|
+
return (0, benchmark_1.buildBenchmarkComparison)(strategyReturns, benchmarkReturns, result.daily[0]?.equityStart ?? 0);
|
|
102
|
+
}
|
|
103
|
+
function buildSyntheticPath(sourceDays, horizonDays, blockLength, rng) {
|
|
104
|
+
const startDate = new Date(`${sourceDays[0].date}T00:00:00Z`);
|
|
105
|
+
const generated = [];
|
|
106
|
+
const contiguousTuples = sourceDays.map((day) => ({
|
|
107
|
+
assetReturn: day.assetReturn,
|
|
108
|
+
signalInput: day.signalInput,
|
|
109
|
+
borrowRateDaily: day.borrowRateDaily,
|
|
110
|
+
benchmarkReturn: day.benchmarkReturn,
|
|
111
|
+
contribution: day.contribution,
|
|
112
|
+
expenseRatioAnnual: day.expenseRatioAnnual,
|
|
113
|
+
tradeSpread: day.tradeSpread,
|
|
114
|
+
}));
|
|
115
|
+
let generatedDays = 0;
|
|
116
|
+
const maxStartIndex = contiguousTuples.length - blockLength;
|
|
117
|
+
while (generatedDays < horizonDays) {
|
|
118
|
+
// Non-wrapping block bootstrap: sample only starts where a full block fits in source history.
|
|
119
|
+
const startIndex = Math.floor(rng() * (maxStartIndex + 1));
|
|
120
|
+
const currentBlockLength = Math.min(blockLength, horizonDays - generatedDays);
|
|
121
|
+
for (let j = 0; j < currentBlockLength; j += 1) {
|
|
122
|
+
const sampled = contiguousTuples[startIndex + j];
|
|
123
|
+
const date = new Date(startDate.getTime());
|
|
124
|
+
date.setUTCDate(startDate.getUTCDate() + generatedDays);
|
|
125
|
+
generated.push({
|
|
126
|
+
...sampled,
|
|
127
|
+
date: date.toISOString().slice(0, 10),
|
|
128
|
+
});
|
|
129
|
+
generatedDays += 1;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return generated;
|
|
133
|
+
}
|
|
134
|
+
function resolveBlockLength(configBlockLength, sourceLength) {
|
|
135
|
+
const blockLength = configBlockLength ?? DEFAULT_BLOCK_LENGTH;
|
|
136
|
+
if (blockLength > sourceLength) {
|
|
137
|
+
throw new Error('blockLength must not exceed sourceDays length');
|
|
138
|
+
}
|
|
139
|
+
return blockLength;
|
|
140
|
+
}
|
|
141
|
+
function buildMonteCarloSummary(results) {
|
|
142
|
+
const endingEquitySeries = results.map((r) => r.finalEquity);
|
|
143
|
+
const twrSeries = results.map((r) => r.twrCagr ?? 0);
|
|
144
|
+
const maxDrawdownSeries = results.map((r) => r.maxDrawdown);
|
|
145
|
+
const endingEquity = summarizePercentiles(endingEquitySeries);
|
|
146
|
+
const twrCagr = summarizePercentiles(twrSeries);
|
|
147
|
+
const maxDrawdown = summarizePercentiles(maxDrawdownSeries);
|
|
148
|
+
return {
|
|
149
|
+
endingEquity,
|
|
150
|
+
twrCagr,
|
|
151
|
+
maxDrawdown,
|
|
152
|
+
medianEndingEquity: endingEquity.p50,
|
|
153
|
+
medianTwrCagr: twrCagr.p50,
|
|
154
|
+
medianMaxDrawdown: maxDrawdown.p50,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function summarizePercentiles(values) {
|
|
158
|
+
return {
|
|
159
|
+
p5: (0, metrics_1.percentile)(values, 0.05),
|
|
160
|
+
p50: (0, metrics_1.percentile)(values, 0.5),
|
|
161
|
+
p95: (0, metrics_1.percentile)(values, 0.95),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function mulberry32(seed) {
|
|
165
|
+
let s = seed >>> 0;
|
|
166
|
+
return function next() {
|
|
167
|
+
s = (s + 0x6d2b79f5) >>> 0;
|
|
168
|
+
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
|
169
|
+
t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
|
|
170
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BenchmarkComparison, ReturnPoint } from './types';
|
|
2
|
+
export interface AlignedSeries {
|
|
3
|
+
dates: string[];
|
|
4
|
+
strategyReturns: number[];
|
|
5
|
+
benchmarkReturns: number[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Matched-date alignment only. Non-overlapping dates are excluded by design.
|
|
9
|
+
*/
|
|
10
|
+
export declare function alignReturnsByMatchedDates(strategy: ReturnPoint[], benchmark: ReturnPoint[]): AlignedSeries;
|
|
11
|
+
export declare function buildBenchmarkComparison(strategy: ReturnPoint[], benchmark: ReturnPoint[], initialEquity: number): BenchmarkComparison | null;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.alignReturnsByMatchedDates = alignReturnsByMatchedDates;
|
|
4
|
+
exports.buildBenchmarkComparison = buildBenchmarkComparison;
|
|
5
|
+
const metrics_1 = require("./metrics");
|
|
6
|
+
/**
|
|
7
|
+
* Matched-date alignment only. Non-overlapping dates are excluded by design.
|
|
8
|
+
*/
|
|
9
|
+
function alignReturnsByMatchedDates(strategy, benchmark) {
|
|
10
|
+
const strategyMap = new Map(strategy.map((r) => [r.date, r.value]));
|
|
11
|
+
const benchmarkMap = new Map(benchmark.map((r) => [r.date, r.value]));
|
|
12
|
+
const dates = [...strategyMap.keys()]
|
|
13
|
+
.filter((d) => benchmarkMap.has(d))
|
|
14
|
+
.sort((a, b) => a.localeCompare(b));
|
|
15
|
+
return {
|
|
16
|
+
dates,
|
|
17
|
+
strategyReturns: dates.map((d) => strategyMap.get(d) ?? 0),
|
|
18
|
+
benchmarkReturns: dates.map((d) => benchmarkMap.get(d) ?? 0),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function buildBenchmarkComparison(strategy, benchmark, initialEquity) {
|
|
22
|
+
const aligned = alignReturnsByMatchedDates(strategy, benchmark);
|
|
23
|
+
if (aligned.dates.length === 0) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const strategyEquity = [];
|
|
27
|
+
const benchmarkEquity = [];
|
|
28
|
+
let strategyLevel = initialEquity;
|
|
29
|
+
let benchmarkLevel = initialEquity;
|
|
30
|
+
for (let i = 0; i < aligned.dates.length; i += 1) {
|
|
31
|
+
strategyLevel *= 1 + aligned.strategyReturns[i];
|
|
32
|
+
benchmarkLevel *= 1 + aligned.benchmarkReturns[i];
|
|
33
|
+
strategyEquity.push(strategyLevel);
|
|
34
|
+
benchmarkEquity.push(benchmarkLevel);
|
|
35
|
+
}
|
|
36
|
+
const benchmarkTotalReturn = (0, metrics_1.computeTotalReturn)(benchmarkEquity, initialEquity) ?? 0;
|
|
37
|
+
const strategyTotalReturn = (0, metrics_1.computeTotalReturn)(strategyEquity, initialEquity);
|
|
38
|
+
const benchmarkTwrCagr = computeCagrFromReturns(aligned.dates, aligned.benchmarkReturns);
|
|
39
|
+
const strategyTwrCagr = computeCagrFromReturns(aligned.dates, aligned.strategyReturns);
|
|
40
|
+
const benchmarkVolatility = (0, metrics_1.computeVolatilityFromReturns)(aligned.benchmarkReturns);
|
|
41
|
+
const benchmarkMaxDrawdown = (0, metrics_1.computeMaxDrawdownFromEquity)(benchmarkEquity);
|
|
42
|
+
const excessTotalReturn = strategyTotalReturn === null ? null : strategyTotalReturn - benchmarkTotalReturn;
|
|
43
|
+
const excessTwrCagr = strategyTwrCagr === null || benchmarkTwrCagr === null ? null : strategyTwrCagr - benchmarkTwrCagr;
|
|
44
|
+
const trackingError = computeTrackingError(aligned.strategyReturns, aligned.benchmarkReturns);
|
|
45
|
+
return {
|
|
46
|
+
dates: aligned.dates,
|
|
47
|
+
strategyReturns: aligned.strategyReturns,
|
|
48
|
+
benchmarkReturns: aligned.benchmarkReturns,
|
|
49
|
+
strategyEquity,
|
|
50
|
+
benchmarkEquity,
|
|
51
|
+
benchmarkTotalReturn,
|
|
52
|
+
benchmarkTwrCagr,
|
|
53
|
+
benchmarkVolatility,
|
|
54
|
+
benchmarkMaxDrawdown,
|
|
55
|
+
excessTotalReturn,
|
|
56
|
+
excessTwrCagr,
|
|
57
|
+
trackingError,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function computeTrackingError(strategyReturns, benchmarkReturns) {
|
|
61
|
+
if (strategyReturns.length !== benchmarkReturns.length || strategyReturns.length < 2) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const activeReturns = strategyReturns.map((value, idx) => value - benchmarkReturns[idx]);
|
|
65
|
+
return (0, metrics_1.computeVolatilityFromReturns)(activeReturns);
|
|
66
|
+
}
|
|
67
|
+
function computeCagrFromReturns(dates, returns) {
|
|
68
|
+
if (dates.length < 2 || returns.length !== dates.length) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
let multiplier = 1;
|
|
72
|
+
for (const r of returns) {
|
|
73
|
+
if (!Number.isFinite(r) || r <= -1) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
multiplier *= 1 + r;
|
|
77
|
+
}
|
|
78
|
+
const start = new Date(`${dates[0]}T00:00:00Z`).getTime();
|
|
79
|
+
const end = new Date(`${dates[dates.length - 1]}T00:00:00Z`).getTime();
|
|
80
|
+
const daySpan = (end - start) / (1000 * 60 * 60 * 24);
|
|
81
|
+
if (daySpan <= 0) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return Math.pow(multiplier, 365.25 / daySpan) - 1;
|
|
85
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { DayInput } from './types';
|
|
2
|
+
/** Contribution timing convention: contribution is credited before day-t return. */
|
|
3
|
+
export declare function getContributionForDay(day: DayInput): number;
|
|
4
|
+
export declare function applyContribution(equity: number, contribution: number): number;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getContributionForDay = getContributionForDay;
|
|
4
|
+
exports.applyContribution = applyContribution;
|
|
5
|
+
/** Contribution timing convention: contribution is credited before day-t return. */
|
|
6
|
+
function getContributionForDay(day) {
|
|
7
|
+
return day.contribution ?? 0;
|
|
8
|
+
}
|
|
9
|
+
function applyContribution(equity, contribution) {
|
|
10
|
+
return equity + contribution;
|
|
11
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function calculateTurnoverNotional(appliedPosition: number, priorAppliedPosition: number, equityBase: number): number;
|
|
2
|
+
/** Trade cost convention: charged at start of day on turnover notional. */
|
|
3
|
+
export declare function calculateTradeCost(turnoverNotional: number, tradeCostRate: number, tradeSpread?: number): number;
|
|
4
|
+
/** Borrow drag applies only to exposure above 1x notional. */
|
|
5
|
+
export declare function calculateBorrowDrag(appliedPosition: number, equityBase: number, borrowRateDaily: number): number;
|
|
6
|
+
/** Expense-ratio drag is applied separately from borrow drag. */
|
|
7
|
+
export declare function calculateExpenseDrag(equityBase: number, expenseRatioDaily: number): number;
|
|
8
|
+
export declare function annualToDailyRate(annualRate: number): number;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.calculateTurnoverNotional = calculateTurnoverNotional;
|
|
4
|
+
exports.calculateTradeCost = calculateTradeCost;
|
|
5
|
+
exports.calculateBorrowDrag = calculateBorrowDrag;
|
|
6
|
+
exports.calculateExpenseDrag = calculateExpenseDrag;
|
|
7
|
+
exports.annualToDailyRate = annualToDailyRate;
|
|
8
|
+
function calculateTurnoverNotional(appliedPosition, priorAppliedPosition, equityBase) {
|
|
9
|
+
return Math.abs(appliedPosition - priorAppliedPosition) * equityBase;
|
|
10
|
+
}
|
|
11
|
+
/** Trade cost convention: charged at start of day on turnover notional. */
|
|
12
|
+
function calculateTradeCost(turnoverNotional, tradeCostRate, tradeSpread = 0) {
|
|
13
|
+
return turnoverNotional * (tradeCostRate + tradeSpread);
|
|
14
|
+
}
|
|
15
|
+
/** Borrow drag applies only to exposure above 1x notional. */
|
|
16
|
+
function calculateBorrowDrag(appliedPosition, equityBase, borrowRateDaily) {
|
|
17
|
+
const borrowedFraction = Math.max(0, Math.abs(appliedPosition) - 1);
|
|
18
|
+
return borrowedFraction * equityBase * borrowRateDaily;
|
|
19
|
+
}
|
|
20
|
+
/** Expense-ratio drag is applied separately from borrow drag. */
|
|
21
|
+
function calculateExpenseDrag(equityBase, expenseRatioDaily) {
|
|
22
|
+
return equityBase * expenseRatioDaily;
|
|
23
|
+
}
|
|
24
|
+
function annualToDailyRate(annualRate) {
|
|
25
|
+
if (annualRate <= -1) {
|
|
26
|
+
throw new Error('annualRate must be greater than -1');
|
|
27
|
+
}
|
|
28
|
+
return Math.pow(1 + annualRate, 1 / 365.25) - 1;
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry points for UI and application integrations.
|
|
3
|
+
* Prefer importing from this module (especially `public-api`) over deep internal files.
|
|
4
|
+
*/
|
|
5
|
+
export * from './public-api';
|
|
6
|
+
export * from './types';
|
|
7
|
+
export * from './signal';
|
|
8
|
+
export * from './costs';
|
|
9
|
+
export * from './contributions';
|
|
10
|
+
export * from './benchmark';
|
|
11
|
+
export * from './metrics';
|
|
12
|
+
export * from './step';
|
|
13
|
+
export * from './backtest';
|
|
14
|
+
export * from './validation';
|
|
15
|
+
export * from './analysis';
|
|
16
|
+
export * from './reporting';
|
|
17
|
+
export * from './validation-cases';
|
|
18
|
+
export * from './validation-harness';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
/**
|
|
18
|
+
* Public entry points for UI and application integrations.
|
|
19
|
+
* Prefer importing from this module (especially `public-api`) over deep internal files.
|
|
20
|
+
*/
|
|
21
|
+
__exportStar(require("./public-api"), exports);
|
|
22
|
+
__exportStar(require("./types"), exports);
|
|
23
|
+
__exportStar(require("./signal"), exports);
|
|
24
|
+
__exportStar(require("./costs"), exports);
|
|
25
|
+
__exportStar(require("./contributions"), exports);
|
|
26
|
+
__exportStar(require("./benchmark"), exports);
|
|
27
|
+
__exportStar(require("./metrics"), exports);
|
|
28
|
+
__exportStar(require("./step"), exports);
|
|
29
|
+
__exportStar(require("./backtest"), exports);
|
|
30
|
+
__exportStar(require("./validation"), exports);
|
|
31
|
+
__exportStar(require("./analysis"), exports);
|
|
32
|
+
__exportStar(require("./reporting"), exports);
|
|
33
|
+
__exportStar(require("./validation-cases"), exports);
|
|
34
|
+
__exportStar(require("./validation-harness"), exports);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CashFlowPoint, DailyState } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* True flow-neutral TWR CAGR.
|
|
4
|
+
* Daily return neutralizes external cash flows by using capital base after contribution.
|
|
5
|
+
*/
|
|
6
|
+
export declare function computeTwrCagr(dailyStates: DailyState[]): number | null;
|
|
7
|
+
export declare function computeMaxDrawdownFromEquity(equityCurve: number[]): number;
|
|
8
|
+
export declare function computeTotalReturn(equityCurve: number[], initialEquity: number): number | null;
|
|
9
|
+
export declare function computeVolatilityFromReturns(returns: number[]): number | null;
|
|
10
|
+
export declare function percentile(values: number[], p: number): number;
|
|
11
|
+
/**
|
|
12
|
+
* XIRR with Newton + bracketed bisection fallback.
|
|
13
|
+
* Returns null when a valid root cannot be found safely.
|
|
14
|
+
*/
|
|
15
|
+
export declare function computeXirr(cashFlows: CashFlowPoint[], guess?: number, maxIterations?: number, tolerance?: number): number | null;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeTwrCagr = computeTwrCagr;
|
|
4
|
+
exports.computeMaxDrawdownFromEquity = computeMaxDrawdownFromEquity;
|
|
5
|
+
exports.computeTotalReturn = computeTotalReturn;
|
|
6
|
+
exports.computeVolatilityFromReturns = computeVolatilityFromReturns;
|
|
7
|
+
exports.percentile = percentile;
|
|
8
|
+
exports.computeXirr = computeXirr;
|
|
9
|
+
const DAYS_IN_YEAR = 365.25;
|
|
10
|
+
/**
|
|
11
|
+
* True flow-neutral TWR CAGR.
|
|
12
|
+
* Daily return neutralizes external cash flows by using capital base after contribution.
|
|
13
|
+
*/
|
|
14
|
+
function computeTwrCagr(dailyStates) {
|
|
15
|
+
if (dailyStates.length < 2) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
let twrMultiplier = 1;
|
|
19
|
+
for (const state of dailyStates) {
|
|
20
|
+
const base = state.capitalBaseBeforeReturn;
|
|
21
|
+
if (!Number.isFinite(base) || base <= 0) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const dailyReturn = state.equityEnd / base - 1;
|
|
25
|
+
if (!Number.isFinite(dailyReturn) || dailyReturn <= -1) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
twrMultiplier *= 1 + dailyReturn;
|
|
29
|
+
}
|
|
30
|
+
if (twrMultiplier <= 0) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const days = dayDiff(dailyStates[0].date, dailyStates[dailyStates.length - 1].date);
|
|
34
|
+
if (days <= 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return Math.pow(twrMultiplier, DAYS_IN_YEAR / days) - 1;
|
|
38
|
+
}
|
|
39
|
+
function computeMaxDrawdownFromEquity(equityCurve) {
|
|
40
|
+
if (equityCurve.length === 0) {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
let peak = equityCurve[0];
|
|
44
|
+
let maxDrawdown = 0;
|
|
45
|
+
for (const level of equityCurve) {
|
|
46
|
+
if (level > peak) {
|
|
47
|
+
peak = level;
|
|
48
|
+
}
|
|
49
|
+
if (peak > 0) {
|
|
50
|
+
const drawdown = (peak - level) / peak;
|
|
51
|
+
if (drawdown > maxDrawdown) {
|
|
52
|
+
maxDrawdown = drawdown;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return maxDrawdown;
|
|
57
|
+
}
|
|
58
|
+
function computeTotalReturn(equityCurve, initialEquity) {
|
|
59
|
+
if (equityCurve.length === 0 || initialEquity <= 0) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const end = equityCurve[equityCurve.length - 1];
|
|
63
|
+
if (!Number.isFinite(end)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return end / initialEquity - 1;
|
|
67
|
+
}
|
|
68
|
+
function computeVolatilityFromReturns(returns) {
|
|
69
|
+
if (returns.length < 2) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const mean = returns.reduce((acc, v) => acc + v, 0) / returns.length;
|
|
73
|
+
const variance = returns.reduce((acc, v) => acc + (v - mean) ** 2, 0) / (returns.length - 1);
|
|
74
|
+
if (!Number.isFinite(variance)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return Math.sqrt(variance) * Math.sqrt(365.25);
|
|
78
|
+
}
|
|
79
|
+
function percentile(values, p) {
|
|
80
|
+
if (values.length === 0) {
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
84
|
+
const clampedP = Math.max(0, Math.min(1, p));
|
|
85
|
+
const index = clampedP * (sorted.length - 1);
|
|
86
|
+
const lo = Math.floor(index);
|
|
87
|
+
const hi = Math.ceil(index);
|
|
88
|
+
if (lo === hi) {
|
|
89
|
+
return sorted[lo];
|
|
90
|
+
}
|
|
91
|
+
const weight = index - lo;
|
|
92
|
+
return sorted[lo] * (1 - weight) + sorted[hi] * weight;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* XIRR with Newton + bracketed bisection fallback.
|
|
96
|
+
* Returns null when a valid root cannot be found safely.
|
|
97
|
+
*/
|
|
98
|
+
function computeXirr(cashFlows, guess = 0.1, maxIterations = 100, tolerance = 1e-8) {
|
|
99
|
+
if (cashFlows.length < 2) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const ordered = [...cashFlows].sort((a, b) => a.date.localeCompare(b.date));
|
|
103
|
+
const hasPositive = ordered.some((f) => f.amount > 0);
|
|
104
|
+
const hasNegative = ordered.some((f) => f.amount < 0);
|
|
105
|
+
if (!hasPositive || !hasNegative) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const newton = tryNewton(ordered, guess, maxIterations, tolerance);
|
|
109
|
+
if (newton !== null) {
|
|
110
|
+
return newton;
|
|
111
|
+
}
|
|
112
|
+
const bracket = findBracket(ordered);
|
|
113
|
+
if (bracket === null) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return bisection(ordered, bracket.low, bracket.high, maxIterations * 2, tolerance);
|
|
117
|
+
}
|
|
118
|
+
function tryNewton(flows, guess, maxIterations, tolerance) {
|
|
119
|
+
let rate = Math.max(-0.95, guess);
|
|
120
|
+
for (let i = 0; i < maxIterations; i += 1) {
|
|
121
|
+
const val = npvAndDerivative(flows, rate);
|
|
122
|
+
if (val === null) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const { npv, dNpv } = val;
|
|
126
|
+
if (Math.abs(npv) < tolerance) {
|
|
127
|
+
return rate;
|
|
128
|
+
}
|
|
129
|
+
if (!Number.isFinite(dNpv) || Math.abs(dNpv) < 1e-12) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const next = rate - npv / dNpv;
|
|
133
|
+
if (!Number.isFinite(next) || next <= -0.999999) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (Math.abs(next - rate) < tolerance) {
|
|
137
|
+
return next;
|
|
138
|
+
}
|
|
139
|
+
rate = next;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
function findBracket(flows) {
|
|
144
|
+
let low = -0.95;
|
|
145
|
+
let fLow = npv(flows, low);
|
|
146
|
+
if (fLow === null) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const candidates = [0, 0.1, 0.2, 0.5, 1, 2, 5, 10, 25, 50, 100];
|
|
150
|
+
for (const high of candidates) {
|
|
151
|
+
const fHigh = npv(flows, high);
|
|
152
|
+
if (fHigh === null) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (fLow === 0) {
|
|
156
|
+
return { low, high: low };
|
|
157
|
+
}
|
|
158
|
+
if (fLow * fHigh < 0) {
|
|
159
|
+
return { low, high };
|
|
160
|
+
}
|
|
161
|
+
low = high;
|
|
162
|
+
fLow = fHigh;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
function bisection(flows, low, high, maxIterations, tolerance) {
|
|
167
|
+
let fLow = npv(flows, low);
|
|
168
|
+
let fHigh = npv(flows, high);
|
|
169
|
+
if (fLow === null || fHigh === null || fLow * fHigh > 0) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
if (Math.abs(fLow) < tolerance) {
|
|
173
|
+
return low;
|
|
174
|
+
}
|
|
175
|
+
if (Math.abs(fHigh) < tolerance) {
|
|
176
|
+
return high;
|
|
177
|
+
}
|
|
178
|
+
for (let i = 0; i < maxIterations; i += 1) {
|
|
179
|
+
const mid = (low + high) / 2;
|
|
180
|
+
const fMid = npv(flows, mid);
|
|
181
|
+
if (fMid === null) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
if (Math.abs(fMid) < tolerance || Math.abs(high - low) < tolerance) {
|
|
185
|
+
return mid;
|
|
186
|
+
}
|
|
187
|
+
if (fLow * fMid < 0) {
|
|
188
|
+
high = mid;
|
|
189
|
+
fHigh = fMid;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
low = mid;
|
|
193
|
+
fLow = fMid;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return (low + high) / 2;
|
|
197
|
+
}
|
|
198
|
+
function npvAndDerivative(cashFlows, rate) {
|
|
199
|
+
if (rate <= -1) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
const t0 = toUnixDays(cashFlows[0].date);
|
|
203
|
+
let sum = 0;
|
|
204
|
+
let derivative = 0;
|
|
205
|
+
for (const flow of cashFlows) {
|
|
206
|
+
const t = (toUnixDays(flow.date) - t0) / DAYS_IN_YEAR;
|
|
207
|
+
const base = 1 + rate;
|
|
208
|
+
if (base <= 0) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const discount = Math.pow(base, t);
|
|
212
|
+
if (!Number.isFinite(discount) || discount === 0) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
sum += flow.amount / discount;
|
|
216
|
+
derivative += (-t * flow.amount) / (discount * base);
|
|
217
|
+
}
|
|
218
|
+
if (!Number.isFinite(sum) || !Number.isFinite(derivative)) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
return { npv: sum, dNpv: derivative };
|
|
222
|
+
}
|
|
223
|
+
function npv(cashFlows, rate) {
|
|
224
|
+
const val = npvAndDerivative(cashFlows, rate);
|
|
225
|
+
return val?.npv ?? null;
|
|
226
|
+
}
|
|
227
|
+
function dayDiff(start, end) {
|
|
228
|
+
return Math.max(0, toUnixDays(end) - toUnixDays(start));
|
|
229
|
+
}
|
|
230
|
+
function toUnixDays(date) {
|
|
231
|
+
return new Date(`${date}T00:00:00Z`).getTime() / (1000 * 60 * 60 * 24);
|
|
232
|
+
}
|