tradelab 1.0.0 → 1.1.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 (67) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +75 -12
  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 +1893 -1003
  7. package/dist/cjs/live.cjs +134 -25
  8. package/dist/cjs/ta.cjs +339 -0
  9. package/docs/api-reference.md +46 -0
  10. package/docs/backtest-engine.md +112 -0
  11. package/docs/live-trading.md +51 -0
  12. package/docs/mcp.md +64 -0
  13. package/docs/research.md +103 -0
  14. package/docs/superpowers/plans/2026-00-overview.md +101 -0
  15. package/docs/superpowers/plans/2026-01-metrics-correctness.md +873 -0
  16. package/docs/superpowers/plans/2026-02-indicator-library.md +677 -0
  17. package/docs/superpowers/plans/2026-03-overfitting-toolkit.md +882 -0
  18. package/docs/superpowers/plans/2026-04-async-signals-seeding.md +981 -0
  19. package/docs/superpowers/plans/2026-05-mcp-server.md +758 -0
  20. package/docs/superpowers/plans/2026-06-parallel-param-sweep.md +508 -0
  21. package/docs/superpowers/plans/2026-07-funding-carry-costs.md +535 -0
  22. package/docs/superpowers/plans/2026-08-live-dashboard.md +547 -0
  23. package/docs/superpowers/plans/HANDOFF.md +88 -0
  24. package/examples/liveDashboard.js +33 -0
  25. package/examples/llmSignal.js +33 -0
  26. package/examples/optimize.js +25 -0
  27. package/package.json +16 -2
  28. package/src/engine/asyncSignal.js +28 -0
  29. package/src/engine/backtest.js +13 -1
  30. package/src/engine/backtestAsync.js +27 -0
  31. package/src/engine/backtestTicks.js +13 -2
  32. package/src/engine/barSystemRunner.js +96 -41
  33. package/src/engine/execution.js +39 -0
  34. package/src/engine/grid.js +15 -0
  35. package/src/engine/llmSignal.js +84 -0
  36. package/src/engine/optimize.js +86 -0
  37. package/src/engine/optimizeWorker.js +67 -0
  38. package/src/engine/walkForward.js +1 -0
  39. package/src/index.js +9 -0
  40. package/src/live/dashboard/server.js +120 -0
  41. package/src/live/engine/liveEngine.js +2 -2
  42. package/src/live/index.js +1 -0
  43. package/src/mcp/schemas.js +48 -0
  44. package/src/mcp/server.js +31 -0
  45. package/src/mcp/tools.js +142 -0
  46. package/src/metrics/annualize.js +32 -0
  47. package/src/metrics/benchmark.js +55 -0
  48. package/src/metrics/buildMetrics.js +34 -13
  49. package/src/metrics/finite.js +17 -0
  50. package/src/research/combinations.js +18 -0
  51. package/src/research/cpcv.js +47 -0
  52. package/src/research/deflatedSharpe.js +35 -0
  53. package/src/research/index.js +6 -0
  54. package/src/research/monteCarlo.js +88 -0
  55. package/src/research/pbo.js +69 -0
  56. package/src/research/stats.js +78 -0
  57. package/src/strategies/builtins.js +96 -0
  58. package/src/strategies/index.js +30 -0
  59. package/src/ta/channels.js +67 -0
  60. package/src/ta/index.js +16 -0
  61. package/src/ta/oscillators.js +70 -0
  62. package/src/ta/trend.js +78 -0
  63. package/src/utils/random.js +33 -0
  64. package/templates/dashboard.html +174 -0
  65. package/types/index.d.ts +154 -0
  66. package/types/live.d.ts +15 -0
  67. package/types/ta.d.ts +45 -0
@@ -5,6 +5,7 @@
5
5
  This page covers the simulation layer:
6
6
 
7
7
  - `backtest(options)`
8
+ - `backtestAsync(options)`
8
9
  - `backtestTicks(options)`
9
10
  - `backtestPortfolio(options)`
10
11
  - `walkForwardOptimize(options)`
