tradelab 1.0.1 → 1.2.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 (66) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +188 -328
  3. package/bin/tradelab-mcp.js +7 -0
  4. package/bin/tradelab.js +29 -0
  5. package/dist/cjs/data.cjs +149 -26
  6. package/dist/cjs/index.cjs +1917 -1005
  7. package/dist/cjs/live.cjs +536 -25
  8. package/dist/cjs/ta.cjs +339 -0
  9. package/docs/README.md +32 -66
  10. package/docs/api-reference.md +283 -112
  11. package/docs/backtest-engine.md +210 -252
  12. package/docs/data-reporting-cli.md +114 -156
  13. package/docs/examples.md +6 -6
  14. package/docs/live-trading.md +263 -92
  15. package/docs/mcp.md +285 -0
  16. package/docs/research.md +157 -0
  17. package/examples/liveDashboard.js +33 -0
  18. package/examples/llmSignal.js +33 -0
  19. package/examples/mcpLiveTrading.js +77 -0
  20. package/examples/optimize.js +25 -0
  21. package/package.json +26 -4
  22. package/src/engine/asyncSignal.js +28 -0
  23. package/src/engine/backtest.js +13 -1
  24. package/src/engine/backtestAsync.js +27 -0
  25. package/src/engine/backtestTicks.js +13 -2
  26. package/src/engine/barSystemRunner.js +96 -41
  27. package/src/engine/execution.js +39 -0
  28. package/src/engine/grid.js +15 -0
  29. package/src/engine/llmSignal.js +84 -0
  30. package/src/engine/optimize.js +110 -0
  31. package/src/engine/optimizeWorker.js +67 -0
  32. package/src/engine/portfolio.js +4 -1
  33. package/src/engine/walkForward.js +1 -0
  34. package/src/index.js +9 -0
  35. package/src/live/dashboard/server.js +179 -0
  36. package/src/live/engine/liveEngine.js +2 -2
  37. package/src/live/engine/paperEngine.js +5 -0
  38. package/src/live/index.js +3 -0
  39. package/src/live/session.js +402 -0
  40. package/src/mcp/liveTools.js +179 -0
  41. package/src/mcp/schemas.js +167 -0
  42. package/src/mcp/server.js +35 -0
  43. package/src/mcp/tools.js +265 -0
  44. package/src/metrics/annualize.js +32 -0
  45. package/src/metrics/benchmark.js +55 -0
  46. package/src/metrics/buildMetrics.js +34 -13
  47. package/src/metrics/finite.js +17 -0
  48. package/src/research/combinations.js +18 -0
  49. package/src/research/cpcv.js +47 -0
  50. package/src/research/deflatedSharpe.js +35 -0
  51. package/src/research/index.js +6 -0
  52. package/src/research/monteCarlo.js +88 -0
  53. package/src/research/pbo.js +69 -0
  54. package/src/research/stats.js +78 -0
  55. package/src/strategies/builtins.js +96 -0
  56. package/src/strategies/index.js +30 -0
  57. package/src/ta/channels.js +67 -0
  58. package/src/ta/index.js +16 -0
  59. package/src/ta/oscillators.js +70 -0
  60. package/src/ta/trend.js +78 -0
  61. package/src/utils/random.js +33 -0
  62. package/templates/dashboard.html +661 -0
  63. package/types/index.d.ts +179 -0
  64. package/types/live.d.ts +114 -0
  65. package/types/mcp.d.ts +17 -0
  66. package/types/ta.d.ts +45 -0
@@ -1,38 +1,28 @@
1
- # Backtest engine
1
+ # Backtesting
2
2
 
3
- <small>[Back to main page](README.md)</small>
3
+ This guide covers the research engine: `backtest`, `backtestAsync`, `backtestTicks`, `backtestPortfolio`, `walkForwardOptimize`, `optimize`, and `buildMetrics`.
4
4
 
