tradelab 1.1.0 → 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.
- package/CHANGELOG.md +46 -0
- package/README.md +185 -388
- package/dist/cjs/index.cjs +31 -9
- package/dist/cjs/live.cjs +409 -7
- package/docs/README.md +32 -66
- package/docs/api-reference.md +269 -144
- package/docs/backtest-engine.md +167 -321
- package/docs/data-reporting-cli.md +114 -156
- package/docs/examples.md +6 -6
- package/docs/live-trading.md +254 -134
- package/docs/mcp.md +244 -23
- package/docs/research.md +99 -45
- package/examples/mcpLiveTrading.js +77 -0
- package/package.json +11 -3
- package/src/engine/optimize.js +25 -1
- package/src/engine/portfolio.js +4 -1
- package/src/live/dashboard/server.js +67 -8
- package/src/live/engine/paperEngine.js +5 -0
- package/src/live/index.js +2 -0
- package/src/live/session.js +402 -0
- package/src/mcp/liveTools.js +179 -0
- package/src/mcp/schemas.js +119 -0
- package/src/mcp/server.js +5 -1
- package/src/mcp/tools.js +125 -2
- package/templates/dashboard.html +595 -108
- package/types/index.d.ts +25 -0
- package/types/live.d.ts +99 -0
- package/types/mcp.d.ts +17 -0
- package/docs/superpowers/plans/2026-00-overview.md +0 -101
- package/docs/superpowers/plans/2026-01-metrics-correctness.md +0 -873
- package/docs/superpowers/plans/2026-02-indicator-library.md +0 -677
- package/docs/superpowers/plans/2026-03-overfitting-toolkit.md +0 -882
- package/docs/superpowers/plans/2026-04-async-signals-seeding.md +0 -981
- package/docs/superpowers/plans/2026-05-mcp-server.md +0 -758
- package/docs/superpowers/plans/2026-06-parallel-param-sweep.md +0 -508
- package/docs/superpowers/plans/2026-07-funding-carry-costs.md +0 -535
- package/docs/superpowers/plans/2026-08-live-dashboard.md +0 -547
- 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)
|