@@ -21,6 +22,7 @@ The same `signal()` contract is used by `LiveEngine` in `tradelab/live`, so stra
21
22
  | Use case | Function |
22
23
  | ----------------------------------------- | ----------------------- |
23
24
  | One strategy on one candle series | `backtest()` |
25
+ | Async or model-backed candle signal | `backtestAsync()` |
24
26
  | One strategy on tick or quote data | `backtestTicks()` |
25
27
  | Multiple symbols with one combined result | `backtestPortfolio()` |
26
28
  | Rolling or anchored train/test validation | `walkForwardOptimize()` |
@@ -127,6 +129,43 @@ Return `null` for no trade, or a signal object:
127
129
 
128
130
  Practical rule: return the smallest signal object that expresses the trade clearly. In many strategies that is just `side`, `stop`, and `rr`.
129
131
 
132
+ ## Async signals
133
+
134
+ Use `backtestAsync()` when `signal()` returns a promise, such as an LLM call, agent decision, remote service lookup, or any async feature computation.
135
+
136
+ ```js
137
+ import { backtestAsync, LlmSignal } from "tradelab";
138
+
139
+ const llm = new LlmSignal({
140
+ budgetMs: 2000,
141
+ onError: "skip",
142
+ async resolve({ candles, bar }) {
143
+ const recent = candles.slice(-5);
144
+ return recent.every((c, i) => i === 0 || c.close >= recent[i - 1].close)
145
+ ? { side: "long", stop: bar.close * 0.98, rr: 2 }
146
+ : null;
147
+ },
148
+ });
149
+
150
+ const result = await backtestAsync({
151
+ candles,
152
+ signal: llm.signal,
153
+ signalBudgetMs: 3000,
154
+ });
155
+ ```
156
+
157
+ `backtestAsync()` returns the same result shape as `backtest()`. `signalBudgetMs` races each signal call against a per-bar deadline; set it to `0` or omit it to disable the timeout.
158
+
159
+ `LlmSignal` is an optional wrapper for model-backed decisions:
160
+
161
+ - caches by bar time, so repeated calls for one bar reuse the same decision
162
+ - passes a no-lookahead candle view into `resolve()`
163
+ - enforces `budgetMs` with the same timeout primitive as `backtestAsync()`
164
+ - records each result or error in `llm.log`
165
+ - returns `null` on errors by default, or rethrows with `onError: "throw"`
166
+
167
+ Live trading also awaits async signals; see [live trading](live-trading.md).
168
+
130
169
  ### Optional per-trade hints
131
170
 
132
171
  These values are read from the signal object when present:
@@ -164,6 +203,15 @@ For more control, use `costs`:
164
203
  commissionPerUnit: 0,
165
204
  commissionPerOrder: 1,
166
205
  minCommission: 1,
206
+ carry: {
207
+ longAnnualBps: 500,
208
+ shortAnnualBps: 800,
209
+ },
210
+ funding: {
211
+ rateBps: 10,
212
+ intervalMs: 8 * 60 * 60 * 1000,
213
+ anchorMs: 0,
214
+ },
167
215
  },
168
216
  }