5
- This page covers the simulation layer:
5
+ [Back to docs](README.md)
6
6
 
7
- - `backtest(options)`
8
- - `backtestTicks(options)`
9
- - `backtestPortfolio(options)`
10
- - `walkForwardOptimize(options)`
11
- - `buildMetrics(input)`
7
+ ## Choose an Entry Point
12
8
 
13
- ## Overview
9
+ | Use this when... | Call |
10
+ | --------------------------------------- | ------------------------------ |
11
+ | You have candles for one symbol | `backtest(options)` |
12
+ | Your signal returns a promise | `backtestAsync(options)` |
13
+ | You have tick or quote data | `backtestTicks(options)` |
14
+ | You want one result across many systems | `backtestPortfolio(options)` |
15
+ | You want rolling train/test validation | `walkForwardOptimize(options)` |
16
+ | You want a worker-pool parameter sweep | `optimize(options)` |
17
+ | You already have trades and equity data | `buildMetrics(input)` |
14
18
 
15
- Use the engine layer when you already have candles and want to simulate strategy behavior, inspect the result, and export or post-process it.
19
+ ## Candle Shape
16
20
 
17
- The same `signal()` contract is used by `LiveEngine` in `tradelab/live`, so strategy logic can move from research to execution without rewriting signal inputs.
18
-
19
- ## Choose the right function
20
-
21
- | Use case | Function |
22
- | ----------------------------------------- | ----------------------- |
23
- | One strategy on one candle series | `backtest()` |
24
- | One strategy on tick or quote data | `backtestTicks()` |
25
- | Multiple symbols with one combined result | `backtestPortfolio()` |
26
- | Rolling or anchored train/test validation | `walkForwardOptimize()` |
27
- | Recompute metrics from realized trades | `buildMetrics()` |
28
-
29
- ## Candle input
30
-
31
- Candles should be sorted in ascending time order.
21
+ Candles should be sorted oldest to newest.
32
22
 
33
23
  ```js
34
24
  {
35
- time: 1735828200000,
25
+ time: 1735828200000, // Unix milliseconds
36
26
  open: 100,
37
27
  high: 102,
38
28
  low: 99,
@@ -41,117 +31,116 @@ Candles should be sorted in ascending time order.
41
31
  }
42
32
  ```
43
33
 
44
- The package also normalizes common aliases such as `timestamp`, `date`, `o`, `h`, `l`, and `c`.
45
-
46
- ## `backtest(options)`
47
-
48
- `backtest()` is the main single-symbol entry point.
34
+ The data loaders normalize common aliases such as `timestamp`, `date`, `o`, `h`, `l`, and `c`.
49
35
 
50
- ### Minimal example
36
+ ## First Backtest
51
37
 
52
38
  ```js
53
39
  import { backtest } from "tradelab";
54
40
 
55
41
  const result = backtest({
56
42
  candles,
57
- signal({ bar, index }) {
58
- if (index !== 20) return null;
43
+ symbol: "SPY",
44
+ interval: "1d",
45
+ equity: 10_000,
46
+ riskPct: 1,
47
+ warmupBars: 50,
48
+ signal({ bar, index, openPosition }) {
49
+ if (openPosition || index < 50) return null;
59
50
  return {
60
51
  side: "long",
61
- entry: bar.close,
62
- stop: bar.close - 2,
52
+ stop: bar.close * 0.97,
63
53
  rr: 2,
64
54
  };
65
55
  },
66
56
  });
67
- ```
68
-
69
- ### Required fields
70
57
 
71
- ```js
72
- {
73
- candles: Candle[],
74
- signal: ({ candles, index, bar, equity, openPosition, pendingOrder }) => Signal | null
75
- }
58
+ console.log(result.metrics);
76
59
  ```
77
60
 
78
- ### Core options
61
+ Set these options first:
79
62
 
