tradelab 1.1.0 → 1.2.1

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 (39) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +183 -373
  3. package/dist/cjs/index.cjs +39 -12
  4. package/dist/cjs/live.cjs +457 -18
  5. package/docs/README.md +32 -66
  6. package/docs/api-reference.md +269 -144
  7. package/docs/backtest-engine.md +167 -321
  8. package/docs/data-reporting-cli.md +114 -156
  9. package/docs/examples.md +6 -6
  10. package/docs/live-trading.md +254 -134
  11. package/docs/mcp.md +244 -23
  12. package/docs/research.md +99 -45
  13. package/examples/mcpLiveTrading.js +77 -0
  14. package/package.json +11 -3
  15. package/src/engine/optimize.js +25 -1
  16. package/src/engine/portfolio.js +6 -2
  17. package/src/live/dashboard/server.js +67 -8
  18. package/src/live/engine/paperEngine.js +21 -11
  19. package/src/live/index.js +2 -0
  20. package/src/live/session.js +439 -0
  21. package/src/mcp/liveTools.js +202 -0
  22. package/src/mcp/schemas.js +119 -0
  23. package/src/mcp/server.js +5 -1
  24. package/src/mcp/tools.js +125 -2
  25. package/src/research/monteCarlo.js +6 -2
  26. package/templates/dashboard.html +595 -108
  27. package/types/index.d.ts +25 -0
  28. package/types/live.d.ts +102 -1
  29. package/types/mcp.d.ts +17 -0
  30. package/docs/superpowers/plans/2026-00-overview.md +0 -101
  31. package/docs/superpowers/plans/2026-01-metrics-correctness.md +0 -873
  32. package/docs/superpowers/plans/2026-02-indicator-library.md +0 -677
  33. package/docs/superpowers/plans/2026-03-overfitting-toolkit.md +0 -882
  34. package/docs/superpowers/plans/2026-04-async-signals-seeding.md +0 -981
  35. package/docs/superpowers/plans/2026-05-mcp-server.md +0 -758
  36. package/docs/superpowers/plans/2026-06-parallel-param-sweep.md +0 -508
  37. package/docs/superpowers/plans/2026-07-funding-carry-costs.md +0 -535
  38. package/docs/superpowers/plans/2026-08-live-dashboard.md +0 -547
  39. package/docs/superpowers/plans/HANDOFF.md +0 -88
