@openfinclaw/fin-strategy-engine 0.0.1
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/LICENSE +21 -0
- package/index.test.ts +269 -0
- package/index.ts +578 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +40 -0
- package/src/backtest-engine.live.test.ts +313 -0
- package/src/backtest-engine.test.ts +368 -0
- package/src/backtest-engine.ts +362 -0
- package/src/builtin-strategies/bollinger-bands.test.ts +96 -0
- package/src/builtin-strategies/bollinger-bands.ts +75 -0
- package/src/builtin-strategies/custom-rule-engine.ts +274 -0
- package/src/builtin-strategies/macd-divergence.test.ts +122 -0
- package/src/builtin-strategies/macd-divergence.ts +77 -0
- package/src/builtin-strategies/multi-timeframe-confluence.test.ts +287 -0
- package/src/builtin-strategies/multi-timeframe-confluence.ts +253 -0
- package/src/builtin-strategies/regime-adaptive.test.ts +210 -0
- package/src/builtin-strategies/regime-adaptive.ts +285 -0
- package/src/builtin-strategies/risk-parity-triple-screen.test.ts +295 -0
- package/src/builtin-strategies/risk-parity-triple-screen.ts +295 -0
- package/src/builtin-strategies/rsi-mean-reversion.test.ts +143 -0
- package/src/builtin-strategies/rsi-mean-reversion.ts +74 -0
- package/src/builtin-strategies/sma-crossover.test.ts +113 -0
- package/src/builtin-strategies/sma-crossover.ts +85 -0
- package/src/builtin-strategies/trend-following-momentum.test.ts +228 -0
- package/src/builtin-strategies/trend-following-momentum.ts +209 -0
- package/src/builtin-strategies/volatility-mean-reversion.test.ts +193 -0
- package/src/builtin-strategies/volatility-mean-reversion.ts +212 -0
- package/src/composite-pipeline.live.test.ts +347 -0
- package/src/e2e-pipeline.test.ts +494 -0
- package/src/fitness.test.ts +103 -0
- package/src/fitness.ts +61 -0
- package/src/full-pipeline.live.test.ts +339 -0
- package/src/indicators.test.ts +224 -0
- package/src/indicators.ts +238 -0
- package/src/stats.test.ts +215 -0
- package/src/stats.ts +115 -0
- package/src/strategy-registry.test.ts +235 -0
- package/src/strategy-registry.ts +183 -0
- package/src/types.ts +19 -0
- package/src/walk-forward.test.ts +185 -0
- package/src/walk-forward.ts +114 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { OHLCV } from "../../fin-shared-types/src/types.js";
|
|
2
|
+
import type { BacktestEngine } from "./backtest-engine.js";
|
|
3
|
+
import type { BacktestConfig, StrategyDefinition, WalkForwardResult } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export interface WalkForwardOptions {
|
|
6
|
+
windows?: number; // number of train/test windows (default 5)
|
|
7
|
+
inSamplePct?: number; // fraction of each window for training (default 0.7)
|
|
8
|
+
threshold?: number; // pass if combinedTest / avgTrain >= threshold (default 0.6)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Walk-Forward validation.
|
|
13
|
+
* Splits data into N windows, runs backtest on train and test segments,
|
|
14
|
+
* and checks if out-of-sample performance is at least threshold * in-sample.
|
|
15
|
+
*/
|
|
16
|
+
export class WalkForward {
|
|
17
|
+
constructor(private engine: BacktestEngine) {}
|
|
18
|
+
|
|
19
|
+
async validate(
|
|
20
|
+
strategy: StrategyDefinition,
|
|
21
|
+
data: OHLCV[],
|
|
22
|
+
config: BacktestConfig,
|
|
23
|
+
options?: WalkForwardOptions,
|
|
24
|
+
): Promise<WalkForwardResult> {
|
|
25
|
+
const numWindows = options?.windows ?? 5;
|
|
26
|
+
const inSamplePct = options?.inSamplePct ?? 0.7;
|
|
27
|
+
const threshold = options?.threshold ?? 0.6;
|
|
28
|
+
|
|
29
|
+
if (data.length < numWindows * 2) {
|
|
30
|
+
return {
|
|
31
|
+
passed: false,
|
|
32
|
+
windows: [],
|
|
33
|
+
combinedTestSharpe: 0,
|
|
34
|
+
avgTrainSharpe: 0,
|
|
35
|
+
ratio: 0,
|
|
36
|
+
threshold,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Calculate window size: each window is data.length / numWindows bars
|
|
41
|
+
const windowSize = Math.floor(data.length / numWindows);
|
|
42
|
+
const trainSize = Math.floor(windowSize * inSamplePct);
|
|
43
|
+
const testSize = windowSize - trainSize;
|
|
44
|
+
|
|
45
|
+
const windows: WalkForwardResult["windows"] = [];
|
|
46
|
+
let totalTrainSharpe = 0;
|
|
47
|
+
let totalTestSharpe = 0;
|
|
48
|
+
|
|
49
|
+
for (let w = 0; w < numWindows; w++) {
|
|
50
|
+
const windowStart = w * windowSize;
|
|
51
|
+
const trainStart = windowStart;
|
|
52
|
+
const trainEnd = windowStart + trainSize;
|
|
53
|
+
const testStart = trainEnd;
|
|
54
|
+
const testEnd = Math.min(windowStart + windowSize, data.length);
|
|
55
|
+
|
|
56
|
+
const trainData = data.slice(trainStart, trainEnd);
|
|
57
|
+
const testData = data.slice(testStart, testEnd);
|
|
58
|
+
|
|
59
|
+
if (trainData.length < 2 || testData.length < 2) continue;
|
|
60
|
+
|
|
61
|
+
const trainResult = await this.engine.run(strategy, trainData, config);
|
|
62
|
+
const testResult = await this.engine.run(strategy, testData, config);
|
|
63
|
+
|
|
64
|
+
const trainSharpe = trainResult.sharpe;
|
|
65
|
+
const testSharpe = testResult.sharpe;
|
|
66
|
+
|
|
67
|
+
totalTrainSharpe += trainSharpe;
|
|
68
|
+
totalTestSharpe += testSharpe;
|
|
69
|
+
|
|
70
|
+
windows.push({
|
|
71
|
+
trainStart: trainData[0]!.timestamp,
|
|
72
|
+
trainEnd: trainData[trainData.length - 1]!.timestamp,
|
|
73
|
+
testStart: testData[0]!.timestamp,
|
|
74
|
+
testEnd: testData[testData.length - 1]!.timestamp,
|
|
75
|
+
trainSharpe,
|
|
76
|
+
testSharpe,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (windows.length === 0) {
|
|
81
|
+
return {
|
|
82
|
+
passed: false,
|
|
83
|
+
windows: [],
|
|
84
|
+
combinedTestSharpe: 0,
|
|
85
|
+
avgTrainSharpe: 0,
|
|
86
|
+
ratio: 0,
|
|
87
|
+
threshold,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const combinedTestSharpe = totalTestSharpe / windows.length;
|
|
92
|
+
const avgTrainSharpe = totalTrainSharpe / windows.length;
|
|
93
|
+
|
|
94
|
+
// Handle edge cases for ratio
|
|
95
|
+
let ratio: number;
|
|
96
|
+
if (avgTrainSharpe === 0) {
|
|
97
|
+
ratio = combinedTestSharpe >= 0 ? 1 : 0;
|
|
98
|
+
} else {
|
|
99
|
+
ratio = combinedTestSharpe / avgTrainSharpe;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const safeCombined = Number.isFinite(combinedTestSharpe) ? combinedTestSharpe : 0;
|
|
103
|
+
const safeRatio = Number.isFinite(ratio) ? ratio : 0;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
passed: safeRatio >= threshold,
|
|
107
|
+
windows,
|
|
108
|
+
combinedTestSharpe: safeCombined,
|
|
109
|
+
avgTrainSharpe,
|
|
110
|
+
ratio: safeRatio,
|
|
111
|
+
threshold,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|