80
- | Option | Purpose |
81
- | ---------------------------------- | -------------------------------------------------------------- |
82
- | `symbol`, `interval`, `range` | Labels carried into results and exports |
83
- | `equity` | Starting equity, default `10000` |
84
- | `riskPct` or `riskFraction` | Default risk per trade when `qty` is not provided |
85
- | `warmupBars` | Bars skipped before signal evaluation starts |
86
- | `flattenAtClose` | Forces end-of-day exit when enabled |
87
- | `collectEqSeries`, `collectReplay` | Builds extra output for charts and exports |
88
- | `strict` | Throws on direct lookahead access such as `candles[index + 1]` |
89
- | `costs` | Slippage, spread, and commission model |
63
+ | Option | Why it matters |
64
+ | ------------ | --------------------------------------------- |
65
+ | `symbol` | Labels results and exports |
66
+ | `interval` | Annualizes metrics correctly |
67
+ | `equity` | Starting account value |
68
+ | `riskPct` | Default risk per trade when `qty` is absent |
69
+ | `warmupBars` | Prevents indicators from trading before ready |
70
+ | `costs` | Keeps edge estimates from ignoring friction |
90
71
 
91
- If you are starting from scratch, the most useful options to set explicitly are:
72
+ ## Signal Contract
92
73
 
93
- - `equity`
94
- - `riskPct`
95
- - `warmupBars`
96
- - `flattenAtClose`
97
- - `costs`
74
+ Every engine calls your strategy with the same shape:
98
75
 
99
- ### Signal contract
76
+ ```js
77
+ signal({ candles, index, bar, equity, openPosition, pendingOrder });
78
+ ```
100
79
 
101
- The signal function receives a context object with these fields:
80
+ Return `null` to skip the bar. Return a signal object to enter.
102
81
 
103
- <!-- prettier-ignore -->
104
82
  ```js
105
- { candles, index, bar, equity, openPosition, pendingOrder }
83
+ {
84
+ side: "long",
85
+ entry: 101.25,
86
+ stop: 99.75,
87
+ takeProfit: 104.25
88
+ }
106
89
  ```
107
90
 
108
- Return `null` for no trade, or a signal object:
91
+ You can omit `entry`; the engine uses the current close. You can also omit `takeProfit` when you provide `rr`.
109
92
 
110
93
  ```js
111
- // minimal
112
- { side: "long", stop: 99.75, rr: 2 }
113
-
114
- // explicit targets
115
- { side: "long", entry: 101.25, stop: 99.75, takeProfit: 104.25 }
94
+ { side: "short", stop: 105, rr: 2 }
116
95
  ```
117
96
 
118
- ### Signal conveniences
119
-
120
- | Field | Behavior |
121
- | --------------------------- | ------------------------------------------------ |
122
- | `side` | Accepts `long`, `short`, `buy`, or `sell` |
123
- | `entry` | Defaults to the current close if omitted |
124
- | `takeProfit` | Can be derived from `rr` or `_rr` |
125
- | `qty` or `size` | Overrides risk-based sizing |
126
- | `riskPct` or `riskFraction` | Overrides the global risk setting for that trade |
97
+ Useful signal fields:
127
98
 
128
- Practical rule: return the smallest signal object that expresses the trade clearly. In many strategies that is just `side`, `stop`, and `rr`.
99
+ | Field | Meaning |
100
+ | ---------------------------- | --------------------------------- |
101
+ | `side` | `long`, `short`, `buy`, or `sell` |
102
+ | `entry`, `limit`, `price` | Entry price aliases |
103
+ | `stop`, `stopLoss`, `sl` | Stop price aliases |
104
+ | `takeProfit`, `target`, `tp` | Target price aliases |
105
+ | `rr` or `_rr` | Target in R multiples |
106
+ | `qty` or `size` | Fixed position size |
107
+ | `riskPct` or `riskFraction` | Per-trade risk override |
129
108
 
130
- ### Optional per-trade hints
109
+ ## Async Signals
131
110
 