169
217
  ```
@@ -174,9 +222,13 @@ For more control, use `costs`:
174
222
  - spread is modeled as half-spread paid on entry and exit
175
223
  - commission can be percentage-based, per-unit, per-order, or mixed
176
224
  - `minCommission` floors the fee for that fill
225
+ - `carry.longAnnualBps` and `carry.shortAnnualBps` are annualized financing or borrow rates deducted when each leg closes
226
+ - `funding.rateBps` applies once per funding boundary in `(openTime, closeTime]`; positive rates charge longs and credit shorts
177
227
 
178
228
  This is still a bar-based simulation. It does not model queue position, exchange microstructure, or realistic intrabar order priority.
179
229
 
230
+ Closed trades expose the time-based charge as `trade.exit.financing`. It is already included in `trade.exit.pnl` and aggregate metrics, so use it only when you need attribution.
231
+
180
232
  ### Advanced trade management
181
233
 
182
234
  These are optional. Ignore them until the strategy actually needs them.
@@ -248,6 +300,18 @@ Useful first checks after any run:
248
300
  - `metrics.maxDrawdown`: whether the path is survivable
249
301
  - `metrics.sideBreakdown`: whether one side carries the result
250
302
 
303
+ ### Risk-adjusted metrics
304
+
305
+ - `sharpe` / `sortino` are per-period (daily-bucketed).
306
+ - `sharpeAnnualized` / `sortinoAnnualized` scale by `sqrt(annualizationPeriods)`,
307
+ where `annualizationPeriods` is derived from `interval` (falling back to the
308
+ median bar spacing). Use these to compare strategies across timeframes.
309
+ - `profitFactor`, `calmar`, and the Sharpe/Sortino family are clamped to a finite
310
+ `BIG_NUMBER` (1e9) so `metrics` JSON never contains `Infinity` or `NaN`.
311
+ - `benchmark` (`{ alpha, beta, correlation, informationRatio, trackingError }`)
312
+ is populated when you pass `benchmarkReturns` (per-day return array aligned to
313
+ the strategy's daily equity buckets) to `backtest()`.
314
+
251
315
  ### `eqSeries`
252
316
 
253
317
  Realized equity points:
@@ -309,6 +373,7 @@ Use tick mode when you want event-driven fills while keeping the same result sha
309
373
  const result = backtestTicks({
310
374
  ticks,
311
375
  queueFillProbability: 0.5,
376
+ seed: "experiment-42",
312
377
  signal,
313
378
  });
314
379
  ```
