@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,287 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { OHLCV } from "../../../fin-shared-types/src/types.js";
|
|
3
|
+
import { BacktestEngine } from "../backtest-engine.js";
|
|
4
|
+
import type { BacktestConfig } from "../types.js";
|
|
5
|
+
import { createMultiTimeframeConfluence } from "./multi-timeframe-confluence.js";
|
|
6
|
+
|
|
7
|
+
function makeBar(index: number, close: number, overrides?: Partial<OHLCV>): OHLCV {
|
|
8
|
+
return {
|
|
9
|
+
timestamp: 1000000 + index * 86400000,
|
|
10
|
+
open: close,
|
|
11
|
+
high: close * 1.01,
|
|
12
|
+
low: close * 0.99,
|
|
13
|
+
close,
|
|
14
|
+
volume: 1000,
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const config: BacktestConfig = {
|
|
20
|
+
capital: 10000,
|
|
21
|
+
commissionRate: 0,
|
|
22
|
+
slippageBps: 0,
|
|
23
|
+
market: "crypto",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe("Multi-Timeframe Confluence strategy", () => {
|
|
27
|
+
it("creates strategy with default parameters", () => {
|
|
28
|
+
const strategy = createMultiTimeframeConfluence();
|
|
29
|
+
expect(strategy.id).toBe("multi-timeframe-confluence");
|
|
30
|
+
expect(strategy.name).toBe("Multi-Timeframe Confluence");
|
|
31
|
+
expect(strategy.parameters.longSma).toBe(200);
|
|
32
|
+
expect(strategy.parameters.mediumEma).toBe(50);
|
|
33
|
+
expect(strategy.parameters.shortEma).toBe(20);
|
|
34
|
+
expect(strategy.parameters.rsiPeriod).toBe(7);
|
|
35
|
+
expect(strategy.parameters.rsiOversold).toBe(35);
|
|
36
|
+
expect(strategy.parameters.rsiOverbought).toBe(65);
|
|
37
|
+
expect(strategy.parameters.bbPeriod).toBe(10);
|
|
38
|
+
expect(strategy.parameters.bbStdDev).toBe(2.0);
|
|
39
|
+
expect(strategy.parameters.atrPeriod).toBe(14);
|
|
40
|
+
expect(strategy.parameters.atrStopMultiplier).toBe(2.0);
|
|
41
|
+
expect(strategy.parameters.atrProfitMultiplier).toBe(3.0);
|
|
42
|
+
expect(strategy.parameters.maxSizePct).toBe(70);
|
|
43
|
+
expect(strategy.parameters.minConfluenceScore).toBe(3);
|
|
44
|
+
|
|
45
|
+
expect(strategy.parameterRanges).toBeDefined();
|
|
46
|
+
expect(strategy.parameterRanges!.longSma).toEqual({ min: 50, max: 300, step: 50 });
|
|
47
|
+
expect(strategy.parameterRanges!.minConfluenceScore).toEqual({ min: 2, max: 5, step: 1 });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("creates strategy with custom parameters", () => {
|
|
51
|
+
const strategy = createMultiTimeframeConfluence({
|
|
52
|
+
longSma: 100,
|
|
53
|
+
mediumEma: 30,
|
|
54
|
+
shortEma: 10,
|
|
55
|
+
rsiPeriod: 5,
|
|
56
|
+
rsiOversold: 30,
|
|
57
|
+
rsiOverbought: 70,
|
|
58
|
+
bbPeriod: 15,
|
|
59
|
+
bbStdDev: 1.5,
|
|
60
|
+
atrPeriod: 10,
|
|
61
|
+
atrStopMultiplier: 1.5,
|
|
62
|
+
atrProfitMultiplier: 4.0,
|
|
63
|
+
maxSizePct: 50,
|
|
64
|
+
minConfluenceScore: 2,
|
|
65
|
+
symbol: "ETH/USDT",
|
|
66
|
+
});
|
|
67
|
+
expect(strategy.parameters.longSma).toBe(100);
|
|
68
|
+
expect(strategy.parameters.mediumEma).toBe(30);
|
|
69
|
+
expect(strategy.parameters.shortEma).toBe(10);
|
|
70
|
+
expect(strategy.parameters.rsiPeriod).toBe(5);
|
|
71
|
+
expect(strategy.parameters.rsiOversold).toBe(30);
|
|
72
|
+
expect(strategy.parameters.rsiOverbought).toBe(70);
|
|
73
|
+
expect(strategy.parameters.bbPeriod).toBe(15);
|
|
74
|
+
expect(strategy.parameters.bbStdDev).toBe(1.5);
|
|
75
|
+
expect(strategy.parameters.atrPeriod).toBe(10);
|
|
76
|
+
expect(strategy.parameters.atrStopMultiplier).toBe(1.5);
|
|
77
|
+
expect(strategy.parameters.atrProfitMultiplier).toBe(4.0);
|
|
78
|
+
expect(strategy.parameters.maxSizePct).toBe(50);
|
|
79
|
+
expect(strategy.parameters.minConfluenceScore).toBe(2);
|
|
80
|
+
expect(strategy.symbols).toEqual(["ETH/USDT"]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("returns null during warm-up period", async () => {
|
|
84
|
+
// Use small params: longSma:10 needs index >= 9 for valid SMA
|
|
85
|
+
// Give only 5 bars — not enough for SMA(10)
|
|
86
|
+
const data = [
|
|
87
|
+
makeBar(0, 100),
|
|
88
|
+
makeBar(1, 101),
|
|
89
|
+
makeBar(2, 102),
|
|
90
|
+
makeBar(3, 103),
|
|
91
|
+
makeBar(4, 104),
|
|
92
|
+
];
|
|
93
|
+
const strategy = createMultiTimeframeConfluence({
|
|
94
|
+
longSma: 10,
|
|
95
|
+
mediumEma: 5,
|
|
96
|
+
shortEma: 3,
|
|
97
|
+
rsiPeriod: 3,
|
|
98
|
+
bbPeriod: 3,
|
|
99
|
+
bbStdDev: 2,
|
|
100
|
+
atrPeriod: 3,
|
|
101
|
+
minConfluenceScore: 3,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const engine = new BacktestEngine();
|
|
105
|
+
const result = await engine.run(strategy, data, config);
|
|
106
|
+
|
|
107
|
+
expect(result.totalTrades).toBe(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("generates buy on confluence of long-term uptrend and short-term pullback", async () => {
|
|
111
|
+
// Test the onBar logic directly with a mock IndicatorLib to verify
|
|
112
|
+
// the confluence scoring and buy conditions work correctly.
|
|
113
|
+
const strategy = createMultiTimeframeConfluence({
|
|
114
|
+
longSma: 10,
|
|
115
|
+
mediumEma: 5,
|
|
116
|
+
shortEma: 3,
|
|
117
|
+
rsiPeriod: 3,
|
|
118
|
+
rsiOversold: 35,
|
|
119
|
+
rsiOverbought: 65,
|
|
120
|
+
bbPeriod: 3,
|
|
121
|
+
bbStdDev: 2,
|
|
122
|
+
atrPeriod: 3,
|
|
123
|
+
minConfluenceScore: 3,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const bar = makeBar(20, 105);
|
|
127
|
+
// Mock indicators that satisfy all confluence conditions:
|
|
128
|
+
// smaRising: currSma(100) > prevSma(99) AND close(105) > currSma(100) → 1
|
|
129
|
+
// structure: close(105) > ema50(103) AND ema50(103) > sma(100) → 1
|
|
130
|
+
// emaStack: ema20(104) > ema50(103) → 1
|
|
131
|
+
// longScore = 3
|
|
132
|
+
// rsiPullback: RSI(30) < 35 → 1
|
|
133
|
+
// shortScore = 1
|
|
134
|
+
// totalScore = 4 >= 3, RSI turning up: 30 > 25
|
|
135
|
+
const mockIndicators = {
|
|
136
|
+
sma: () => Array(20).fill(NaN).concat([99, 100]),
|
|
137
|
+
ema: (period: number) => {
|
|
138
|
+
if (period === 5) return Array(20).fill(NaN).concat([102, 103]);
|
|
139
|
+
return Array(20).fill(NaN).concat([103, 104]); // shortEma
|
|
140
|
+
},
|
|
141
|
+
rsi: () => Array(20).fill(NaN).concat([25, 30]),
|
|
142
|
+
bollingerBands: () => ({
|
|
143
|
+
upper: Array(21).fill(NaN).concat([110]),
|
|
144
|
+
middle: Array(21).fill(NaN).concat([105]),
|
|
145
|
+
lower: Array(21).fill(NaN).concat([100]),
|
|
146
|
+
}),
|
|
147
|
+
atr: () => Array(20).fill(NaN).concat([1, 2]),
|
|
148
|
+
macd: () => ({ macd: [], signal: [], histogram: [] }),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const memory = new Map<string, unknown>();
|
|
152
|
+
const ctx = {
|
|
153
|
+
portfolio: {
|
|
154
|
+
equity: 10000,
|
|
155
|
+
cash: 10000,
|
|
156
|
+
positions: [] as {
|
|
157
|
+
side: "long";
|
|
158
|
+
symbol: string;
|
|
159
|
+
quantity: number;
|
|
160
|
+
entryPrice: number;
|
|
161
|
+
currentPrice: number;
|
|
162
|
+
unrealizedPnl: number;
|
|
163
|
+
}[],
|
|
164
|
+
},
|
|
165
|
+
history: Array(22).fill(bar),
|
|
166
|
+
indicators: mockIndicators,
|
|
167
|
+
regime: "sideways" as const,
|
|
168
|
+
memory,
|
|
169
|
+
log: () => {},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const signal = await strategy.onBar(bar, ctx);
|
|
173
|
+
expect(signal).not.toBeNull();
|
|
174
|
+
expect(signal!.action).toBe("buy");
|
|
175
|
+
expect(signal!.confidence).toBeGreaterThanOrEqual(0.3);
|
|
176
|
+
expect(signal!.stopLoss).toBeDefined();
|
|
177
|
+
expect(signal!.takeProfit).toBeDefined();
|
|
178
|
+
expect(memory.get("entryConfluence")).toBe(4);
|
|
179
|
+
expect(memory.get("partialExitDone")).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("does not buy when long-term trend is down", async () => {
|
|
183
|
+
const strategy = createMultiTimeframeConfluence({
|
|
184
|
+
longSma: 10,
|
|
185
|
+
mediumEma: 5,
|
|
186
|
+
shortEma: 3,
|
|
187
|
+
rsiPeriod: 3,
|
|
188
|
+
rsiOversold: 35,
|
|
189
|
+
rsiOverbought: 65,
|
|
190
|
+
bbPeriod: 3,
|
|
191
|
+
bbStdDev: 2,
|
|
192
|
+
atrPeriod: 3,
|
|
193
|
+
minConfluenceScore: 3,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const prices: number[] = [];
|
|
197
|
+
|
|
198
|
+
// 15 bars declining from 120 to ~80 (SMA10 falling, price below SMA)
|
|
199
|
+
for (let i = 0; i < 15; i++) prices.push(120 - i * 2.67);
|
|
200
|
+
|
|
201
|
+
// Brief dip and partial recovery — RSI may go oversold, but longScore < 2
|
|
202
|
+
// because SMA is falling and price is below SMA
|
|
203
|
+
prices.push(78, 76, 75, 74, 73);
|
|
204
|
+
for (let i = 0; i < 5; i++) prices.push(75 + i * 0.5);
|
|
205
|
+
|
|
206
|
+
const data = prices.map((p, i) => makeBar(i, p));
|
|
207
|
+
const engine = new BacktestEngine();
|
|
208
|
+
const result = await engine.run(strategy, data, config);
|
|
209
|
+
|
|
210
|
+
// No buy should occur: downtrend means longScore < 2
|
|
211
|
+
expect(result.totalTrades).toBe(0);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("executes partial exit on RSI overbought reversal", async () => {
|
|
215
|
+
// Test partial exit logic with mock IndicatorLib.
|
|
216
|
+
// Simulate: holding a position, RSI > overbought (65) and declining.
|
|
217
|
+
const strategy = createMultiTimeframeConfluence({
|
|
218
|
+
longSma: 10,
|
|
219
|
+
mediumEma: 5,
|
|
220
|
+
shortEma: 3,
|
|
221
|
+
rsiPeriod: 3,
|
|
222
|
+
rsiOversold: 35,
|
|
223
|
+
rsiOverbought: 65,
|
|
224
|
+
bbPeriod: 3,
|
|
225
|
+
bbStdDev: 2,
|
|
226
|
+
atrPeriod: 3,
|
|
227
|
+
atrStopMultiplier: 2.0,
|
|
228
|
+
atrProfitMultiplier: 3.0,
|
|
229
|
+
minConfluenceScore: 3,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const bar = makeBar(25, 150);
|
|
233
|
+
|
|
234
|
+
// Set up memory as if we entered earlier (partialExitDone = false)
|
|
235
|
+
const memory = new Map<string, unknown>();
|
|
236
|
+
memory.set("highestClose", 155);
|
|
237
|
+
memory.set("partialExitDone", false);
|
|
238
|
+
memory.set("entryConfluence", 4);
|
|
239
|
+
|
|
240
|
+
// Mock indicators: RSI overbought (70) and declining (prevRsi=75)
|
|
241
|
+
// Price still above SMA (no breakdown), no EMA collapse, no trailing stop
|
|
242
|
+
const mockIndicators = {
|
|
243
|
+
sma: () => Array(25).fill(NaN).concat([140, 141]),
|
|
244
|
+
ema: (period: number) => {
|
|
245
|
+
if (period === 5) return Array(25).fill(NaN).concat([146, 148]);
|
|
246
|
+
return Array(25).fill(NaN).concat([148, 149]); // shortEma
|
|
247
|
+
},
|
|
248
|
+
rsi: () => Array(25).fill(NaN).concat([75, 70]), // overbought and declining
|
|
249
|
+
bollingerBands: () => ({
|
|
250
|
+
upper: Array(26).fill(NaN).concat([160]),
|
|
251
|
+
middle: Array(26).fill(NaN).concat([145]),
|
|
252
|
+
lower: Array(26).fill(NaN).concat([130]),
|
|
253
|
+
}),
|
|
254
|
+
atr: () => Array(25).fill(NaN).concat([3, 3]),
|
|
255
|
+
macd: () => ({ macd: [], signal: [], histogram: [] }),
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const ctx = {
|
|
259
|
+
portfolio: {
|
|
260
|
+
equity: 10000,
|
|
261
|
+
cash: 5000,
|
|
262
|
+
positions: [
|
|
263
|
+
{
|
|
264
|
+
side: "long" as const,
|
|
265
|
+
symbol: "BTC/USDT",
|
|
266
|
+
quantity: 1,
|
|
267
|
+
entryPrice: 105,
|
|
268
|
+
currentPrice: 150,
|
|
269
|
+
unrealizedPnl: 45,
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
history: Array(27).fill(bar),
|
|
274
|
+
indicators: mockIndicators,
|
|
275
|
+
regime: "sideways" as const,
|
|
276
|
+
memory,
|
|
277
|
+
log: () => {},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const signal = await strategy.onBar(bar, ctx);
|
|
281
|
+
expect(signal).not.toBeNull();
|
|
282
|
+
expect(signal!.action).toBe("sell");
|
|
283
|
+
expect(signal!.sizePct).toBe(50);
|
|
284
|
+
expect(signal!.reason).toBe("Partial exit: RSI overbought reversal");
|
|
285
|
+
expect(memory.get("partialExitDone")).toBe(true);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import type { OHLCV } from "../../../fin-shared-types/src/types.js";
|
|
2
|
+
import type { Signal, StrategyContext, StrategyDefinition } from "../types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Multi-Timeframe Confluence composite strategy.
|
|
6
|
+
*
|
|
7
|
+
* Combines long-term trend alignment (SMA200, EMA50/20 structure) with
|
|
8
|
+
* short-term pullback signals (RSI oversold, Bollinger Band lower touch)
|
|
9
|
+
* to enter high-probability long positions during confirmed uptrends.
|
|
10
|
+
*
|
|
11
|
+
* Buy when: long-term uptrend confirmed (score >= 2) + short-term pullback
|
|
12
|
+
* detected (score >= 1) + RSI turning up + total confluence >= minConfluenceScore.
|
|
13
|
+
*
|
|
14
|
+
* Sell when: SMA200 breakdown, EMA structure collapse, or ATR trailing stop hit.
|
|
15
|
+
* Partial exit when RSI overbought + turning down (first occurrence only).
|
|
16
|
+
*/
|
|
17
|
+
export function createMultiTimeframeConfluence(params?: {
|
|
18
|
+
longSma?: number;
|
|
19
|
+
mediumEma?: number;
|
|
20
|
+
shortEma?: number;
|
|
21
|
+
rsiPeriod?: number;
|
|
22
|
+
rsiOversold?: number;
|
|
23
|
+
rsiOverbought?: number;
|
|
24
|
+
bbPeriod?: number;
|
|
25
|
+
bbStdDev?: number;
|
|
26
|
+
atrPeriod?: number;
|
|
27
|
+
atrStopMultiplier?: number;
|
|
28
|
+
atrProfitMultiplier?: number;
|
|
29
|
+
maxSizePct?: number;
|
|
30
|
+
minConfluenceScore?: number;
|
|
31
|
+
symbol?: string;
|
|
32
|
+
}): StrategyDefinition {
|
|
33
|
+
const longSma = params?.longSma ?? 200;
|
|
34
|
+
const mediumEma = params?.mediumEma ?? 50;
|
|
35
|
+
const shortEma = params?.shortEma ?? 20;
|
|
36
|
+
const rsiPeriod = params?.rsiPeriod ?? 7;
|
|
37
|
+
const rsiOversold = params?.rsiOversold ?? 35;
|
|
38
|
+
const rsiOverbought = params?.rsiOverbought ?? 65;
|
|
39
|
+
const bbPeriod = params?.bbPeriod ?? 10;
|
|
40
|
+
const bbStdDev = params?.bbStdDev ?? 2.0;
|
|
41
|
+
const atrPeriod = params?.atrPeriod ?? 14;
|
|
42
|
+
const atrStopMultiplier = params?.atrStopMultiplier ?? 2.0;
|
|
43
|
+
const atrProfitMultiplier = params?.atrProfitMultiplier ?? 3.0;
|
|
44
|
+
const maxSizePct = params?.maxSizePct ?? 70;
|
|
45
|
+
const minConfluenceScore = params?.minConfluenceScore ?? 3;
|
|
46
|
+
const symbol = params?.symbol ?? "BTC/USDT";
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
id: "multi-timeframe-confluence",
|
|
50
|
+
name: "Multi-Timeframe Confluence",
|
|
51
|
+
version: "1.0.0",
|
|
52
|
+
markets: ["crypto", "equity"],
|
|
53
|
+
symbols: [symbol],
|
|
54
|
+
timeframes: ["1d"],
|
|
55
|
+
parameters: {
|
|
56
|
+
longSma,
|
|
57
|
+
mediumEma,
|
|
58
|
+
shortEma,
|
|
59
|
+
rsiPeriod,
|
|
60
|
+
rsiOversold,
|
|
61
|
+
rsiOverbought,
|
|
62
|
+
bbPeriod,
|
|
63
|
+
bbStdDev,
|
|
64
|
+
atrPeriod,
|
|
65
|
+
atrStopMultiplier,
|
|
66
|
+
atrProfitMultiplier,
|
|
67
|
+
maxSizePct,
|
|
68
|
+
minConfluenceScore,
|
|
69
|
+
},
|
|
70
|
+
parameterRanges: {
|
|
71
|
+
longSma: { min: 50, max: 300, step: 50 },
|
|
72
|
+
mediumEma: { min: 20, max: 100, step: 10 },
|
|
73
|
+
shortEma: { min: 5, max: 50, step: 5 },
|
|
74
|
+
rsiPeriod: { min: 3, max: 21, step: 2 },
|
|
75
|
+
rsiOversold: { min: 20, max: 45, step: 5 },
|
|
76
|
+
rsiOverbought: { min: 55, max: 80, step: 5 },
|
|
77
|
+
bbPeriod: { min: 5, max: 30, step: 5 },
|
|
78
|
+
bbStdDev: { min: 1.0, max: 3.0, step: 0.5 },
|
|
79
|
+
atrPeriod: { min: 7, max: 28, step: 7 },
|
|
80
|
+
atrStopMultiplier: { min: 1.0, max: 4.0, step: 0.5 },
|
|
81
|
+
atrProfitMultiplier: { min: 1.5, max: 6.0, step: 0.5 },
|
|
82
|
+
maxSizePct: { min: 20, max: 100, step: 10 },
|
|
83
|
+
minConfluenceScore: { min: 2, max: 5, step: 1 },
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
async onBar(bar: OHLCV, ctx: StrategyContext): Promise<Signal | null> {
|
|
87
|
+
// Compute all indicators
|
|
88
|
+
const smaArr = ctx.indicators.sma(longSma);
|
|
89
|
+
const medEmaArr = ctx.indicators.ema(mediumEma);
|
|
90
|
+
const shortEmaArr = ctx.indicators.ema(shortEma);
|
|
91
|
+
const rsiArr = ctx.indicators.rsi(rsiPeriod);
|
|
92
|
+
const bb = ctx.indicators.bollingerBands(bbPeriod, bbStdDev);
|
|
93
|
+
const atrArr = ctx.indicators.atr(atrPeriod);
|
|
94
|
+
|
|
95
|
+
const len = smaArr.length;
|
|
96
|
+
if (len < 2) return null;
|
|
97
|
+
|
|
98
|
+
const currSma = smaArr[len - 1]!;
|
|
99
|
+
const prevSma = smaArr[len - 2]!;
|
|
100
|
+
const currEma50 = medEmaArr[len - 1]!;
|
|
101
|
+
const currEma20 = shortEmaArr[len - 1]!;
|
|
102
|
+
const currRsi = rsiArr[len - 1]!;
|
|
103
|
+
const prevRsi = rsiArr[len - 2]!;
|
|
104
|
+
const currBBLower = bb.lower[len - 1]!;
|
|
105
|
+
const currAtr = atrArr[len - 1]!;
|
|
106
|
+
|
|
107
|
+
// NaN guard: need at least longSma bars for SMA warmup
|
|
108
|
+
if (
|
|
109
|
+
Number.isNaN(currSma) ||
|
|
110
|
+
Number.isNaN(prevSma) ||
|
|
111
|
+
Number.isNaN(currEma50) ||
|
|
112
|
+
Number.isNaN(currEma20) ||
|
|
113
|
+
Number.isNaN(currRsi) ||
|
|
114
|
+
Number.isNaN(prevRsi) ||
|
|
115
|
+
Number.isNaN(currBBLower) ||
|
|
116
|
+
Number.isNaN(currAtr)
|
|
117
|
+
) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Regime filter: skip during crisis
|
|
122
|
+
if (ctx.regime === "crisis") return null;
|
|
123
|
+
|
|
124
|
+
const hasLong = ctx.portfolio.positions.some((p) => p.side === "long");
|
|
125
|
+
|
|
126
|
+
// --- SELL / PARTIAL EXIT conditions (check first when holding) ---
|
|
127
|
+
if (hasLong) {
|
|
128
|
+
// Update highest close for trailing stop
|
|
129
|
+
const storedHighest = (ctx.memory.get("highestClose") as number) ?? bar.close;
|
|
130
|
+
const highestClose = Math.max(storedHighest, bar.close);
|
|
131
|
+
ctx.memory.set("highestClose", highestClose);
|
|
132
|
+
|
|
133
|
+
// Partial exit: RSI overbought reversal (one-time)
|
|
134
|
+
const partialExitDone = ctx.memory.get("partialExitDone") as boolean | undefined;
|
|
135
|
+
if (!partialExitDone && currRsi > rsiOverbought && currRsi < prevRsi) {
|
|
136
|
+
ctx.memory.set("partialExitDone", true);
|
|
137
|
+
return {
|
|
138
|
+
action: "sell",
|
|
139
|
+
symbol,
|
|
140
|
+
sizePct: 50,
|
|
141
|
+
orderType: "market",
|
|
142
|
+
reason: "Partial exit: RSI overbought reversal",
|
|
143
|
+
confidence: 0.7,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// SMA200 breakdown: price fell below long-term trend
|
|
148
|
+
if (bar.close < currSma) {
|
|
149
|
+
ctx.memory.delete("longConfluence");
|
|
150
|
+
ctx.memory.delete("shortConfluence");
|
|
151
|
+
ctx.memory.delete("entryConfluence");
|
|
152
|
+
ctx.memory.delete("highestClose");
|
|
153
|
+
ctx.memory.delete("partialExitDone");
|
|
154
|
+
return {
|
|
155
|
+
action: "sell",
|
|
156
|
+
symbol,
|
|
157
|
+
sizePct: 100,
|
|
158
|
+
orderType: "market",
|
|
159
|
+
reason: `SMA breakdown: close=${bar.close.toFixed(2)} < SMA(${longSma})=${currSma.toFixed(2)}`,
|
|
160
|
+
confidence: 0.8,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// EMA structure collapse: full bearish alignment
|
|
165
|
+
if (currEma20 < currEma50 && currEma50 < currSma) {
|
|
166
|
+
ctx.memory.delete("longConfluence");
|
|
167
|
+
ctx.memory.delete("shortConfluence");
|
|
168
|
+
ctx.memory.delete("entryConfluence");
|
|
169
|
+
ctx.memory.delete("highestClose");
|
|
170
|
+
ctx.memory.delete("partialExitDone");
|
|
171
|
+
return {
|
|
172
|
+
action: "sell",
|
|
173
|
+
symbol,
|
|
174
|
+
sizePct: 100,
|
|
175
|
+
orderType: "market",
|
|
176
|
+
reason: `EMA structure collapse: EMA(${shortEma})=${currEma20.toFixed(2)} < EMA(${mediumEma})=${currEma50.toFixed(2)} < SMA(${longSma})=${currSma.toFixed(2)}`,
|
|
177
|
+
confidence: 0.85,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Trailing stop: price dropped more than atrStopMultiplier * ATR from highest
|
|
182
|
+
if (bar.close < highestClose - atrStopMultiplier * currAtr) {
|
|
183
|
+
ctx.memory.delete("longConfluence");
|
|
184
|
+
ctx.memory.delete("shortConfluence");
|
|
185
|
+
ctx.memory.delete("entryConfluence");
|
|
186
|
+
ctx.memory.delete("highestClose");
|
|
187
|
+
ctx.memory.delete("partialExitDone");
|
|
188
|
+
return {
|
|
189
|
+
action: "sell",
|
|
190
|
+
symbol,
|
|
191
|
+
sizePct: 100,
|
|
192
|
+
orderType: "market",
|
|
193
|
+
reason: `Trailing stop: close=${bar.close.toFixed(2)} < highest=${highestClose.toFixed(2)} - ${atrStopMultiplier}*ATR=${currAtr.toFixed(2)}`,
|
|
194
|
+
confidence: 0.75,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// --- BUY conditions ---
|
|
200
|
+
if (!hasLong) {
|
|
201
|
+
// Long-term confluence scoring (3 items, each 0 or 1)
|
|
202
|
+
const smaRising = currSma > prevSma && bar.close > currSma ? 1 : 0;
|
|
203
|
+
const structure = bar.close > currEma50 && currEma50 > currSma ? 1 : 0;
|
|
204
|
+
const emaStack = currEma20 > currEma50 ? 1 : 0;
|
|
205
|
+
const longScore = smaRising + structure + emaStack;
|
|
206
|
+
|
|
207
|
+
// Short-term pullback scoring (2 items)
|
|
208
|
+
const rsiPullback = currRsi < rsiOversold ? 1 : 0;
|
|
209
|
+
const bbTouch = bar.close <= currBBLower ? 1 : 0;
|
|
210
|
+
const shortScore = rsiPullback + bbTouch;
|
|
211
|
+
|
|
212
|
+
const totalScore = longScore + shortScore;
|
|
213
|
+
|
|
214
|
+
// All conditions must be met
|
|
215
|
+
const longTrendStrong = longScore >= 2;
|
|
216
|
+
const pullbackHappening = shortScore >= 1;
|
|
217
|
+
const rsiTurningUp = currRsi > prevRsi;
|
|
218
|
+
const confluenceMet = totalScore >= minConfluenceScore;
|
|
219
|
+
|
|
220
|
+
if (longTrendStrong && pullbackHappening && rsiTurningUp && confluenceMet) {
|
|
221
|
+
// Confidence = 0.3 + 0.12 * totalScore, clamped [0.3, 0.95]
|
|
222
|
+
const confidence = Math.max(0.3, Math.min(0.95, 0.3 + 0.12 * totalScore));
|
|
223
|
+
|
|
224
|
+
const stopLoss = bar.close - atrStopMultiplier * currAtr;
|
|
225
|
+
const takeProfit = bar.close + atrProfitMultiplier * currAtr;
|
|
226
|
+
|
|
227
|
+
// Store state in memory
|
|
228
|
+
ctx.memory.set("longConfluence", longScore);
|
|
229
|
+
ctx.memory.set("shortConfluence", shortScore);
|
|
230
|
+
ctx.memory.set("entryConfluence", totalScore);
|
|
231
|
+
ctx.memory.set("highestClose", bar.close);
|
|
232
|
+
ctx.memory.set("partialExitDone", false);
|
|
233
|
+
|
|
234
|
+
// Dynamic position sizing
|
|
235
|
+
const sizePct = Math.min(maxSizePct, Math.max(20, confidence * 90));
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
action: "buy",
|
|
239
|
+
symbol,
|
|
240
|
+
sizePct,
|
|
241
|
+
orderType: "market",
|
|
242
|
+
reason: `Confluence buy: longScore=${longScore} shortScore=${shortScore} total=${totalScore} RSI=${currRsi.toFixed(1)}`,
|
|
243
|
+
confidence,
|
|
244
|
+
stopLoss,
|
|
245
|
+
takeProfit,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return null;
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|