132
- These values are read from the signal object when present:
111
+ Use `backtestAsync()` when your signal waits on a model, service, file read, or other async work.
133
112
 
134
- - `_entryExpiryBars`
135
- - `_cooldownBars`
136
- - `_breakevenAtR`
137
- - `_trailAfterR`
138
- - `_maxBarsInTrade`
139
- - `_maxHoldMin`
140
- - `_rr`
141
- - `_initRisk`
142
- - `_imb`
113
+ ```js
114
+ import { backtestAsync, LlmSignal } from "tradelab";
115
+
116
+ const modelSignal = new LlmSignal({
117
+ budgetMs: 2000,
118
+ onError: "skip",
119
+ async resolve({ candles, bar }) {
120
+ const recent = candles.slice(-10);
121
+ return recent.at(-1).close > recent[0].close
122
+ ? { side: "long", stop: bar.close * 0.98, rr: 2 }
123
+ : null;
124
+ },
125
+ });
143
126
 
144
- ### Execution and cost model
127
+ const result = await backtestAsync({
128
+ candles,
129
+ signal: modelSignal.signal,
130
+ signalBudgetMs: 3000,
131
+ });
132
+ ```
145
133
 
146
- Legacy options still work:
134
+ `LlmSignal` caches one decision per bar, blocks lookahead access, records decisions in `log`, and can either skip or throw on errors.
147
135
 
148
- - `slippageBps`
149
- - `feeBps`
136
+ ## Costs
150
137
 
151
- For more control, use `costs`:
138
+ The old top-level `slippageBps` and `feeBps` options still work. Prefer `costs` for new work:
152
139
 
153
140
  ```js
154
- {
141
+ const result = backtest({
142
+ candles,
143
+ signal,
155
144
  costs: {
156
145
  slippageBps: 2,
157
146
  spreadBps: 1,
@@ -161,217 +150,186 @@ For more control, use `costs`:
161
150
  stop: 4,
162
151
  },
163
152
  commissionBps: 1,
164
- commissionPerUnit: 0,
165
153
  commissionPerOrder: 1,
166
154
  minCommission: 1,
155
+ carry: {
156
+ longAnnualBps: 500,
157
+ shortAnnualBps: 800,
158
+ },
159
+ funding: {
160
+ rateBps: 10,
161
+ intervalMs: 8 * 60 * 60 * 1000,
162
+ anchorMs: 0,
163
+ },
167
164
  },
168
- }
165
+ });
169
166
  ```
170
167
 
171
- ### Cost model behavior
172
-
173
- - slippage is applied in trade direction
174
- - spread is modeled as half-spread paid on entry and exit
175
- - commission can be percentage-based, per-unit, per-order, or mixed
176
- - `minCommission` floors the fee for that fill
177
-
178
- This is still a bar-based simulation. It does not model queue position, exchange microstructure, or realistic intrabar order priority.
168
+ How the cost model works:
179
169
 
180
- ### Advanced trade management
170
+ - slippage is applied in the trade direction
171
+ - spread is paid as half-spread on entry and exit
172
+ - commission can be bps-based, per-unit, per-order, or mixed
173
+ - `minCommission` applies per fill
174
+ - carry is annualized and deducted when a leg closes
175
+ - funding applies at boundaries in `(openTime, closeTime]`
176
+ - positive funding charges longs and credits shorts
181
177
 
182
- These are optional. Ignore them until the strategy actually needs them.
178
+ Closed trades include `exit.financing` when carry or funding applies. It is already included in `exit.pnl`.
183
179
 
184
- - `scaleOutAtR`, `scaleOutFrac`, `finalTP_R`
185
- - `maxDailyLossPct`, `dailyMaxTrades`, `postLossCooldownBars`
186
- - `atrTrailMult`, `atrTrailPeriod`
187
- - `mfeTrail`
188
- - `pyramiding`
189
- - `volScale`
190
- - `entryChase`
191
- - `qtyStep`, `minQty`, `maxLeverage`
192
- - `reanchorStopOnFill`, `maxSlipROnFill`
193
- - `oco`
194
- - `triggerMode`
180
+ ## Result Shape
195
181
 