@@ -317,6 +382,7 @@ const result = backtestTicks({
317
382
 
318
383
  - market entries fill on the next tick
319
384
  - limit orders can fill at the touch based on `queueFillProbability`
385
+ - identical `seed` + data + options produce identical probabilistic limit-fill outcomes
320
386
  - stop exits fill at the stop and use the normal stop slippage model from `costs.slippageByKind.stop`
321
387
  - results still come back as `trades`, `positions`, `metrics`, `eqSeries`, and `replay`
322
388
 
@@ -358,6 +424,52 @@ const wf = walkForwardOptimize({
358
424
 
359
425
  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.
360
426
 
427
+ ## Optimization (parallel sweeps)
428
+
429
+ Use `optimize()` for large parameter sweeps that can run independently across a worker pool.
430
+
431
+ ```js
432
+ import path from "node:path";
433
+ import { optimize, grid } from "tradelab";
434
+
435
+ const out = await optimize({
436
+ candles,
437
+ interval: "1d",
438
+ signalModulePath: path.resolve("./strategies/emaSignal.js"),
439
+ parameterSets: grid({ fast: [5, 8, 10], slow: [20, 30, 50], rr: 2 }),
440
+ concurrency: 4,
441
+ scoreBy: "sharpeAnnualized",
442
+ });
443
+
444
+ console.log(out.best?.params, out.best?.metrics);
445
+ ```
446
+
447
+ `signalModulePath` must point to an ESM module that exports `createSignal(params)` or a default factory:
448
+
449
+ ```js
450
+ export function createSignal(params) {
451
+ return function signal(context) {
452
+ return null;
453
+ };
454
+ }
455
+ ```
456
+
457
+ Functions cannot cross the worker boundary, so the signal is passed as a module path plus JSON-like parameter objects. Candles are copied once per worker, not once per parameter set.
458
+
459
+ The return shape is:
460
+
461
+ ```js
462
+ {
463
+ (results, // original order, one entry per parameter set
464
+ leaderboard, // sorted descending by scoreBy
465
+ best); // leaderboard[0] or null
466
+ }
467
+ ```
468
+
469
+ Each result contains `{ params, metrics }` or `{ params, error }`. Worker IPC only returns compact ranking metrics, not trade logs or replay frames.
470
+
471
+ `optimize()` is ESM-only in this release because it starts an ESM `worker_threads` worker via `import.meta.url`. Use it from ESM code, for example `node examples/optimize.js`.
472
+
361
473
  ## `buildMetrics(input)`
362
474
 
363
475
  Most users do not need this directly. Use it when:
@@ -63,10 +63,35 @@ await engine.stop();
63
63
  Important behavior:
64
64
 
65
65
  - `signal()` is called with the same context shape as backtesting
66
+ - `signal()` may be async; `LiveEngine` awaits the decision before normalizing it
66
67
  - market and limit/stop order lifecycles are tracked through broker events
67
68
  - state is persisted after fills, order updates, and equity updates
68
69
  - `getStatus()` returns runtime and risk state for health checks
69
70
 
71
+ Async/model-backed signals can use `LlmSignal` from the main package:
72
+
73
+ ```js
74
+ import { LlmSignal } from "tradelab";
75
+
76
+ const llm = new LlmSignal({
77
+ budgetMs: 2000,
78
+ onError: "skip",
79
+ async resolve(context) {
80
+ // Call a model or agent here.
81
+ return null;
82
+ },
83
+ });
84
+
85
+ const engine = new LiveEngine({
86
+ symbol: "AAPL",
87
+ interval: "1m",
88
+ broker,
89
+ signal: llm.signal,
90
+ });
91
+ ```
92
+
93
+ `LlmSignal` caches one decision per bar, passes a no-lookahead candle view to `resolve()`, and records decisions in `llm.log`. Use `backtestAsync()` to test the same signal before running it live.
94
+
70
95
  ## `LiveOrchestrator` quick start
71
96
 
72
97
  ```js
@@ -97,6 +122,32 @@ Use orchestrator when multiple systems should share one broker/account context.
97
122
  | `tradelab paper` | Shortcut for `live` with paper broker mode |
98
123
  | `tradelab status` | Inspect persisted live state |
99
124
 
125
+ ## Live dashboard
126
+
127
+ Use `createDashboardServer()` to watch a running `LiveEngine` or `LiveOrchestrator` locally. The dashboard serves a static page over `node:http`, streams live events with Server-Sent Events at `/events`, and reads current state from `/state`.
128
+
129
+ ```js
130
+ import { createDashboardServer } from "tradelab/live";
131
+
132
+ const dashboard = createDashboardServer({ source: engine, port: 4317 });
133
+ const url = await dashboard.start();
134
+ console.log(`dashboard: ${url}`);
135
+
136
+ // Later, during shutdown:
137
+ await dashboard.close();
138
+ ```
139
+
140
+ The page shows equity, day PnL, open position, risk state, and a recent event tail for signals, fills, position changes, equity updates, and risk halts. New browser clients receive a bounded replay of recent events so the page is useful immediately after opening.
141
+
142
+ The CLI can start the same dashboard for both single-engine and config/orchestrator runs:
143
+
144
+ ```bash
145
+ tradelab paper --symbol AAPL --interval 1m --mode polling --dashboard --dashboardPort 4317
146
+ tradelab live --config ./live-portfolio.json --paper --dashboard --dashboardPort 4317
147
+ ```
148
+
149
+ The dashboard implementation is ESM-first. The CommonJS live bundle can be imported, but packaged dashboard usage should prefer `import { createDashboardServer } from "tradelab/live"`.
150
+
100
151
  ### Single-system paper run
101
152
 
102
153
  ```bash
package/docs/mcp.md ADDED
@@ -0,0 +1,64 @@
1
+ # MCP server
2
+
3
+ <small>[Back to main page](README.md)</small>
4
+
5
+ `tradelab-mcp` exposes the research loop to MCP-capable agents such as Claude Desktop, Cursor, and Claude Code.
6
+
7
+ ## Tools
8
+
9
+ | Tool | Purpose |
10
+ | ----------------- | ----------------------------------------------------------------------- |
11
+ | `list_strategies` | List built-in strategies and their tunable parameters |
12
+ | `fetch_candles` | Fetch Yahoo or CSV candles and return a compact first/last bar summary |
13
+ | `run_backtest` | Run a named strategy with JSON params and return compact metrics |
14
+ | `walk_forward` | Run a named strategy over a parameter grid and return stability metrics |
15
+
16
+ Tool outputs are summaries for agent context, not full report payloads. `run_backtest` returns metrics and a small trade preview, but not replay frames.
17
+
18
+ ## Agent research loop
19
+
20
+ 1. Call `list_strategies` to inspect available strategy names and parameters.
21
+ 2. Call `fetch_candles` or provide inline `candles`.
22
+ 3. Call `run_backtest` with a strategy name and params.
23
+ 4. Read `metrics`, especially trade count, profit factor, drawdown, and annualized Sharpe.
24
+ 5. Call `walk_forward` with a parameter grid to check out-of-sample stability.
25
+
26
+ ## Claude Desktop config
27
+
28
+ Use this with the published package:
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "tradelab": {
34
+ "command": "npx",
35
+ "args": ["-y", "tradelab", "tradelab-mcp"]
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ After installing globally with `npm install -g tradelab`, you can use:
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "tradelab": {
47
+ "command": "tradelab-mcp"
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ ## Strategies
54
+
55
+ Agents cannot pass JavaScript closures over MCP, so strategies are name-addressable. Built-ins currently include:
56
+
57
+ - `ema-cross`
58
+ - `rsi-reversion`
59
+ - `donchian-breakout`
60
+ - `buy-hold`
61
+
62
+ Register custom strategies in application code with `registerStrategy(name, def)` from the main package. A strategy definition includes `description`, `params`, and a `factory(params)` function that returns a normal tradelab `signal(context)`.
63
+
64
+ <small>[Back to main page](README.md)</small>
@@ -0,0 +1,103 @@
1
+ # Research & overfitting
2
+
3
+ <small>[Back to main page](README.md)</small>
4
+
5
+ The `research` namespace contains pure statistical helpers for checking whether a backtest is robust enough to take seriously.
6
+
7
+ ```js
8
+ import { backtest, research } from "tradelab";
9
+
10
+ const result = backtest({ candles, interval: "1d", signal });
11
+ const pnls = result.positions.map((p) => p.exit.pnl);
12
+
13
+ const mc = research.monteCarlo({ tradePnls: pnls, equityStart: 10_000, seed: 1 });
14
+ console.log("5% worst final equity:", mc.finalEquity.p5);
15
+
16
+ const dsr = research.deflatedSharpe({
17
+ sharpe: result.metrics.sharpeDaily,
18
+ sampleSize: result.metrics.trades,
19
+ numTrials: 20,
20
+ sharpeStd: 0.5,
21
+ skew: 0,
22
+ kurtosis: 3,
23
+ });
24
+ console.log("Deflated Sharpe prob:", dsr);
25
+ ```
26
+
27
+ ## `research.monteCarlo(options)`
28
+
29
+ Seeded block-bootstrap of trade PnLs.
30
+
31
+ ```js
32
+ research.monteCarlo({
33
+ tradePnls,
34
+ equityStart: 10_000,
35
+ iterations: 1000,
36
+ blockSize: 1,
37
+ seed: "run-1",
38
+ });
39
+ ```
40
+
41
+ Returns:
42
+
43
+ - `finalEquity`: `{ p5, p25, p50, p75, p95 }`
44
+ - `maxDrawdown`: `{ p5, p25, p50, p75, p95 }`
45
+ - `pathBands`: per-trade-step `{ p5, p50, p95 }` equity bands
46
+ - `probProfit`: fraction of simulations ending above starting equity
47
+
48
+ Use `blockSize > 1` when you want to preserve short streaks in the resampled trade sequence.
49
+
50
+ ## `research.deflatedSharpe(options)`
51
+
52
+ Returns a probability in `[0, 1]` that the observed Sharpe is real after accounting for finite sample size, non-normality, and multiple trials.
53
+
54
+ ```js
55
+ research.deflatedSharpe({
56
+ sharpe,
57
+ sampleSize,
58
+ numTrials,
59
+ sharpeStd,
60
+ skew,
61
+ kurtosis,
62
+ });
63
+ ```
64
+
65
+ Below roughly `0.95`, treat the Sharpe as not convincingly significant.
66
+
67
+ ## `research.sweepHaircut(options)`
68
+
69
+ Estimates the expected maximum Sharpe under the null when trying many strategy variants.
70
+
71
+ ```js
72
+ research.sweepHaircut({ numTrials: 50, sharpeStd: 0.4 });
73
+ ```
74
+
75
+ Use `expectedMaxSharpe` as the multiple-testing hurdle your selected strategy should clear.
76
+
77
+ ## `research.probabilityOfBacktestOverfitting(matrix, options)`
78
+
79
+ CSCV estimate of Probability of Backtest Overfitting.
80
+
81
+ ```js
82
+ const matrix = parameterSets.map((params) => returnsForParams(params));
83
+ const pbo = research.probabilityOfBacktestOverfitting(matrix, { groups: 8 });
84
+ ```
85
+
86
+ Rows are strategy variants or parameter sets. Columns are per-period returns. `pbo > 0.5` means the selection process is likely overfit; lower is better.
87
+
88
+ ## `research.combinatorialPurgedSplits(options)`
89
+
90
+ Creates CPCV train/test index splits with optional embargo.
91
+
92
+ ```js
93
+ const splits = research.combinatorialPurgedSplits({
94
+ nObservations: candles.length,
95
+ nGroups: 6,
96
+ nTestGroups: 2,
97
+ embargo: 3,
98
+ });
99
+ ```
100
+
101
+ Each split is `{ train, test, testGroups }`. Training observations near test blocks are purged by `embargo` observations to reduce leakage from overlapping or serially correlated samples.
102
+
103
+ <small>[Back to main page](README.md)</small>
@@ -0,0 +1,101 @@
1
+ # tradelab 2026 Roadmap — Overview & Sequencing
2
+
3
+ > **For agentic workers:** Each subsystem below has its own plan file. Use
4
+ > `superpowers:subagent-driven-development` (recommended) or
5
+ > `superpowers:executing-plans` to implement one plan at a time, task-by-task.
6
+ > All step lists use checkbox (`- [ ]`) syntax for tracking.
7
+
8
+ **Goal:** Turn tradelab from a strong single-run backtester into an AI-native,
9
+ statistically-defensible research + execution platform for quants, simple
10
+ traders, and autonomous agents.
11
+
12
+ ---
13
+
14
+ ## The 8 subsystems
15
+
16
+ | # | Plan file | What it delivers | Depends on |
17
+ | --- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------- |
18
+ | 1 | [2026-01-metrics-correctness.md](2026-01-metrics-correctness.md) | Annualized Sharpe/Sortino, finite-clamped metrics JSON, benchmark alpha/beta/IR | — |
19
+ | 2 | [2026-02-indicator-library.md](2026-02-indicator-library.md) | `tradelab/ta` namespace: RSI, MACD, Bollinger, VWAP, Supertrend, Donchian, Keltner, stochastics | — |
20
+ | 3 | [2026-03-overfitting-toolkit.md](2026-03-overfitting-toolkit.md) | CPCV, PBO, Deflated Sharpe, Monte Carlo bands, sweep haircut | 1 |
21
+ | 4 | [2026-04-async-signals-seeding.md](2026-04-async-signals-seeding.md) | `async signal()` with per-bar budget + cache + no-lookahead guard, `LlmSignal`, configurable RNG seed | — |
22
+ | 5 | [2026-05-mcp-server.md](2026-05-mcp-server.md) | `tradelab/mcp` server exposing data/backtest/walk-forward/metrics as agent tools | 1, 4 (soft) |
23
+ | 6 | [2026-06-parallel-param-sweep.md](2026-06-parallel-param-sweep.md) | Worker-pool param sweep + `optimize()` API | — |
24
+ | 7 | [2026-07-funding-carry-costs.md](2026-07-funding-carry-costs.md) | Funding/borrow/overnight carry in the cost model | — |
25
+ | 8 | [2026-08-live-dashboard.md](2026-08-live-dashboard.md) | Local realtime dashboard for `LiveEngine`/`LiveOrchestrator` | — |
26
+
27
+ ### Dependency graph
28
+
29
+ ```
30
+ 1 metrics ──► 3 overfitting
31
+ 1 metrics ──► 5 mcp (soft: nicer tool output)
32
+ 4 async ────► 5 mcp (soft: agent-driven backtests)
33
+ 2, 6, 7, 8 are independent
34
+ ```
35
+
36
+ ### Recommended execution order
37
+
38
+ 1. **Plan 1 (metrics)** — small, corrects existing bugs, unblocks 3 and 5.
39
+ 2. **Plan 2 (indicators)** — independent, high user value, unblocks NL strategies later.
40
+ 3. **Plan 4 (async signals + seed)** — engine change; do before MCP so agents can drive live.
41
+ 4. **Plan 5 (MCP)** — the 2026 headline; sits on top of 1 + 4.
42
+ 5. **Plan 3 (overfitting)** — the quant moat; needs clean metrics.
43
+ 6. **Plans 6, 7, 8** — parallelizable, any order.
44
+
45
+ ---
46
+
47
+ ## Shared conventions (all plans assume these)
48
+
49
+ **Runtime:** Node `>=18`, ESM (`"type": "module"`). No transpile step for `src/`.
50
+ The CJS build is generated by `npm run build` (esbuild) from `src/`.
51
+
52
+ **Tests:** `node:test` + `node:assert/strict`. Run a single file with:
53
+
54
+ ```bash
55
+ node --test test/<name>.test.js
56
+ ```
57
+
58
+ Run everything with `npm test` (`node --test`). New test files live under `test/`
59
+ mirroring `src/` layout (e.g. `src/metrics/finite.js` → `test/metrics/finite.test.js`).
60
+ There is no test runner config — discovery is by filename.
61
+
62
+ **Lint/format before commit:**
63
+
64
+ ```bash
65
+ npm run lint
66
+ npm run format:check
67
+ ```
68
+
69
+ **Commit style:** match existing history — `feat:`, `fix:`, `docs:`, `perf:`,
70
+ `test:`. Every commit message MUST end with the trailer:
71
+
72
+ ```
73
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
74
+ ```
75
+
76
+ **Public exports:** add new top-level exports in [src/index.js](../../../src/index.js).
77
+ New subpath entrypoints (`tradelab/ta`, `tradelab/mcp`) require an entry in the
78
+ `exports` map of [package.json](../../../package.json) AND a matching CJS bundle
79
+ in [scripts/build-cjs.mjs](../../../scripts/build-cjs.mjs).
80
+
81
+ **Canonical result shape (do not break it):** every engine returns
82
+ `{ symbol, interval, range, trades, positions, openPositions, metrics, eqSeries, replay }`.
83
+ `buildMetrics` is the single source of truth for `metrics`. Plans that add metrics
84
+ fields ADD keys; they never rename or remove existing ones (dashboards depend on them).
85
+
86
+ **The signal contract (do not break it):** `signal(context)` receives
87
+ `{ candles, index, bar, equity, openPosition, pendingOrder }` and returns `null`
88
+ or `{ side, entry?, stop, rr|takeProfit, ... }`. Two engines call it independently:
89
+ the standalone loop in [src/engine/backtest.js](../../../src/engine/backtest.js) and
90
+ the shared [src/engine/barSystemRunner.js](../../../src/engine/barSystemRunner.js)
91
+ (used by portfolio). The live path uses
92
+ [src/live/engine/liveEngine.js](../../../src/live/engine/liveEngine.js). Plan 4
93
+ touches all three call sites.
94
+
95
+ ---
96
+
97
+ ## Out of scope for this roadmap
98
+
99
+ - Natural-language → signal compiler (separate spec; depends on Plan 2 + a strategy schema).
100
+ - New broker adapters / options / fundamentals data sources.
101
+ - L2 microstructure simulator.