@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.
Files changed (40) hide show
  1. package/README.md +120 -13
  2. package/content/config/source/timeframe_15m.pine +114 -0
  3. package/content/config/source/timeframe_4h.pine +57 -0
  4. package/content/config/symbol.config.cjs +460 -0
  5. package/content/docker/ollama/docker-compose.yaml +34 -0
  6. package/content/docker/ollama/watch.sh +2 -0
  7. package/content/scripts/cache/cache_candles.mjs +47 -0
  8. package/content/scripts/cache/cache_model.mjs +42 -0
  9. package/content/scripts/cache/validate_candles.mjs +46 -0
  10. package/content/scripts/run_timeframe_15m.mjs +77 -0
  11. package/content/scripts/run_timeframe_4h.mjs +68 -0
  12. package/package.json +2 -2
  13. package/scripts/init.mjs +40 -9
  14. package/src/classes/BacktestLowerStopOnBreakevenAction.mjs +18 -0
  15. package/src/classes/BacktestPartialProfitTakingAction.mjs +5 -0
  16. package/src/classes/BacktestPositionMonitorAction.mjs +4 -0
  17. package/src/config/setup.mjs +27 -1
  18. package/src/enum/ActionName.mjs +1 -1
  19. package/src/enum/FrameName.mjs +1 -0
  20. package/src/enum/RiskName.mjs +1 -1
  21. package/src/logic/action/backtest_lower_stop_on_breakeven.action.mjs +9 -0
  22. package/src/logic/exchange/binance.exchange.mjs +12 -2
  23. package/src/logic/frame/feb_2024.frame.mjs +10 -0
  24. package/src/logic/index.mjs +3 -2
  25. package/src/logic/risk/sl_distance.risk.mjs +32 -0
  26. package/src/logic/risk/tp_distance.risk.mjs +5 -3
  27. package/src/logic/strategy/main.strategy.mjs +29 -12
  28. package/src/main/bootstrap.mjs +1 -1
  29. package/src/math/timeframe_15m.math.mjs +68 -0
  30. package/src/math/timeframe_4h.math.mjs +53 -0
  31. package/template/CLAUDE.mustache +421 -0
  32. package/template/README.mustache +232 -24
  33. package/template/env.mustache +1 -17
  34. package/template/jsconfig.json.mustache +1 -0
  35. package/template/package.mustache +8 -5
  36. package/src/classes/BacktestTightenStopOnBreakevenAction.mjs +0 -13
  37. package/src/func/market.func.mjs +0 -46
  38. package/src/logic/action/backtest_tighten_stop_on_breakeven.action.mjs +0 -9
  39. package/src/logic/risk/rr_ratio.risk.mjs +0 -39
  40. /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`)