196
- Recommended order of adoption:
197
-
198
- 1. Start with `entry`, `stop`, and `rr`
199
- 2. Add `costs`
200
- 3. Add trailing, scale-outs, or pyramiding only if the real strategy uses them
201
-
202
- ## Result shape
203
-
204
- `backtest()` returns an object with these fields:
205
-
206
- <!-- prettier-ignore -->
207
182
  ```js
208
- { symbol, interval, range, trades, positions, openPositions, metrics, eqSeries, replay }
183
+ {
184
+ (symbol,
185
+ interval,
186
+ range,
187
+ trades, // every realized leg
188
+ positions, // completed positions
189
+ openPositions, // still open at the end
190
+ metrics,
191
+ eqSeries,
192
+ replay);
193
+ }
209
194
  ```
210
195
 
211
- ### `trades`
212
-
213
- Every realized leg, including partial exits and scale-outs.
214
-
215
- ### `positions`
216
-
217
- Completed positions only. This is the collection most users want for top-line analysis.
218
-
219
- If you are unsure whether to use `trades` or `positions`, start with `positions`.
220
-
221
- ### `metrics`
196
+ Use `positions` for normal trade analysis. Use `trades` when you need partial exits or scale-out legs.
222
197
 
223
- Most users start with:
198
+ Important metrics:
224
199
 
225
- - `trades`
226
- - `winRate`
227
- - `expectancy`
228
- - `profitFactor`
229
- - `maxDrawdown`
230
- - `sharpe`
231
- - `avgHold`
232
- - `returnPct`
233
- - `totalPnL`
234
- - `finalEquity`
235
- - `sideBreakdown`
200
+ | Metric | Meaning |
201
+ | ---------------------- | ----------------------------------------------------------------------------------- |
202
+ | `trades` | Number of completed positions |
203
+ | `winRate` | Winning completed positions / all positions |
204
+ | `profitFactor` | Gross profit / gross loss |
205
+ | `totalPnL` | Realized PnL |
206
+ | `returnPct` | Return on starting equity |
207
+ | `maxDrawdown` | Max drawdown as a decimal |
208
+ | `sharpeDaily` | Daily-bucketed Sharpe |
209
+ | `sharpeAnnualized` | Annualized Sharpe |
210
+ | `annualizationPeriods` | Periods used for annualization |
211
+ | `sideBreakdown` | Long and short side summaries |
212
+ | `benchmark` | Alpha, beta, correlation, and information ratio when benchmark returns are supplied |
236
213
 
237
- Also included:
214
+ Ratios are clamped to finite numbers before returning, so exported JSON does not contain `Infinity` or `NaN`.
238
215
 
239
- - position-vs-leg variants such as `profitFactor_pos` and `profitFactor_leg`
240
- - `rDist` percentiles
241
- - `holdDistMin` percentiles
242
- - daily stats under `daily`
216
+ ## Tick Backtests
243
217
 
244
- Useful first checks after any run:
245
-
246
- - `metrics.trades`: enough sample size to care
247
- - `metrics.profitFactor`: whether winners beat losers gross of the chosen fill model
248
- - `metrics.maxDrawdown`: whether the path is survivable
249
- - `metrics.sideBreakdown`: whether one side carries the result
250
-
251
- ### `eqSeries`
252
-
253
- Realized equity points:
218
+ `backtestTicks()` uses event-style tick or quote rows:
254
219
 
255
220
  ```js
256
- [{ time, timestamp, equity }];
221
+ const result = backtestTicks({
222
+ ticks,
223
+ symbol: "BTC-USD",
224
+ signal,
225
+ queueFillProbability: 0.4,
226
+ seed: "btc-run-1",
227
+ });
257
228
  ```
