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.
- package/CHANGELOG.md +66 -0
- package/README.md +75 -12
- package/bin/tradelab-mcp.js +7 -0
- package/bin/tradelab.js +29 -0
- package/dist/cjs/data.cjs +149 -26
- package/dist/cjs/index.cjs +1893 -1003
- package/dist/cjs/live.cjs +134 -25
- package/dist/cjs/ta.cjs +339 -0
- package/docs/api-reference.md +46 -0
- package/docs/backtest-engine.md +112 -0
- package/docs/live-trading.md +51 -0
- package/docs/mcp.md +64 -0
- package/docs/research.md +103 -0
- package/docs/superpowers/plans/2026-00-overview.md +101 -0
- package/docs/superpowers/plans/2026-01-metrics-correctness.md +873 -0
- package/docs/superpowers/plans/2026-02-indicator-library.md +677 -0
- package/docs/superpowers/plans/2026-03-overfitting-toolkit.md +882 -0
- package/docs/superpowers/plans/2026-04-async-signals-seeding.md +981 -0
- package/docs/superpowers/plans/2026-05-mcp-server.md +758 -0
- package/docs/superpowers/plans/2026-06-parallel-param-sweep.md +508 -0
- package/docs/superpowers/plans/2026-07-funding-carry-costs.md +535 -0
- package/docs/superpowers/plans/2026-08-live-dashboard.md +547 -0
- package/docs/superpowers/plans/HANDOFF.md +88 -0
- package/examples/liveDashboard.js +33 -0
- package/examples/llmSignal.js +33 -0
- package/examples/optimize.js +25 -0
- package/package.json +16 -2
- package/src/engine/asyncSignal.js +28 -0
- package/src/engine/backtest.js +13 -1
- package/src/engine/backtestAsync.js +27 -0
- package/src/engine/backtestTicks.js +13 -2
- package/src/engine/barSystemRunner.js +96 -41
- package/src/engine/execution.js +39 -0
- package/src/engine/grid.js +15 -0
- package/src/engine/llmSignal.js +84 -0
- package/src/engine/optimize.js +86 -0
- package/src/engine/optimizeWorker.js +67 -0
- package/src/engine/walkForward.js +1 -0
- package/src/index.js +9 -0
- package/src/live/dashboard/server.js +120 -0
- package/src/live/engine/liveEngine.js +2 -2
- package/src/live/index.js +1 -0
- package/src/mcp/schemas.js +48 -0
- package/src/mcp/server.js +31 -0
- package/src/mcp/tools.js +142 -0
- package/src/metrics/annualize.js +32 -0
- package/src/metrics/benchmark.js +55 -0
- package/src/metrics/buildMetrics.js +34 -13
- package/src/metrics/finite.js +17 -0
- package/src/research/combinations.js +18 -0
- package/src/research/cpcv.js +47 -0
- package/src/research/deflatedSharpe.js +35 -0
- package/src/research/index.js +6 -0
- package/src/research/monteCarlo.js +88 -0
- package/src/research/pbo.js +69 -0
- package/src/research/stats.js +78 -0
- package/src/strategies/builtins.js +96 -0
- package/src/strategies/index.js +30 -0
- package/src/ta/channels.js +67 -0
- package/src/ta/index.js +16 -0
- package/src/ta/oscillators.js +70 -0
- package/src/ta/trend.js +78 -0
- package/src/utils/random.js +33 -0
- package/templates/dashboard.html +174 -0
- package/types/index.d.ts +154 -0
- package/types/live.d.ts +15 -0
- package/types/ta.d.ts +45 -0
package/docs/backtest-engine.md
CHANGED
|
@@ -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:
|
package/docs/live-trading.md
CHANGED
|
@@ -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>
|
package/docs/research.md
ADDED
|
@@ -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.
|