@backtest-kit/sidekick 0.1.2 → 3.0.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 +120 -13
- package/content/config/source/timeframe_15m.pine +114 -0
- package/content/config/source/timeframe_4h.pine +57 -0
- package/content/config/symbol.config.cjs +460 -0
- package/content/docker/ollama/docker-compose.yaml +34 -0
- package/content/docker/ollama/watch.sh +2 -0
- package/content/scripts/cache/cache_candles.mjs +47 -0
- package/content/scripts/cache/cache_model.mjs +42 -0
- package/content/scripts/cache/validate_candles.mjs +46 -0
- package/content/scripts/run_timeframe_15m.mjs +77 -0
- package/content/scripts/run_timeframe_4h.mjs +68 -0
- package/package.json +2 -2
- package/scripts/init.mjs +40 -9
- package/src/classes/BacktestLowerStopOnBreakevenAction.mjs +18 -0
- package/src/classes/BacktestPartialProfitTakingAction.mjs +5 -0
- package/src/classes/BacktestPositionMonitorAction.mjs +4 -0
- package/src/config/setup.mjs +27 -1
- package/src/enum/ActionName.mjs +1 -1
- package/src/enum/FrameName.mjs +1 -0
- package/src/enum/RiskName.mjs +1 -1
- package/src/logic/action/backtest_lower_stop_on_breakeven.action.mjs +9 -0
- package/src/logic/exchange/binance.exchange.mjs +12 -2
- package/src/logic/frame/feb_2024.frame.mjs +10 -0
- package/src/logic/index.mjs +3 -2
- package/src/logic/risk/sl_distance.risk.mjs +32 -0
- package/src/logic/risk/tp_distance.risk.mjs +5 -3
- package/src/logic/strategy/main.strategy.mjs +29 -12
- package/src/main/bootstrap.mjs +1 -1
- package/src/math/timeframe_15m.math.mjs +68 -0
- package/src/math/timeframe_4h.math.mjs +53 -0
- package/template/CLAUDE.mustache +421 -0
- package/template/README.mustache +232 -24
- package/template/env.mustache +1 -17
- package/template/jsconfig.json.mustache +1 -0
- package/template/package.mustache +8 -5
- package/src/classes/BacktestTightenStopOnBreakevenAction.mjs +0 -13
- package/src/func/market.func.mjs +0 -46
- package/src/logic/action/backtest_tighten_stop_on_breakeven.action.mjs +0 -9
- package/src/logic/risk/rr_ratio.risk.mjs +0 -39
- /package/{types/backtest-kit.d.ts → template/types.mustache} +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Cache } from "backtest-kit";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
run,
|
|
5
|
+
File,
|
|
6
|
+
toMarkdown,
|
|
7
|
+
toSignalDto,
|
|
8
|
+
extract,
|
|
9
|
+
dumpPlotData,
|
|
10
|
+
} from "@backtest-kit/pinets";
|
|
11
|
+
|
|
12
|
+
const SIGNAL_SCHEMA = {
|
|
13
|
+
allowLong: "AllowLong",
|
|
14
|
+
allowShort: "AllowShort",
|
|
15
|
+
allowBoth: "AllowBoth",
|
|
16
|
+
noTrades: "NoTrades",
|
|
17
|
+
rsi: "RSI",
|
|
18
|
+
adx: "ADX",
|
|
19
|
+
d_MACDLine: "d_MACDLine",
|
|
20
|
+
d_SignalLine: "d_SignalLine",
|
|
21
|
+
d_MACDHist: "d_MACDHist",
|
|
22
|
+
d_DIPlus: "d_DIPlus",
|
|
23
|
+
d_DIMinus: "d_DIMinus",
|
|
24
|
+
d_StrongTrend: "d_StrongTrend",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const getPlot = Cache.fn(
|
|
28
|
+
async (symbol) =>
|
|
29
|
+
await run(File.fromPath("timeframe_4h.pine"), {
|
|
30
|
+
symbol,
|
|
31
|
+
timeframe: "4h",
|
|
32
|
+
limit: 100,
|
|
33
|
+
}),
|
|
34
|
+
{
|
|
35
|
+
interval: "4h",
|
|
36
|
+
key: ([symbol]) => `${symbol}`,
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
export const getMarkdown = async (signalId, symbol) => {
|
|
41
|
+
const plots = await getPlot(symbol);
|
|
42
|
+
return await toMarkdown(signalId, plots, SIGNAL_SCHEMA);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const getData = async (signalId, symbol) => {
|
|
46
|
+
const plots = await getPlot(symbol);
|
|
47
|
+
return await extract(plots, SIGNAL_SCHEMA);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const dumpPlot = async (signalId, symbol) => {
|
|
51
|
+
const plots = await getPlot(symbol);
|
|
52
|
+
dumpPlotData(signalId, plots, SIGNAL_SCHEMA, "math_4h");
|
|
53
|
+
};
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# CLAUDE.md — AI Strategy Development Guide
|
|
2
|
+
|
|
3
|
+
This project is an **AI-in-the-loop trading strategy development environment**. You (Claude) write Pine Script indicators, run backtests, read JSONL reports, analyze results, and iterate. The human provides direction; you do the research, coding, and optimization.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
- **Framework**: `backtest-kit` with `@backtest-kit/pinets` (Pine Script runtime for Node.js)
|
|
8
|
+
- **Exchange**: Binance spot via CCXT
|
|
9
|
+
- **Strategy**: Multi-timeframe — 4H trend filter + 15m signal generator
|
|
10
|
+
- **Entry point**: `node ./src/index.mjs --backtest`
|
|
11
|
+
- **Output**: `dump/report/*.jsonl` + `dump/ta/*.md`
|
|
12
|
+
|
|
13
|
+
## Iteration Cycle
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
1. Read current Pine Script indicators → config/source/*.pine
|
|
17
|
+
2. Read current backtest results → dump/report/*.jsonl
|
|
18
|
+
3. Analyze PnL, win rate, close reasons → write Node.js analysis script
|
|
19
|
+
4. Identify problems → e.g. "all trades expire, TP too far"
|
|
20
|
+
5. Modify Pine Script or strategy code → config/source/*.pine, src/logic/**
|
|
21
|
+
6. Run backtest → npm start
|
|
22
|
+
7. Compare results with previous run → write diff analysis script
|
|
23
|
+
8. Repeat from step 2
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Key Files to Read and Modify
|
|
27
|
+
|
|
28
|
+
### Pine Script Indicators (your main editing targets)
|
|
29
|
+
|
|
30
|
+
- `config/source/timeframe_4h.pine` — 4H trend filter (RSI, MACD, ADX). Outputs: `AllowLong`, `AllowShort`, `AllowBoth`, `NoTrades`
|
|
31
|
+
- `config/source/timeframe_15m.pine` — 15m signal generator (EMA crossover, ATR, volume, momentum). Outputs: `Signal`, `Close`, `TakeProfit`, `StopLoss`, `EstimatedTime`
|
|
32
|
+
|
|
33
|
+
### Strategy Logic
|
|
34
|
+
|
|
35
|
+
- `src/logic/strategy/main.strategy.mjs` — Combines 4H filter + 15m signal. Edit to change timeframe interaction logic
|
|
36
|
+
- `src/math/timeframe_4h.math.mjs` — SIGNAL_SCHEMA mapping for 4H Pine outputs
|
|
37
|
+
- `src/math/timeframe_15m.math.mjs` — SIGNAL_SCHEMA mapping for 15m Pine outputs
|
|
38
|
+
|
|
39
|
+
### Risk Management
|
|
40
|
+
|
|
41
|
+
- `src/logic/risk/sl_distance.risk.mjs` — SL distance threshold (currently 0.2%)
|
|
42
|
+
- `src/logic/risk/tp_distance.risk.mjs` — TP distance threshold (currently 0.2%)
|
|
43
|
+
|
|
44
|
+
### Position Actions
|
|
45
|
+
|
|
46
|
+
- `src/classes/BacktestPartialProfitTakingAction.mjs` — Partial profit levels (33/33/34%)
|
|
47
|
+
- `src/classes/BacktestLowerStopOnBreakevenAction.mjs` — Trailing stop on breakeven (-3 points)
|
|
48
|
+
- `src/classes/BacktestPositionMonitorAction.mjs` — Position lifecycle logger
|
|
49
|
+
|
|
50
|
+
### Backtest Frames
|
|
51
|
+
|
|
52
|
+
- `src/logic/frame/*.frame.mjs` — Time periods. Edit date ranges or add new frames
|
|
53
|
+
- Available: `feb_2024_frame` (bull), `oct_2025_frame` (drop), `nov_2025_frame` (sideways), `dec_2025_frame` (flat)
|
|
54
|
+
|
|
55
|
+
## Reading Backtest Results
|
|
56
|
+
|
|
57
|
+
### JSONL Report Files
|
|
58
|
+
|
|
59
|
+
All in `dump/report/`. One JSON object per line. Key files:
|
|
60
|
+
|
|
61
|
+
**`heat.jsonl`** — Most important. One line per closed position:
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"data": {
|
|
65
|
+
"action": "closed",
|
|
66
|
+
"position": "long",
|
|
67
|
+
"priceOpen": 120385.76,
|
|
68
|
+
"pnl": 0.9596,
|
|
69
|
+
"closeReason": "time_expired",
|
|
70
|
+
"openTime": 1759501800000,
|
|
71
|
+
"closeTime": 1759588200000
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**`schedule.jsonl`** — Signal entries:
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"data": {
|
|
80
|
+
"action": "scheduled",
|
|
81
|
+
"position": "long",
|
|
82
|
+
"priceOpen": 118659.16,
|
|
83
|
+
"priceTakeProfit": 122218.93,
|
|
84
|
+
"priceStopLoss": 116285.97,
|
|
85
|
+
"minuteEstimatedTime": 1440
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**`partial.jsonl`** — Partial profit/loss at each level (10, 20, 30 ... 90):
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"data": {
|
|
94
|
+
"action": "profit",
|
|
95
|
+
"level": 40,
|
|
96
|
+
"partialExecuted": 34,
|
|
97
|
+
"currentPrice": 120084.69,
|
|
98
|
+
"priceOpen": 118659.16,
|
|
99
|
+
"_partial": [{"type": "profit", "percent": 34, "price": 119666.13}]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**`breakeven.jsonl`** — Breakeven trigger events (price returned to entry level)
|
|
105
|
+
|
|
106
|
+
**`performance.jsonl`** — Execution timing per timeframe step (duration in ms)
|
|
107
|
+
|
|
108
|
+
**`backtest.jsonl`** — Every strategy tick. Large file (25MB+). Use `offset`/`limit` or grep for `"action":"signal"` lines
|
|
109
|
+
|
|
110
|
+
### TA Markdown Dumps
|
|
111
|
+
|
|
112
|
+
- `dump/ta/math_15m/{signalId}.md` — Full indicator table for 15m timeframe at signal time
|
|
113
|
+
- `dump/ta/math_4h/{signalId}.md` — Full indicator table for 4H timeframe at signal time
|
|
114
|
+
|
|
115
|
+
These contain 100 rows of indicator data (one per candle) with all Pine Script plot values. The last row with `position != 0` is the actual signal. Cross-reference `signalId` across all files.
|
|
116
|
+
|
|
117
|
+
### Storage State
|
|
118
|
+
|
|
119
|
+
- `dump/data/storage/backtest/{signalId}.json` — Final signal state including `_partial` array, `pnl`, `status`, `closeReason`
|
|
120
|
+
|
|
121
|
+
## Writing Analysis Scripts
|
|
122
|
+
|
|
123
|
+
When analyzing backtest results, write standalone Node.js scripts (ESM, `.mjs`) in the `scripts/` directory. These scripts should read JSONL files, compute metrics, and print results.
|
|
124
|
+
|
|
125
|
+
### Example: PnL Summary
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
// scripts/analyze_pnl.mjs
|
|
129
|
+
import { readFileSync } from "fs";
|
|
130
|
+
|
|
131
|
+
const lines = readFileSync("dump/report/heat.jsonl", "utf-8")
|
|
132
|
+
.trim()
|
|
133
|
+
.split("\n")
|
|
134
|
+
.filter(Boolean)
|
|
135
|
+
.map(JSON.parse);
|
|
136
|
+
|
|
137
|
+
const trades = lines.filter((l) => l.data.action === "closed");
|
|
138
|
+
|
|
139
|
+
const totalPnl = trades.reduce((sum, t) => sum + t.data.pnl, 0);
|
|
140
|
+
const avgPnl = totalPnl / trades.length;
|
|
141
|
+
const wins = trades.filter((t) => t.data.pnl > 0);
|
|
142
|
+
const losses = trades.filter((t) => t.data.pnl <= 0);
|
|
143
|
+
|
|
144
|
+
const byReason = {};
|
|
145
|
+
for (const t of trades) {
|
|
146
|
+
const reason = t.data.closeReason;
|
|
147
|
+
byReason[reason] = byReason[reason] || { count: 0, pnl: 0 };
|
|
148
|
+
byReason[reason].count++;
|
|
149
|
+
byReason[reason].pnl += t.data.pnl;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(`=== PnL Summary ===`);
|
|
153
|
+
console.log(`Total trades: ${trades.length}`);
|
|
154
|
+
console.log(`Win rate: ${((wins.length / trades.length) * 100).toFixed(1)}%`);
|
|
155
|
+
console.log(`Total PnL: ${totalPnl.toFixed(4)}%`);
|
|
156
|
+
console.log(`Average PnL: ${avgPnl.toFixed(4)}%`);
|
|
157
|
+
console.log(`Best trade: ${Math.max(...trades.map((t) => t.data.pnl)).toFixed(4)}%`);
|
|
158
|
+
console.log(`Worst trade: ${Math.min(...trades.map((t) => t.data.pnl)).toFixed(4)}%`);
|
|
159
|
+
console.log(`\n=== By Close Reason ===`);
|
|
160
|
+
for (const [reason, data] of Object.entries(byReason)) {
|
|
161
|
+
console.log(`${reason}: ${data.count} trades, PnL ${data.pnl.toFixed(4)}%`);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Example: Trade Duration Analysis
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
// scripts/analyze_duration.mjs
|
|
169
|
+
import { readFileSync } from "fs";
|
|
170
|
+
|
|
171
|
+
const lines = readFileSync("dump/report/heat.jsonl", "utf-8")
|
|
172
|
+
.trim()
|
|
173
|
+
.split("\n")
|
|
174
|
+
.filter(Boolean)
|
|
175
|
+
.map(JSON.parse);
|
|
176
|
+
|
|
177
|
+
const trades = lines.filter((l) => l.data.action === "closed");
|
|
178
|
+
|
|
179
|
+
console.log(`=== Trade Duration Analysis ===\n`);
|
|
180
|
+
for (const t of trades) {
|
|
181
|
+
const durationMs = t.data.closeTime - t.data.openTime;
|
|
182
|
+
const durationH = (durationMs / 3600000).toFixed(1);
|
|
183
|
+
const pnl = t.data.pnl.toFixed(4);
|
|
184
|
+
const reason = t.data.closeReason;
|
|
185
|
+
const date = new Date(t.data.openTime).toISOString().slice(0, 10);
|
|
186
|
+
console.log(
|
|
187
|
+
`${date} | ${t.data.position.padEnd(5)} | ${durationH}h | PnL ${pnl}% | ${reason}`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Example: Partial Profit Execution Analysis
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
// scripts/analyze_partials.mjs
|
|
196
|
+
import { readFileSync } from "fs";
|
|
197
|
+
|
|
198
|
+
const lines = readFileSync("dump/report/partial.jsonl", "utf-8")
|
|
199
|
+
.trim()
|
|
200
|
+
.split("\n")
|
|
201
|
+
.filter(Boolean)
|
|
202
|
+
.map(JSON.parse);
|
|
203
|
+
|
|
204
|
+
// Group by signalId
|
|
205
|
+
const bySignal = {};
|
|
206
|
+
for (const l of lines) {
|
|
207
|
+
const id = l.signalId;
|
|
208
|
+
bySignal[id] = bySignal[id] || [];
|
|
209
|
+
bySignal[id].push(l.data);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
console.log(`=== Partial Profit Analysis ===\n`);
|
|
213
|
+
for (const [signalId, events] of Object.entries(bySignal)) {
|
|
214
|
+
const maxLevel = Math.max(...events.map((e) => e.level));
|
|
215
|
+
const executed = events.filter((e) => e.partialExecuted > 0);
|
|
216
|
+
const maxExecuted = Math.max(...events.map((e) => e.partialExecuted), 0);
|
|
217
|
+
console.log(
|
|
218
|
+
`${signalId.slice(0, 8)}... | max level: ${maxLevel}% | executed: ${maxExecuted}%`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Example: Compare Two Backtest Runs
|
|
224
|
+
|
|
225
|
+
```javascript
|
|
226
|
+
// scripts/compare_runs.mjs
|
|
227
|
+
// Usage: node scripts/compare_runs.mjs dump/report/heat.jsonl dump_prev/report/heat.jsonl
|
|
228
|
+
import { readFileSync } from "fs";
|
|
229
|
+
|
|
230
|
+
const parse = (path) =>
|
|
231
|
+
readFileSync(path, "utf-8")
|
|
232
|
+
.trim()
|
|
233
|
+
.split("\n")
|
|
234
|
+
.filter(Boolean)
|
|
235
|
+
.map(JSON.parse)
|
|
236
|
+
.filter((l) => l.data.action === "closed");
|
|
237
|
+
|
|
238
|
+
const [, , current, previous] = process.argv;
|
|
239
|
+
|
|
240
|
+
if (!current || !previous) {
|
|
241
|
+
console.log("Usage: node scripts/compare_runs.mjs <current.jsonl> <previous.jsonl>");
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const cur = parse(current);
|
|
246
|
+
const prev = parse(previous);
|
|
247
|
+
|
|
248
|
+
const stats = (trades) => ({
|
|
249
|
+
count: trades.length,
|
|
250
|
+
totalPnl: trades.reduce((s, t) => s + t.data.pnl, 0),
|
|
251
|
+
winRate: trades.filter((t) => t.data.pnl > 0).length / trades.length,
|
|
252
|
+
avgPnl: trades.reduce((s, t) => s + t.data.pnl, 0) / trades.length,
|
|
253
|
+
byReason: trades.reduce((acc, t) => {
|
|
254
|
+
acc[t.data.closeReason] = (acc[t.data.closeReason] || 0) + 1;
|
|
255
|
+
return acc;
|
|
256
|
+
}, {}),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const c = stats(cur);
|
|
260
|
+
const p = stats(prev);
|
|
261
|
+
|
|
262
|
+
const delta = (a, b) => {
|
|
263
|
+
const diff = a - b;
|
|
264
|
+
return diff >= 0 ? `+${diff.toFixed(4)}` : diff.toFixed(4);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
console.log(`=== Run Comparison ===`);
|
|
268
|
+
console.log(`Metric | Previous | Current | Delta`);
|
|
269
|
+
console.log(`----------------|----------------|----------------|-------`);
|
|
270
|
+
console.log(
|
|
271
|
+
`Trades | ${String(p.count).padEnd(14)} | ${String(c.count).padEnd(14)} | ${delta(c.count, p.count)}`
|
|
272
|
+
);
|
|
273
|
+
console.log(
|
|
274
|
+
`Total PnL | ${p.totalPnl.toFixed(4).padEnd(14)} | ${c.totalPnl.toFixed(4).padEnd(14)} | ${delta(c.totalPnl, p.totalPnl)}`
|
|
275
|
+
);
|
|
276
|
+
console.log(
|
|
277
|
+
`Win Rate | ${(p.winRate * 100).toFixed(1).padEnd(13)}% | ${(c.winRate * 100).toFixed(1).padEnd(13)}% | ${delta(c.winRate * 100, p.winRate * 100)}%`
|
|
278
|
+
);
|
|
279
|
+
console.log(
|
|
280
|
+
`Avg PnL | ${p.avgPnl.toFixed(4).padEnd(14)} | ${c.avgPnl.toFixed(4).padEnd(14)} | ${delta(c.avgPnl, p.avgPnl)}`
|
|
281
|
+
);
|
|
282
|
+
console.log(`\n=== Close Reasons ===`);
|
|
283
|
+
const allReasons = new Set([
|
|
284
|
+
...Object.keys(c.byReason),
|
|
285
|
+
...Object.keys(p.byReason),
|
|
286
|
+
]);
|
|
287
|
+
for (const reason of allReasons) {
|
|
288
|
+
const pc = p.byReason[reason] || 0;
|
|
289
|
+
const cc = c.byReason[reason] || 0;
|
|
290
|
+
console.log(
|
|
291
|
+
`${reason.padEnd(16)}| ${String(pc).padEnd(14)} | ${String(cc).padEnd(14)} | ${delta(cc, pc)}`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Example: TA Indicator Snapshot at Signal Time
|
|
297
|
+
|
|
298
|
+
```javascript
|
|
299
|
+
// scripts/analyze_signal_ta.mjs
|
|
300
|
+
// Reads TA markdown dump for a specific signal to understand what indicators looked like at entry
|
|
301
|
+
import { readFileSync, readdirSync } from "fs";
|
|
302
|
+
|
|
303
|
+
const taDir15m = "dump/ta/math_15m";
|
|
304
|
+
const taDir4h = "dump/ta/math_4h";
|
|
305
|
+
|
|
306
|
+
const files = readdirSync(taDir15m).filter((f) => f.endsWith(".md"));
|
|
307
|
+
|
|
308
|
+
for (const file of files) {
|
|
309
|
+
const signalId = file.replace(".md", "");
|
|
310
|
+
const content15m = readFileSync(`${taDir15m}/${file}`, "utf-8");
|
|
311
|
+
const content4h = readFileSync(`${taDir4h}/${file}`, "utf-8");
|
|
312
|
+
|
|
313
|
+
// Extract last row (the actual signal row)
|
|
314
|
+
const rows15m = content15m.split("\n").filter((l) => l.startsWith("|") && !l.includes("---"));
|
|
315
|
+
const lastRow = rows15m[rows15m.length - 1];
|
|
316
|
+
|
|
317
|
+
const rows4h = content4h.split("\n").filter((l) => l.startsWith("|") && !l.includes("---"));
|
|
318
|
+
const lastRow4h = rows4h[rows4h.length - 1];
|
|
319
|
+
|
|
320
|
+
console.log(`\n=== Signal: ${signalId} ===`);
|
|
321
|
+
console.log(`15m signal row: ${lastRow}`);
|
|
322
|
+
console.log(`4H regime row: ${lastRow4h}`);
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Strategy Modification Checklist
|
|
327
|
+
|
|
328
|
+
When modifying the strategy, follow this order:
|
|
329
|
+
|
|
330
|
+
### Changing Pine Script Indicators
|
|
331
|
+
|
|
332
|
+
1. Edit `config/source/timeframe_15m.pine` or `config/source/timeframe_4h.pine`
|
|
333
|
+
2. If you add/remove `plot()` outputs, update the corresponding SIGNAL_SCHEMA in:
|
|
334
|
+
- `src/math/timeframe_15m.math.mjs` (key = JS field name, value = Pine plot title)
|
|
335
|
+
- `src/math/timeframe_4h.math.mjs`
|
|
336
|
+
3. Run `node scripts/run_timeframe_15m.mjs` or `node scripts/run_timeframe_4h.mjs` to verify output
|
|
337
|
+
4. Run `npm start` for full backtest
|
|
338
|
+
|
|
339
|
+
### Changing Risk Parameters
|
|
340
|
+
|
|
341
|
+
1. Edit `src/logic/risk/sl_distance.risk.mjs` or `tp_distance.risk.mjs`
|
|
342
|
+
2. Adjust `SLIPPAGE_THRESHOLD` constant
|
|
343
|
+
3. Run backtest and check if more/fewer signals pass validation
|
|
344
|
+
|
|
345
|
+
### Changing Position Management
|
|
346
|
+
|
|
347
|
+
1. Edit `src/classes/BacktestPartialProfitTakingAction.mjs` — change percentages at TP levels
|
|
348
|
+
2. Edit `src/classes/BacktestLowerStopOnBreakevenAction.mjs` — change trailing stop offset
|
|
349
|
+
3. Run backtest and analyze `partial.jsonl` and `breakeven.jsonl`
|
|
350
|
+
|
|
351
|
+
### Adding a New Backtest Frame
|
|
352
|
+
|
|
353
|
+
1. Create `src/logic/frame/new_name.frame.mjs`:
|
|
354
|
+
```javascript
|
|
355
|
+
import { addFrameSchema } from "backtest-kit";
|
|
356
|
+
import FrameName from "../../enum/FrameName.mjs";
|
|
357
|
+
addFrameSchema({
|
|
358
|
+
frameName: FrameName.NewName,
|
|
359
|
+
interval: "1m",
|
|
360
|
+
startDate: new Date("2025-01-01T00:00:00Z"),
|
|
361
|
+
endDate: new Date("2025-01-31T23:59:59Z"),
|
|
362
|
+
note: "Description of market conditions",
|
|
363
|
+
});
|
|
364
|
+
```
|
|
365
|
+
2. Add enum value in `src/enum/FrameName.mjs`
|
|
366
|
+
3. Import in `src/logic/index.mjs`
|
|
367
|
+
4. Run: `node ./src/index.mjs --backtest --frameName new_name_frame`
|
|
368
|
+
|
|
369
|
+
## Common Analysis Patterns
|
|
370
|
+
|
|
371
|
+
### "All trades close by time_expired"
|
|
372
|
+
|
|
373
|
+
**Diagnosis**: TP too far from entry, or `minuteEstimatedTime` too short.
|
|
374
|
+
**Fix options**:
|
|
375
|
+
- Reduce TP multiplier in `timeframe_15m.pine` (e.g. `close * 1.02` instead of `close * 1.03`)
|
|
376
|
+
- Increase `EstimatedTime` plot value (e.g. 2880 = 48h instead of 1440 = 24h)
|
|
377
|
+
- Switch from static SL/TP to ATR-based (uncomment lines 72-73 in `timeframe_15m.pine`)
|
|
378
|
+
|
|
379
|
+
### "Too few signals generated"
|
|
380
|
+
|
|
381
|
+
**Diagnosis**: Entry conditions too strict.
|
|
382
|
+
**Fix options**:
|
|
383
|
+
- Relax RSI range in `timeframe_15m.pine` (e.g. `rsi > 35 and rsi < 70` for longs)
|
|
384
|
+
- Lower volume spike threshold (e.g. `volume > vol_ma * 1.2` instead of `* 1.5`)
|
|
385
|
+
- Reduce `signal_valid_bars` expiry
|
|
386
|
+
|
|
387
|
+
### "Signals but all stopped out"
|
|
388
|
+
|
|
389
|
+
**Diagnosis**: SL too tight or entry timing poor.
|
|
390
|
+
**Fix options**:
|
|
391
|
+
- Increase SL multiplier in ATR-based mode
|
|
392
|
+
- Add additional confirmation (e.g. require 2 consecutive momentum bars)
|
|
393
|
+
- Check if 4H filter is allowing trades in wrong regime
|
|
394
|
+
|
|
395
|
+
### "Good PnL but low win rate"
|
|
396
|
+
|
|
397
|
+
**Diagnosis**: Few big winners compensate many small losers. May be acceptable.
|
|
398
|
+
**Check**: Analyze `partial.jsonl` — are partial profits being taken? If `partialExecuted` stays at 0 for most trades, partials aren't triggering.
|
|
399
|
+
|
|
400
|
+
## Before Running Backtest
|
|
401
|
+
|
|
402
|
+
1. **Clear previous dump** (optional): `rm -rf dump/report dump/ta` to get clean results
|
|
403
|
+
2. **Cache candles** (if new date range): `node scripts/cache/cache_candles.mjs`
|
|
404
|
+
3. **Validate cache**: `node scripts/cache/validate_candles.mjs`
|
|
405
|
+
4. **Run**: `npm start` or `node ./src/index.mjs --backtest --frameName <frame>`
|
|
406
|
+
|
|
407
|
+
## After Running Backtest
|
|
408
|
+
|
|
409
|
+
1. **Quick check**: Read last 10 lines of `dump/report/heat.jsonl` — see PnL and close reasons
|
|
410
|
+
2. **Write analysis script**: Create `scripts/analyze_*.mjs` to compute metrics
|
|
411
|
+
3. **Read TA dumps**: Check `dump/ta/math_15m/*.md` to see indicator state at signal time
|
|
412
|
+
4. **Cross-reference**: Use `signalId` to link storage state, TA snapshots, and JSONL events
|
|
413
|
+
5. **Save previous run**: Copy `dump/report/` to `dump_prev/report/` before next run for comparison
|
|
414
|
+
|
|
415
|
+
## Important Constraints
|
|
416
|
+
|
|
417
|
+
- **Never look-ahead**: `backtest-kit` enforces this via `AsyncLocalStorage`, but Pine Script logic must also avoid future data
|
|
418
|
+
- **Pine Script v5**: Use `@version=5` syntax. Supported built-ins: `ta.rsi`, `ta.ema`, `ta.sma`, `ta.macd`, `ta.dmi`, `ta.atr`, `ta.mom`, `ta.crossover`, `ta.crossunder`, etc.
|
|
419
|
+
- **SIGNAL_SCHEMA sync**: Every `plot()` in Pine that you want in JS must have a matching key in SIGNAL_SCHEMA. The plot title (2nd argument) must match the schema value exactly
|
|
420
|
+
- **Append-only JSONL**: Reports accumulate across runs. Clear `dump/report/` between experiments for clean data
|
|
421
|
+
- **ESM modules**: All `.mjs` files use ES module syntax (`import`/`export`)
|