258
229
 
259
- `time` and `timestamp` contain the same Unix-millisecond value.
260
-
261
- ### `replay`
262
-
263
- Visualization payload:
264
-
265
- ```js
266
- {
267
- frames: [{ t, price, equity, posSide, posSize }],
268
- events: [{ t, price, type, side, size, tradeId, reason, pnl }]
269
- }
270
- ```
230
+ Supported tick fields include `time`, `price`, `last`, `bid`, `ask`, `high`, `low`, `size`, and `volume`. Market entries fill on the next tick. Limit orders can fill at touch based on `queueFillProbability`. Stops use the stop-specific slippage model when provided.
271
231
 
272
- This is meant for charts and reports, not as a full audit log.
232
+ Use `seed` to make probabilistic queue fills reproducible.
273
233
 
274
- ## `backtestPortfolio(options)`
234
+ ## Portfolio Backtests
275
235
 
276
- Use portfolio mode when you already have one candle array per symbol and want one combined result.
236
+ `backtestPortfolio()` runs multiple systems against one shared account.
277
237
 
278
238
  ```js
279
239
  const result = backtestPortfolio({
280
240
  equity: 100_000,
241
+ interval: "1d",
242
+ maxDailyLossPct: 3,
281
243
  systems: [
282
- { symbol: "SPY", candles: spy, signal: signalA, weight: 2 },
283
- { symbol: "QQQ", candles: qqq, signal: signalB, weight: 1 },
244
+ { symbol: "SPY", candles: spy, signal: spySignal, weight: 2 },
245
+ { symbol: "QQQ", candles: qqq, signal: qqqSignal, weight: 1 },
284
246
  ],
285
247
  });
286
248
  ```
287
249
 
288
- ### How it works
289
-
290
- - systems share one live capital pool
291
- - `weight` or `allocation: "equal" | "weight"` defines the default per-system cap, not a pre-funded sleeve
292
- - fills lock capital immediately, later fills size against remaining available capital
293
- - `eqSeries` points include `lockedCapital` and `availableCapital`
294
- - `maxDailyLossPct` can halt all systems for the rest of the day once breached
295
-
296
- ### What it is not
297
-
298
- - a cross-margin broker simulator
299
- - a prime-broker margin model
300
- - a full portfolio optimizer
301
-
302
- This mode now does enforce shared capital and cross-system sizing, but it still uses the library's research-oriented execution assumptions rather than full broker accounting.
303
-
304
- ## `backtestTicks(options)`
305
-
306
- Use tick mode when you want event-driven fills while keeping the same result shape as `backtest()`.
307
-
308
- ```js
309
- const result = backtestTicks({
310
- ticks,
311
- queueFillProbability: 0.5,
312
- signal,
313
- });
314
- ```
250
+ Weights are default allocation caps, not pre-funded sleeves. Capital is locked when a fill happens, and later fills size against what remains available.
315
251
 
316
- ### How it works
252
+ Portfolio result extras:
317
253
 
318
- - market entries fill on the next tick
319
- - limit orders can fill at the touch based on `queueFillProbability`
320
- - stop exits fill at the stop and use the normal stop slippage model from `costs.slippageByKind.stop`
321
- - results still come back as `trades`, `positions`, `metrics`, `eqSeries`, and `replay`
254
+ - `systems`: per-system backtest results
255
+ - `eqSeries[].lockedCapital`
256
+ - `eqSeries[].availableCapital`
257
+ - portfolio-level `metrics`
322
258
 
323
- ## `walkForwardOptimize(options)`
259
+ ## Walk-Forward Validation
324
260
 
325
- Use walk-forward mode when one in-sample backtest is not enough and you want rolling or anchored train/test validation.
261
+ Use `walkForwardOptimize()` to reduce the risk of choosing parameters that only worked in-sample.
326
262
 