@@ -1,508 +0,0 @@
1
- # Parallel Parameter Sweep Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Turn large parameter sweeps from serial-and-slow into parallel-and-fast with a `worker_threads` pool, exposed as `optimize()`, returning a scored leaderboard.
6
-
7
- **Architecture:** Functions can't cross the worker boundary, so the signal is supplied as a **module path** exporting `createSignal(params)` (each worker dynamically imports it once). The main thread owns a pool of N workers, streams parameter-set indices to idle workers, and collects metric summaries. Candles are passed once per worker via `workerData`. A `grid()` helper expands `{ key: value[] }` into parameter sets.
8
-
9
- **Tech Stack:** Node ESM `worker_threads` (built-in), `node:os`, `node:test`. No new dependencies. Independent of other plans (integrates with Plan 5's registry if present, but does not require it).
10
-
11
- ---
12
-
13
- ### Task 1: Grid expansion helper
14
-
15
- **Files:**
16
-
17
- - Create: `src/engine/grid.js`
18
- - Test: `test/engine/grid.test.js`
19
-
20
- - [ ] **Step 1: Write the failing test**
21
-
22
- ```js
23
- // test/engine/grid.test.js
24
- import test from "node:test";
25
- import assert from "node:assert/strict";
26
- import { grid } from "../../src/engine/grid.js";
27
-
28
- test("grid expands a cartesian product in stable order", () => {
29
- const sets = grid({ fast: [3, 5], slow: [8, 13] });
30
- assert.equal(sets.length, 4);
31
- assert.deepEqual(sets[0], { fast: 3, slow: 8 });
32
- assert.deepEqual(sets[3], { fast: 5, slow: 13 });
33
- });
34
-
35
- test("grid of an empty spec yields a single empty set", () => {
36
- assert.deepEqual(grid({}), [{}]);
37
- });
38
-
39
- test("grid passes scalar (non-array) values through as fixed", () => {
40
- const sets = grid({ fast: [3, 5], rr: 2 });
41
- assert.equal(sets.length, 2);
42
- assert.equal(sets[0].rr, 2);
43
- assert.equal(sets[1].rr, 2);
44
- });
45
- ```
46
-
47
- - [ ] **Step 2: Run to verify it fails**
48
-
49
- Run: `node --test test/engine/grid.test.js`
50
- Expected: FAIL — cannot find module.
51
-
52
- - [ ] **Step 3: Implement src/engine/grid.js**
53
-
54
- ```js
55
- // src/engine/grid.js
56
-
57
- /**
58
- * Expand a parameter grid into an array of parameter-set objects.
59
- * Array values are swept; scalar values are held fixed across all sets.
60
- * grid({ fast: [3,5], slow: [8,13], rr: 2 })
61
- * => [{fast:3,slow:8,rr:2}, {fast:3,slow:13,rr:2}, {fast:5,slow:8,rr:2}, {fast:5,slow:13,rr:2}]
62
- */
63
- export function grid(spec = {}) {
64
- const keys = Object.keys(spec);
65
- if (!keys.length) return [{}];
66
- return keys.reduce(
67
- (acc, key) => {
68
- const values = Array.isArray(spec[key]) ? spec[key] : [spec[key]];
69
- return acc.flatMap((base) => values.map((v) => ({ ...base, [key]: v })));
70
- },
71
- [{}]
72
- );
73
- }
74
- ```
75
-
76
- - [ ] **Step 4: Run to verify it passes**
77
-
78
- Run: `node --test test/engine/grid.test.js`
79
- Expected: PASS (3 tests).
80
-
81
- - [ ] **Step 5: Commit**
82
-
83
- ```bash
84
- git add src/engine/grid.js test/engine/grid.test.js
85
- git commit -m "feat: add parameter grid expansion helper
86
-
87
- Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
88
- ```
89
-
90
- ---
91
-
92
- ### Task 2: The worker
93
-
94
- **Files:**
95
-
96
- - Create: `src/engine/optimizeWorker.js`
97
- - Test: `test/fixtures/emaSignal.js` (fixture used by Task 3's test)
98
-
99
- - [ ] **Step 1: Create the test fixture (a serializable signal module)**
100
-
101
- ```js
102
- // test/fixtures/emaSignal.js
103
- import { ema } from "../../src/utils/indicators.js";
104
-
105
- export function createSignal({ fast = 10, slow = 30, rr = 2 } = {}) {
106
- return ({ candles, bar }) => {
107
- if (candles.length < slow + 2) return null;
108
- const closes = candles.map((c) => c.close);
109
- const f = ema(closes, fast);
110
- const s = ema(closes, slow);
111
- const i = closes.length - 1;
112
- if (f[i - 1] <= s[i - 1] && f[i] > s[i]) {
113
- const stop = Math.min(...candles.slice(-15).map((c) => c.low));
114
- if (stop >= bar.close) return null;
115
- return { side: "long", entry: bar.close, stop, rr };
116
- }
117
- return null;
118
- };
119
- }
120
- ```
121
-
122
- - [ ] **Step 2: Implement src/engine/optimizeWorker.js**
123
-
124
- ```js
125
- // src/engine/optimizeWorker.js
126
- import { workerData, parentPort } from "node:worker_threads";
127
- import { pathToFileURL } from "node:url";
128
- import { backtest } from "./backtest.js";
129
-
130
- const { candles, signalModulePath, interval, backtestOptions } = workerData;
131
-
132
- const mod = await import(pathToFileURL(signalModulePath).href);
133
- const createSignal = mod.createSignal ?? mod.default;
134
- if (typeof createSignal !== "function") {
135
- throw new Error(
136
- `optimize: ${signalModulePath} must export createSignal(params) or a default factory`
137
- );
138
- }
139
-
140
- // Only ship the metric fields a sweep ranks on — keep IPC payloads tiny.
141
- function pickMetrics(metrics) {
142
- const keep = [
143
- "trades",
144
- "winRate",
145
- "profitFactor",
146
- "expectancy",
147
- "totalR",
148
- "avgR",
149
- "sharpe",
150
- "sharpeAnnualized",
151
- "maxDrawdown",
152
- "calmar",
153
- "returnPct",
154
- "totalPnL",
155
- "finalEquity",
156
- ];
157
- const out = {};
158
- for (const k of keep) out[k] = metrics[k];
159
- return out;
160
- }
161
-
162
- parentPort.on("message", (msg) => {
163
- if (msg.type === "stop") {
164
- process.exit(0);
165
- }
166
- if (msg.type === "run") {
167
- try {
168
- const result = backtest({
169
- candles,
170
- interval,
171
- signal: createSignal(msg.params),
172
- collectReplay: false,
173
- collectEqSeries: false,
174
- ...backtestOptions,
175
- });
176
- parentPort.postMessage({
177
- type: "result",
178
- index: msg.index,
179
- params: msg.params,
180
- metrics: pickMetrics(result.metrics),
181
- });
182
- } catch (error) {
183
- parentPort.postMessage({
184
- type: "error",
185
- index: msg.index,
186
- params: msg.params,
187
- error: error instanceof Error ? error.message : String(error),
188
- });
189
- }
190
- }
191
- });
192
-
193
- parentPort.postMessage({ type: "ready" });
194
- ```
195
-
196
- - [ ] **Step 3: Commit**
197
-
198
- ```bash
199
- git add src/engine/optimizeWorker.js test/fixtures/emaSignal.js
200
- git commit -m "feat: add backtest sweep worker
201
-
202
- Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
203
- ```
204
-
205
- ---
206
-
207
- ### Task 3: The pool — `optimize()`
208
-
209
- **Files:**
210
-
211
- - Create: `src/engine/optimize.js`
212
- - Modify: `src/index.js`
213
- - Test: `test/optimize.test.js`
214
-
215
- - [ ] **Step 1: Write the failing test**
216
-
217
- ```js
218
- // test/optimize.test.js
219
- import test from "node:test";
220
- import assert from "node:assert/strict";
221
- import path from "node:path";
222
- import { fileURLToPath } from "node:url";
223
- import { optimize, grid } from "../src/index.js";
224
-
225
- const here = path.dirname(fileURLToPath(import.meta.url));
226
- const signalModulePath = path.join(here, "fixtures", "emaSignal.js");
227
-
228
- function buildCandles(n = 120) {
229
- const start = Date.UTC(2025, 0, 2, 14, 30, 0);
230
- return Array.from({ length: n }, (_, i) => ({
231
- time: start + i * 86_400_000,
232
- open: 100 + Math.sin(i / 4) * 5,
233
- high: 102 + Math.sin(i / 4) * 5,
234
- low: 98 + Math.sin(i / 4) * 5,
235
- close: 100 + Math.sin(i / 4) * 5,
236
- volume: 1000,
237
- }));
238
- }
239
-
240
- test("optimize runs every parameter set and returns a ranked leaderboard", async () => {
241
- const candles = buildCandles();
242
- const parameterSets = grid({ fast: [3, 5, 8], slow: [13, 21] });
243
- const out = await optimize({
244
- candles,
245
- interval: "1d",
246
- signalModulePath,
247
- parameterSets,
248
- concurrency: 2,
249
- scoreBy: "profitFactor",
250
- });
251
- assert.equal(out.results.length, parameterSets.length);
252
- assert.ok(out.leaderboard.length >= 1);
253
- // leaderboard is sorted descending by score
254
- for (let i = 1; i < out.leaderboard.length; i += 1) {
255
- assert.ok(
256
- out.leaderboard[i - 1].metrics.profitFactor >= out.leaderboard[i].metrics.profitFactor
257
- );
258
- }
259
- assert.equal(out.best, out.leaderboard[0]);
260
- });
261
-
262
- test("optimize matches a serial baseline for the same params", async () => {
263
- const candles = buildCandles();
264
- const parameterSets = grid({ fast: [5], slow: [21] });
265
- const parallel = await optimize({
266
- candles,
267
- interval: "1d",
268
- signalModulePath,
269
- parameterSets,
270
- concurrency: 1,
271
- });
272
- const { backtest } = await import("../src/index.js");
273
- const { createSignal } = await import("./fixtures/emaSignal.js");
274
- const serial = backtest({
275
- candles,
276
- interval: "1d",
277
- signal: createSignal(parameterSets[0]),
278
- collectReplay: false,
279
- collectEqSeries: false,
280
- });
281
- assert.equal(parallel.results[0].metrics.totalPnL, serial.metrics.totalPnL);
282
- });
283
- ```
284
-
285
- - [ ] **Step 2: Run to verify it fails**
286
-
287
- Run: `node --test test/optimize.test.js`
288
- Expected: FAIL — `optimize` not exported.
289
-
290
- - [ ] **Step 3: Implement src/engine/optimize.js**
291
-
292
- ```js
293
- // src/engine/optimize.js
294
- import { Worker } from "node:worker_threads";
295
- import os from "node:os";
296
-
297
- function defaultConcurrency() {
298
- return Math.max(1, (os.cpus()?.length ?? 2) - 1);
299
- }
300
-
301
- function scoreValue(metrics, scoreBy) {
302
- const v = metrics?.[scoreBy];
303
- return Number.isFinite(v) ? v : -Infinity;
304
- }
305
-
306
- /**
307
- * Parallel parameter sweep over a worker pool.
308
- *
309
- * @param {object} opts
310
- * @param {Array} opts.candles OHLCV candles (copied once per worker)
311
- * @param {string} opts.signalModulePath absolute path to a module exporting createSignal(params)
312
- * @param {Array<object>} opts.parameterSets parameter objects (use grid() to build)
313
- * @param {string} [opts.interval]
314
- * @param {object} [opts.backtestOptions]
315
- * @param {number} [opts.concurrency] defaults to cpus-1
316
- * @param {string} [opts.scoreBy="profitFactor"]
317
- * @returns {Promise<{results, leaderboard, best}>}
318
- */
319
- export function optimize({
320
- candles,
321
- signalModulePath,
322
- parameterSets,
323
- interval,
324
- backtestOptions = {},
325
- concurrency,
326
- scoreBy = "profitFactor",
327
- }) {
328
- if (!Array.isArray(parameterSets) || parameterSets.length === 0) {
329
- return Promise.resolve({ results: [], leaderboard: [], best: null });
330
- }
331
-
332
- return new Promise((resolve, reject) => {
333
- const poolSize = Math.min(concurrency || defaultConcurrency(), parameterSets.length);
334
- const results = new Array(parameterSets.length);
335
- const workers = [];
336
- let nextIndex = 0;
337
- let completed = 0;
338
- let settled = false;
339
-
340
- const finish = () => {
341
- if (settled) return;
342
- settled = true;
343
- for (const w of workers) w.terminate();
344
- const ranked = results
345
- .filter((r) => r && r.metrics)
346
- .sort((a, b) => scoreValue(b.metrics, scoreBy) - scoreValue(a.metrics, scoreBy));
347
- resolve({ results, leaderboard: ranked, best: ranked[0] ?? null });
348
- };
349
-
350
- const fail = (error) => {
351
- if (settled) return;
352
- settled = true;
353
- for (const w of workers) w.terminate();
354
- reject(error);
355
- };
356
-
357
- const dispatch = (worker) => {
358
- if (nextIndex >= parameterSets.length) {
359
- worker.postMessage({ type: "stop" });
360
- return;
361
- }
362
- const index = nextIndex;
363
- nextIndex += 1;
364
- worker.postMessage({ type: "run", index, params: parameterSets[index] });
365
- };
366
-
367
- for (let i = 0; i < poolSize; i += 1) {
368
- const worker = new Worker(new URL("./optimizeWorker.js", import.meta.url), {
369
- workerData: { candles, signalModulePath, interval, backtestOptions },
370
- });
371
- workers.push(worker);
372
-
373
- worker.on("message", (msg) => {
374
- if (msg.type === "ready") {
375
- dispatch(worker);
376
- return;
377
- }
378
- if (msg.type === "result" || msg.type === "error") {
379
- results[msg.index] =
380
- msg.type === "result"
381
- ? { params: msg.params, metrics: msg.metrics }
382
- : { params: msg.params, error: msg.error };
383
- completed += 1;
384
- if (completed === parameterSets.length) finish();
385
- else dispatch(worker);
386
- }
387
- });
388
-
389
- worker.on("error", fail);
390
- }
391
- });
392
- }
393
- ```
394
-
395
- - [ ] **Step 4: Export from src/index.js**
396
-
397
- ```js
398
- export { optimize } from "./engine/optimize.js";
399
- export { grid } from "./engine/grid.js";
400
- ```
401
-
402
- - [ ] **Step 5: Run test + full suite**
403
-
404
- Run: `node --test test/optimize.test.js`
405
- Expected: PASS (2 tests).
406
-
407
- Run: `node --test`
408
- Expected: PASS.
409
-
410
- - [ ] **Step 6: Lint + commit**
411
-
412
- ```bash
413
- npm run lint
414
- git add src/engine/optimize.js src/index.js test/optimize.test.js
415
- git commit -m "feat: parallel parameter sweep via worker pool
416
-
417
- Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
418
- ```
419
-
420
- ---
421
-
422
- ### Task 4: Build compatibility + docs
423
-
424
- **Files:**
425
-
426
- - Modify: `scripts/build-cjs.mjs` (ensure the worker file ships)
427
- - Modify: `docs/backtest-engine.md`
428
- - Create: `examples/optimize.js`
429
-
430
- - [ ] **Step 1: Confirm worker file ships in the package**
431
-
432
- `optimizeWorker.js` is under `src/`, which is already in `package.json`'s `files`
433
- array, so it ships. The CJS build does NOT need a separate bundle for the worker —
434
- worker_threads is used from the ESM `src/` path. Verify the file is present after
435
- a pack dry-run:
436
-
437
- Run: `npm pack --dry-run | grep optimizeWorker`
438
- Expected: `src/engine/optimizeWorker.js` appears in the listing.
439
-
440
- Note for the CJS consumer: `optimize()` spawns an ESM worker via
441
- `new URL("./optimizeWorker.js", import.meta.url)`. If the CJS build path breaks
442
- `import.meta.url`, document that `optimize()` is ESM-only (acceptable v1 scope) in
443
- the build script comments and the docs.
444
-
445
- - [ ] **Step 2: Write examples/optimize.js**
446
-
447
- ```js
448
- // examples/optimize.js
449
- // Run: node examples/optimize.js
450
- import path from "node:path";
451
- import { fileURLToPath } from "node:url";
452
- import { optimize, grid, getHistoricalCandles } from "../src/index.js";
453
-
454
- const here = path.dirname(fileURLToPath(import.meta.url));
455
-
456
- const candles = await getHistoricalCandles({
457
- source: "yahoo",
458
- symbol: "SPY",
459
- interval: "1d",
460
- period: "2y",
461
- cache: true,
462
- });
463
-
464
- const { leaderboard, best } = await optimize({
465
- candles,
466
- interval: "1d",
467
- // a module exporting createSignal(params)
468
- signalModulePath: path.join(here, "..", "test", "fixtures", "emaSignal.js"),
469
- parameterSets: grid({ fast: [5, 8, 10, 12], slow: [20, 30, 50] }),
470
- scoreBy: "sharpeAnnualized",
471
- });
472
-
473
- console.log("best params:", best?.params, "sharpe:", best?.metrics.sharpeAnnualized);
474
- console.table(leaderboard.slice(0, 5).map((r) => ({ ...r.params, ...r.metrics })));
475
- ```
476
-
477
- - [ ] **Step 3: Run the example**
478
-
479
- Run: `node examples/optimize.js`
480
- Expected: prints a leaderboard table (network permitting). On a multi-core machine
481
- this completes far faster than a serial loop over the same 12-set grid.
482
-
483
- - [ ] **Step 4: Document in docs/backtest-engine.md**
484
-
485
- Add an "Optimization (parallel sweeps)" section: the `signalModulePath` contract
486
- (`export function createSignal(params)`), the `grid()` helper, `concurrency`,
487
- `scoreBy`, the leaderboard shape, and the ESM-only caveat for `optimize()`.
488
-
489
- - [ ] **Step 5: Lint, format, test, commit**
490
-
491
- ```bash
492
- npm run lint && npm run format:check && npm test
493
- git add scripts/build-cjs.mjs docs/backtest-engine.md examples/optimize.js
494
- git commit -m "docs: parallel optimize example and guide
495
-
496
- Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
497
- ```
498
-
499
- ---
500
-
501
- ## Self-review checklist
502
-
503
- - [ ] Signal crosses the worker boundary as a module path, not a function (workers can't receive closures). ✔ (Task 2)
504
- - [ ] Candles copied once per worker via `workerData`, not per parameter set. ✔ (Task 3)
505
- - [ ] Pool drains correctly: `dispatch` posts `stop` when no sets remain; `finish`/`fail` are idempotent via the `settled` guard; all workers terminated. ✔
506
- - [ ] Parallel result is asserted equal to the serial baseline for identical params. ✔ (Task 3 Step 1)
507
- - [ ] IPC payloads carry only ranking metrics, not full trade logs/replay. ✔ (Task 2 `pickMetrics`)
508
- - [ ] ESM-only limitation of `optimize()` is documented. ✔ (Task 4)