327
263
  ```js
328
264
  const wf = walkForwardOptimize({
329
265
  candles,
330
- mode: "anchored",
331
266
  trainBars: 180,
332
267
  testBars: 60,
333
268
  stepBars: 60,
269
+ mode: "anchored",
334
270
  scoreBy: "profitFactor",
335
- parameterSets: [
336
- { fast: 8, slow: 21, rr: 2 },
337
- { fast: 10, slow: 30, rr: 2 },
338
- ],
271
+ parameterSets,
339
272
  signalFactory(params) {
340
- return createSignalFromParams(params);
273
+ return createSignal(params);
341
274
  },
342
275
  });
343
276
  ```
344
277
 
345
- ### How it works
278
+ For each window, tradelab:
279
+
280
+ 1. scores every parameter set on the training slice
281
+ 2. chooses the winner
282
+ 3. runs that winner on the test slice
283
+ 4. reports aggregate metrics and winner stability
284
+
285
+ Read `wf.windows` before trusting `wf.metrics`. A strategy that changes winners every window is less convincing than one with stable winners.
286
+
287
+ ## Parallel Parameter Sweeps
346
288
 
347
- 1. Evaluate every parameter set on the training slice
348
- 2. Pick the best one by `scoreBy`
349
- 3. Run that parameter set on the next test slice
350
- 4. Repeat for each window using either rolling or anchored training windows
289
+ `optimize()` runs independent parameter sets in worker threads.
351
290
 
352
- ### Return value
291
+ ```js
292
+ import { optimize, grid } from "tradelab";
353
293
 
354
- - `windows`: per-window summaries and chosen parameters
355
- - `trades`, `positions`, `metrics`, `eqSeries`
356
- - `bestParams`: chosen parameters for each window plus a stability summary
357
- - `bestParamsSummary`: adjacent repeat rate, dominant winner, and winner leaderboard
294
+ const out = await optimize({
295
+ candles,
296
+ interval: "1d",
297
+ signalModulePath: new URL("../strategies/ema.js", import.meta.url).pathname,
298
+ parameterSets: grid({
299
+ fast: [8, 10, 12],
300
+ slow: [30, 50],
301
+ }),
302
+ concurrency: 4,
303
+ scoreBy: "sharpeAnnualized",
304
+ });
305
+ ```
358
306
 
359
- In practice, the per-window output matters more than the aggregate headline. If the winning parameters swing wildly from one window to the next, treat that as a real signal.
307
+ The strategy module should export `createSignal(params)`.
360
308
 
361
- ## `buildMetrics(input)`
309
+ ```js
310
+ export function createSignal(params) {
311
+ return function signal(context) {
312
+ // return null or a trade signal
313
+ };
314
+ }
315
+ ```
362
316
 
363
- Most users do not need this directly. Use it when:
317
+ Functions cannot cross worker boundaries, so the worker receives a module path plus JSON-like parameter objects.
364
318
 
365
- - you generate realized trades outside `backtest()`
366
- - you filter a result and want fresh metrics
367
- - you combine results manually
319
+ ## Recomputing Metrics
368
320
 
369
- ## Common mistakes
321
+ Use `buildMetrics()` if you have realized trades and an equity curve from another process.
370
322
 
371
- - using unsorted candles or mixed intervals in one series
372
- - reading `trades` as if they were always full positions
373
- - leaving costs at zero and overestimating edge
374
- - trusting one backtest without out-of-sample validation
375
- - debugging a strategy with `strict: false` when lookahead is possible
323
+ ```js
324
+ const metrics = buildMetrics({
325
+ closed: trades,
326
+ equityStart: 10_000,
327
+ equityFinal: 11_250,
328
+ candles,
329
+ estBarMs: 86_400_000,
330
+ eqSeries,
331
+ interval: "1d",
332
+ });
333
+ ```
376
334
 
377
- <small>[Back to main page](README.md)</small>
335
+ [Back to docs](README.md)