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.
- package/CHANGELOG.md +57 -0
- package/README.md +183 -373
- package/dist/cjs/index.cjs +39 -12
- package/dist/cjs/live.cjs +457 -18
- 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 +6 -2
- package/src/live/dashboard/server.js +67 -8
- package/src/live/engine/paperEngine.js +21 -11
- package/src/live/index.js +2 -0
- package/src/live/session.js +439 -0
- package/src/mcp/liveTools.js +202 -0
- package/src/mcp/schemas.js +119 -0
- package/src/mcp/server.js +5 -1
- package/src/mcp/tools.js +125 -2
- package/src/research/monteCarlo.js +6 -2
- package/templates/dashboard.html +595 -108
- package/types/index.d.ts +25 -0
- package/types/live.d.ts +102 -1
- 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,547 +0,0 @@
|
|
|
1
|
-
# Live Dashboard 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:** A local, zero-dependency realtime dashboard for a running `LiveEngine` or `LiveOrchestrator` — equity, open position, recent signals, fills, and risk halts — so non-quants can watch paper/live runs with confidence.
|
|
6
|
-
|
|
7
|
-
**Architecture:** Both live sources expose `.eventBus` (an `EventBus` with `onAny(handler)`) and `.getStatus()`. A `node:http` server subscribes via `onAny`, keeps a bounded ring buffer of recent events, and pushes them to browsers over Server-Sent Events (`/events`). `/state` returns `getStatus()` JSON; `/` serves a single static HTML page that polls `/state` and live-tails `/events`. No WebSocket library, no frontend framework, no build step.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** Node built-ins (`http`, `fs`, `url`), `node:test`, a static HTML/JS template. No new dependencies. Independent of all other plans.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
### Task 1: The dashboard server (transport + event fan-out)
|
|
14
|
-
|
|
15
|
-
**Files:**
|
|
16
|
-
|
|
17
|
-
- Create: `src/live/dashboard/server.js`
|
|
18
|
-
- Create: `templates/dashboard.html`
|
|
19
|
-
- Test: `test/live/dashboard.test.js`
|
|
20
|
-
|
|
21
|
-
- [ ] **Step 1: Write the failing test**
|
|
22
|
-
|
|
23
|
-
```js
|
|
24
|
-
// test/live/dashboard.test.js
|
|
25
|
-
import test from "node:test";
|
|
26
|
-
import assert from "node:assert/strict";
|
|
27
|
-
import http from "node:http";
|
|
28
|
-
import { EventBus } from "../../src/live/events.js";
|
|
29
|
-
import { createDashboardServer } from "../../src/live/dashboard/server.js";
|
|
30
|
-
|
|
31
|
-
function getJson(url) {
|
|
32
|
-
return new Promise((resolve, reject) => {
|
|
33
|
-
http
|
|
34
|
-
.get(url, (res) => {
|
|
35
|
-
let body = "";
|
|
36
|
-
res.on("data", (c) => (body += c));
|
|
37
|
-
res.on("end", () => resolve({ status: res.statusCode, body }));
|
|
38
|
-
})
|
|
39
|
-
.on("error", reject);
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function makeSource() {
|
|
44
|
-
const eventBus = new EventBus();
|
|
45
|
-
return {
|
|
46
|
-
eventBus,
|
|
47
|
-
getStatus: () => ({ symbol: "AAPL", equity: 25_000, dayPnl: 120, openPosition: null }),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
test("/state returns the source status as JSON", async () => {
|
|
52
|
-
const source = makeSource();
|
|
53
|
-
const dash = createDashboardServer({ source, port: 0 }); // 0 = ephemeral port
|
|
54
|
-
const url = await dash.start();
|
|
55
|
-
const res = await getJson(`${url}/state`);
|
|
56
|
-
assert.equal(res.status, 200);
|
|
57
|
-
const parsed = JSON.parse(res.body);
|
|
58
|
-
assert.equal(parsed.symbol, "AAPL");
|
|
59
|
-
await dash.close();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("/ serves the dashboard HTML", async () => {
|
|
63
|
-
const source = makeSource();
|
|
64
|
-
const dash = createDashboardServer({ source, port: 0 });
|
|
65
|
-
const url = await dash.start();
|
|
66
|
-
const res = await getJson(`${url}/`);
|
|
67
|
-
assert.equal(res.status, 200);
|
|
68
|
-
assert.ok(res.body.includes("tradelab"));
|
|
69
|
-
await dash.close();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("/events streams a bussed event as SSE", async () => {
|
|
73
|
-
const source = makeSource();
|
|
74
|
-
const dash = createDashboardServer({ source, port: 0 });
|
|
75
|
-
const url = await dash.start();
|
|
76
|
-
|
|
77
|
-
const received = await new Promise((resolve, reject) => {
|
|
78
|
-
http.get(`${url}/events`, (res) => {
|
|
79
|
-
res.setEncoding("utf8");
|
|
80
|
-
res.on("data", (chunk) => {
|
|
81
|
-
if (chunk.includes("position:opened")) resolve(chunk);
|
|
82
|
-
});
|
|
83
|
-
res.on("error", reject);
|
|
84
|
-
// emit after the client is attached
|
|
85
|
-
setTimeout(() => source.eventBus.emitEvent("position:opened", { symbol: "AAPL" }), 20);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
assert.ok(received.startsWith("data:"));
|
|
90
|
-
assert.ok(received.includes("position:opened"));
|
|
91
|
-
await dash.close();
|
|
92
|
-
});
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
- [ ] **Step 2: Run to verify it fails**
|
|
96
|
-
|
|
97
|
-
Run: `node --test test/live/dashboard.test.js`
|
|
98
|
-
Expected: FAIL — cannot find module / template.
|
|
99
|
-
|
|
100
|
-
- [ ] **Step 3: Create templates/dashboard.html**
|
|
101
|
-
|
|
102
|
-
```html
|
|
103
|
-
<!doctype html>
|
|
104
|
-
<html lang="en">
|
|
105
|
-
<head>
|
|
106
|
-
<meta charset="utf-8" />
|
|
107
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
108
|
-
<title>tradelab live</title>
|
|
109
|
-
<style>
|
|
110
|
-
:root {
|
|
111
|
-
color-scheme: dark;
|
|
112
|
-
}
|
|
113
|
-
body {
|
|
114
|
-
margin: 0;
|
|
115
|
-
font:
|
|
116
|
-
14px/1.5 ui-monospace,
|
|
117
|
-
SFMono-Regular,
|
|
118
|
-
Menlo,
|
|
119
|
-
monospace;
|
|
120
|
-
background: #0f172a;
|
|
121
|
-
color: #e2e8f0;
|
|
122
|
-
}
|
|
123
|
-
header {
|
|
124
|
-
padding: 16px 24px;
|
|
125
|
-
border-bottom: 1px solid #1e293b;
|
|
126
|
-
display: flex;
|
|
127
|
-
gap: 24px;
|
|
128
|
-
align-items: baseline;
|
|
129
|
-
}
|
|
130
|
-
h1 {
|
|
131
|
-
font-size: 16px;
|
|
132
|
-
margin: 0;
|
|
133
|
-
color: #38bdf8;
|
|
134
|
-
}
|
|
135
|
-
.grid {
|
|
136
|
-
display: grid;
|
|
137
|
-
grid-template-columns: 1fr 1fr;
|
|
138
|
-
gap: 16px;
|
|
139
|
-
padding: 24px;
|
|
140
|
-
}
|
|
141
|
-
.card {
|
|
142
|
-
background: #111827;
|
|
143
|
-
border: 1px solid #1e293b;
|
|
144
|
-
border-radius: 8px;
|
|
145
|
-
padding: 16px;
|
|
146
|
-
}
|
|
147
|
-
.card h2 {
|
|
148
|
-
font-size: 12px;
|
|
149
|
-
text-transform: uppercase;
|
|
150
|
-
letter-spacing: 0.08em;
|
|
151
|
-
color: #64748b;
|
|
152
|
-
margin: 0 0 12px;
|
|
153
|
-
}
|
|
154
|
-
.big {
|
|
155
|
-
font-size: 28px;
|
|
156
|
-
font-weight: 600;
|
|
157
|
-
}
|
|
158
|
-
.pos {
|
|
159
|
-
color: #4ade80;
|
|
160
|
-
}
|
|
161
|
-
.neg {
|
|
162
|
-
color: #f87171;
|
|
163
|
-
}
|
|
164
|
-
ul {
|
|
165
|
-
list-style: none;
|
|
166
|
-
margin: 0;
|
|
167
|
-
padding: 0;
|
|
168
|
-
max-height: 320px;
|
|
169
|
-
overflow: auto;
|
|
170
|
-
}
|
|
171
|
-
li {
|
|
172
|
-
padding: 6px 0;
|
|
173
|
-
border-bottom: 1px solid #1e293b;
|
|
174
|
-
display: flex;
|
|
175
|
-
gap: 12px;
|
|
176
|
-
}
|
|
177
|
-
.halt {
|
|
178
|
-
color: #fbbf24;
|
|
179
|
-
}
|
|
180
|
-
time {
|
|
181
|
-
color: #475569;
|
|
182
|
-
}
|
|
183
|
-
</style>
|
|
184
|
-
</head>
|
|
185
|
-
<body>
|
|
186
|
-
<header>
|
|
187
|
-
<h1>tradelab live</h1>
|
|
188
|
-
<span id="symbol">—</span>
|
|
189
|
-
<span id="conn">connecting…</span>
|
|
190
|
-
</header>
|
|
191
|
-
<div class="grid">
|
|
192
|
-
<div class="card">
|
|
193
|
-
<h2>Equity</h2>
|
|
194
|
-
<div class="big" id="equity">—</div>
|
|
195
|
-
<div>Day PnL: <span id="dayPnl">—</span></div>
|
|
196
|
-
</div>
|
|
197
|
-
<div class="card">
|
|
198
|
-
<h2>Open position</h2>
|
|
199
|
-
<div id="position">flat</div>
|
|
200
|
-
</div>
|
|
201
|
-
<div class="card">
|
|
202
|
-
<h2>Risk</h2>
|
|
203
|
-
<div id="risk">—</div>
|
|
204
|
-
</div>
|
|
205
|
-
<div class="card">
|
|
206
|
-
<h2>Recent events</h2>
|
|
207
|
-
<ul id="events"></ul>
|
|
208
|
-
</div>
|
|
209
|
-
</div>
|
|
210
|
-
<script>
|
|
211
|
-
const fmt = (n) =>
|
|
212
|
-
typeof n === "number" ? n.toLocaleString(undefined, { maximumFractionDigits: 2 }) : "—";
|
|
213
|
-
function applyState(s) {
|
|
214
|
-
document.getElementById("symbol").textContent = s.symbol ?? s.id ?? "portfolio";
|
|
215
|
-
document.getElementById("equity").textContent = fmt(s.equity);
|
|
216
|
-
const dp = document.getElementById("dayPnl");
|
|
217
|
-
dp.textContent = fmt(s.dayPnl);
|
|
218
|
-
dp.className = (s.dayPnl ?? 0) >= 0 ? "pos" : "neg";
|
|
219
|
-
const p = s.openPosition;
|
|
220
|
-
document.getElementById("position").textContent = p
|
|
221
|
-
? `${p.side} ${fmt(p.size)} @ ${fmt(p.entryFill ?? p.entry)} | uPnL ${fmt(p.unrealizedPnl)}`
|
|
222
|
-
: "flat";
|
|
223
|
-
document.getElementById("risk").textContent = s.risk ? JSON.stringify(s.risk) : "ok";
|
|
224
|
-
}
|
|
225
|
-
async function pollState() {
|
|
226
|
-
try {
|
|
227
|
-
applyState(await (await fetch("/state")).json());
|
|
228
|
-
} catch {}
|
|
229
|
-
}
|
|
230
|
-
pollState();
|
|
231
|
-
setInterval(pollState, 3000);
|
|
232
|
-
|
|
233
|
-
const list = document.getElementById("events");
|
|
234
|
-
const es = new EventSource("/events");
|
|
235
|
-
es.onopen = () => (document.getElementById("conn").textContent = "live");
|
|
236
|
-
es.onerror = () => (document.getElementById("conn").textContent = "disconnected");
|
|
237
|
-
es.onmessage = (e) => {
|
|
238
|
-
const msg = JSON.parse(e.data);
|
|
239
|
-
const li = document.createElement("li");
|
|
240
|
-
const halt = msg.event.startsWith("risk:");
|
|
241
|
-
li.innerHTML =
|
|
242
|
-
"<time>" +
|
|
243
|
-
new Date(msg.t).toLocaleTimeString() +
|
|
244
|
-
"</time>" +
|
|
245
|
-
'<span class="' +
|
|
246
|
-
(halt ? "halt" : "") +
|
|
247
|
-
'">' +
|
|
248
|
-
msg.event +
|
|
249
|
-
"</span>" +
|
|
250
|
-
"<span>" +
|
|
251
|
-
(msg.payload?.symbol ?? "") +
|
|
252
|
-
"</span>";
|
|
253
|
-
list.prepend(li);
|
|
254
|
-
while (list.children.length > 100) list.removeChild(list.lastChild);
|
|
255
|
-
if (["equity:update", "position:opened", "position:closed"].includes(msg.event))
|
|
256
|
-
pollState();
|
|
257
|
-
};
|
|
258
|
-
</script>
|
|
259
|
-
</body>
|
|
260
|
-
</html>
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
- [ ] **Step 4: Implement src/live/dashboard/server.js**
|
|
264
|
-
|
|
265
|
-
```js
|
|
266
|
-
// src/live/dashboard/server.js
|
|
267
|
-
import http from "node:http";
|
|
268
|
-
import { readFileSync } from "node:fs";
|
|
269
|
-
import path from "node:path";
|
|
270
|
-
import { fileURLToPath } from "node:url";
|
|
271
|
-
|
|
272
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
273
|
-
const HTML_PATH = path.join(here, "..", "..", "..", "templates", "dashboard.html");
|
|
274
|
-
const HTML = readFileSync(HTML_PATH, "utf8");
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Local realtime dashboard for a LiveEngine or LiveOrchestrator.
|
|
278
|
-
*
|
|
279
|
-
* @param {object} opts
|
|
280
|
-
* @param {{ eventBus: import("../events.js").EventBus, getStatus: Function }} opts.source
|
|
281
|
-
* @param {number} [opts.port=4317] 0 picks an ephemeral port (useful in tests)
|
|
282
|
-
* @param {number} [opts.maxBuffer=200] recent events replayed to new clients
|
|
283
|
-
* @returns {{ start: () => Promise<string>, close: () => Promise<void>, server: http.Server }}
|
|
284
|
-
*/
|
|
285
|
-
export function createDashboardServer({ source, port = 4317, maxBuffer = 200 }) {
|
|
286
|
-
if (!source?.eventBus || typeof source.eventBus.onAny !== "function") {
|
|
287
|
-
throw new Error("dashboard source must expose an eventBus with onAny()");
|
|
288
|
-
}
|
|
289
|
-
const recent = [];
|
|
290
|
-
const clients = new Set();
|
|
291
|
-
|
|
292
|
-
const unsubscribe = source.eventBus.onAny(({ event, payload }) => {
|
|
293
|
-
const msg = { event, payload, t: Date.now() };
|
|
294
|
-
recent.push(msg);
|
|
295
|
-
if (recent.length > maxBuffer) recent.shift();
|
|
296
|
-
const frame = `data: ${JSON.stringify(msg)}\n\n`;
|
|
297
|
-
for (const res of clients) res.write(frame);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
const server = http.createServer((req, res) => {
|
|
301
|
-
const url = (req.url || "/").split("?")[0];
|
|
302
|
-
|
|
303
|
-
if (url === "/") {
|
|
304
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
305
|
-
res.end(HTML);
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (url === "/state") {
|
|
310
|
-
const status = typeof source.getStatus === "function" ? source.getStatus() : {};
|
|
311
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
312
|
-
res.end(JSON.stringify(status));
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (url === "/events") {
|
|
317
|
-
res.writeHead(200, {
|
|
318
|
-
"Content-Type": "text/event-stream",
|
|
319
|
-
"Cache-Control": "no-cache",
|
|
320
|
-
Connection: "keep-alive",
|
|
321
|
-
});
|
|
322
|
-
for (const msg of recent) res.write(`data: ${JSON.stringify(msg)}\n\n`);
|
|
323
|
-
clients.add(res);
|
|
324
|
-
req.on("close", () => clients.delete(res));
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
329
|
-
res.end("not found");
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
return {
|
|
333
|
-
start() {
|
|
334
|
-
return new Promise((resolve) => {
|
|
335
|
-
server.listen(port, () => {
|
|
336
|
-
const actualPort = server.address().port;
|
|
337
|
-
resolve(`http://localhost:${actualPort}`);
|
|
338
|
-
});
|
|
339
|
-
});
|
|
340
|
-
},
|
|
341
|
-
close() {
|
|
342
|
-
unsubscribe();
|
|
343
|
-
for (const res of clients) res.end();
|
|
344
|
-
clients.clear();
|
|
345
|
-
return new Promise((resolve) => server.close(resolve));
|
|
346
|
-
},
|
|
347
|
-
server,
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
- [ ] **Step 5: Run test + suite**
|
|
353
|
-
|
|
354
|
-
Run: `node --test test/live/dashboard.test.js`
|
|
355
|
-
Expected: PASS (3 tests).
|
|
356
|
-
|
|
357
|
-
Run: `node --test`
|
|
358
|
-
Expected: PASS.
|
|
359
|
-
|
|
360
|
-
- [ ] **Step 6: Make sure the template ships**
|
|
361
|
-
|
|
362
|
-
`templates/` is already in `package.json`'s `files` array, so `dashboard.html`
|
|
363
|
-
ships. Confirm:
|
|
364
|
-
|
|
365
|
-
Run: `npm pack --dry-run | grep dashboard.html`
|
|
366
|
-
Expected: `templates/dashboard.html` listed.
|
|
367
|
-
|
|
368
|
-
- [ ] **Step 7: Commit**
|
|
369
|
-
|
|
370
|
-
```bash
|
|
371
|
-
git add src/live/dashboard/server.js templates/dashboard.html test/live/dashboard.test.js
|
|
372
|
-
git commit -m "feat: local SSE dashboard server for live engine
|
|
373
|
-
|
|
374
|
-
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
---
|
|
378
|
-
|
|
379
|
-
### Task 2: Export + live subpath wiring
|
|
380
|
-
|
|
381
|
-
**Files:**
|
|
382
|
-
|
|
383
|
-
- Modify: `src/live/index.js`
|
|
384
|
-
- Test: `test/live/dashboard.test.js` (append export check)
|
|
385
|
-
|
|
386
|
-
- [ ] **Step 1: Append the failing test**
|
|
387
|
-
|
|
388
|
-
```js
|
|
389
|
-
// append to test/live/dashboard.test.js
|
|
390
|
-
import * as live from "../../src/live/index.js";
|
|
391
|
-
|
|
392
|
-
test("createDashboardServer is exported from tradelab/live", () => {
|
|
393
|
-
assert.equal(typeof live.createDashboardServer, "function");
|
|
394
|
-
});
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
- [ ] **Step 2: Run to verify it fails**
|
|
398
|
-
|
|
399
|
-
Run: `node --test test/live/dashboard.test.js`
|
|
400
|
-
Expected: FAIL — `createDashboardServer` not exported from `src/live/index.js`.
|
|
401
|
-
|
|
402
|
-
- [ ] **Step 3: Add the export**
|
|
403
|
-
|
|
404
|
-
In `src/live/index.js`, add:
|
|
405
|
-
|
|
406
|
-
```js
|
|
407
|
-
export { createDashboardServer } from "./dashboard/server.js";
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
- [ ] **Step 4: Rebuild the CJS bundle (live subpath)**
|
|
411
|
-
|
|
412
|
-
Run: `npm run build`
|
|
413
|
-
Expected: exit 0; `dist/cjs/live.cjs` regenerated with the new export.
|
|
414
|
-
|
|
415
|
-
Note: `readFileSync(HTML_PATH)` uses `import.meta.url`. If the CJS build cannot
|
|
416
|
-
resolve `import.meta.url`, document the dashboard as ESM-only (acceptable v1) in
|
|
417
|
-
the live docs.
|
|
418
|
-
|
|
419
|
-
- [ ] **Step 5: Run test + suite**
|
|
420
|
-
|
|
421
|
-
Run: `node --test test/live/dashboard.test.js`
|
|
422
|
-
Expected: PASS (4 tests).
|
|
423
|
-
|
|
424
|
-
Run: `node --test`
|
|
425
|
-
Expected: PASS.
|
|
426
|
-
|
|
427
|
-
- [ ] **Step 6: Commit**
|
|
428
|
-
|
|
429
|
-
```bash
|
|
430
|
-
git add src/live/index.js
|
|
431
|
-
git commit -m "feat: export createDashboardServer from tradelab/live
|
|
432
|
-
|
|
433
|
-
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
---
|
|
437
|
-
|
|
438
|
-
### Task 3: CLI flag + docs + example
|
|
439
|
-
|
|
440
|
-
**Files:**
|
|
441
|
-
|
|
442
|
-
- Modify: `bin/tradelab.js` (add `--dashboard` to the `paper` and `live` commands)
|
|
443
|
-
- Modify: `docs/live-trading.md`
|
|
444
|
-
- Create: `examples/liveDashboard.js`
|
|
445
|
-
|
|
446
|
-
- [ ] **Step 1: Add a `--dashboard` flag to the live CLI commands**
|
|
447
|
-
|
|
448
|
-
Open `bin/tradelab.js` and locate where the `paper` and `live` commands construct
|
|
449
|
-
and `start()` their `LiveEngine`/`LiveOrchestrator`. After the engine/orchestrator
|
|
450
|
-
is created but before/around `start()`, add:
|
|
451
|
-
|
|
452
|
-
```js
|
|
453
|
-
// inside the paper/live command handler, after building `engine` (or `orchestrator`)
|
|
454
|
-
if (args.dashboard) {
|
|
455
|
-
const { createDashboardServer } = await import("../src/live/dashboard/server.js");
|
|
456
|
-
const dash = createDashboardServer({
|
|
457
|
-
source: engine, // or `orchestrator` in the live command
|
|
458
|
-
port: Number(args.dashboardPort) || 4317,
|
|
459
|
-
});
|
|
460
|
-
const dashUrl = await dash.start();
|
|
461
|
-
console.log(`dashboard: ${dashUrl}`);
|
|
462
|
-
// ensure it closes on shutdown alongside the engine
|
|
463
|
-
process.on("SIGINT", () => dash.close());
|
|
464
|
-
}
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
Match the file's existing arg-parsing convention for reading `--dashboard` and
|
|
468
|
-
`--dashboardPort` (the CLI already parses flags like `--symbol`, `--once`; follow
|
|
469
|
-
the same accessor, whether that's `args.dashboard`, `flags.dashboard`, etc.).
|
|
470
|
-
|
|
471
|
-
- [ ] **Step 2: Manually verify the flag wires up (paper mode, one bar)**
|
|
472
|
-
|
|
473
|
-
Run:
|
|
474
|
-
|
|
475
|
-
```bash
|
|
476
|
-
npx tradelab paper --symbol AAPL --interval 1m --mode polling --once true --dashboard --dashboardPort 4399
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
Expected: prints `dashboard: http://localhost:4399`. Open it in a browser during a
|
|
480
|
-
longer run (drop `--once true`) to see equity/events update. For `--once true` the
|
|
481
|
-
process exits quickly; the line printing confirms wiring.
|
|
482
|
-
|
|
483
|
-
- [ ] **Step 3: Write examples/liveDashboard.js**
|
|
484
|
-
|
|
485
|
-
```js
|
|
486
|
-
// examples/liveDashboard.js
|
|
487
|
-
// Run: node examples/liveDashboard.js (then open the printed URL)
|
|
488
|
-
import {
|
|
489
|
-
LiveEngine,
|
|
490
|
-
PaperEngine,
|
|
491
|
-
JsonFileStorage,
|
|
492
|
-
createDashboardServer,
|
|
493
|
-
} from "../src/live/index.js";
|
|
494
|
-
|
|
495
|
-
const engine = new LiveEngine({
|
|
496
|
-
id: "aapl-1m",
|
|
497
|
-
symbol: "AAPL",
|
|
498
|
-
interval: "1m",
|
|
499
|
-
mode: "polling",
|
|
500
|
-
broker: new PaperEngine({ equity: 25_000 }),
|
|
501
|
-
storage: new JsonFileStorage({ baseDir: "./output/live-state" }),
|
|
502
|
-
signal({ bar, openPosition }) {
|
|
503
|
-
if (openPosition) return null;
|
|
504
|
-
return { side: "long", stop: bar.close - 1, rr: 2 };
|
|
505
|
-
},
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
const dashboard = createDashboardServer({ source: engine });
|
|
509
|
-
const url = await dashboard.start();
|
|
510
|
-
console.log(`Dashboard running at ${url} — Ctrl+C to stop`);
|
|
511
|
-
|
|
512
|
-
await engine.start();
|
|
513
|
-
|
|
514
|
-
process.on("SIGINT", async () => {
|
|
515
|
-
await engine.stop();
|
|
516
|
-
await dashboard.close();
|
|
517
|
-
process.exit(0);
|
|
518
|
-
});
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
- [ ] **Step 4: Document in docs/live-trading.md**
|
|
522
|
-
|
|
523
|
-
Add a "Live dashboard" section: programmatic usage (`createDashboardServer({ source })`
|
|
524
|
-
with a `LiveEngine` or `LiveOrchestrator`), the `--dashboard`/`--dashboardPort` CLI
|
|
525
|
-
flags, what the page shows (equity, day PnL, open position, risk state, event tail),
|
|
526
|
-
and the ESM-only note.
|
|
527
|
-
|
|
528
|
-
- [ ] **Step 5: Lint, format, full test, commit**
|
|
529
|
-
|
|
530
|
-
```bash
|
|
531
|
-
npm run lint && npm run format:check && npm test
|
|
532
|
-
git add bin/tradelab.js docs/live-trading.md examples/liveDashboard.js
|
|
533
|
-
git commit -m "feat: --dashboard CLI flag, example, and docs
|
|
534
|
-
|
|
535
|
-
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
---
|
|
539
|
-
|
|
540
|
-
## Self-review checklist
|
|
541
|
-
|
|
542
|
-
- [ ] No new dependencies — `node:http` + SSE only. ✔
|
|
543
|
-
- [ ] Works for both `LiveEngine` and `LiveOrchestrator` (both expose `eventBus.onAny` + `getStatus`). ✔
|
|
544
|
-
- [ ] New SSE clients get the recent-event buffer replayed on connect, so the page isn't blank. ✔ (Task 1 Step 4)
|
|
545
|
-
- [ ] `close()` unsubscribes from the bus, ends client streams, and closes the server (no leaks in tests). ✔
|
|
546
|
-
- [ ] `port: 0` ephemeral binding is supported so tests don't collide on a fixed port. ✔ (Task 1 Step 4 `start()` reads `server.address().port`)
|
|
547
|
-
- [ ] Template ships via the existing `templates/` files entry. ✔ (Task 1 Step 6)
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
> STATUS (2026-06-26): All 8 plans implemented and verified. Release prep for 1.1.0 done.
|
|
2
|
-
|
|
3
|
-
# Execution Handoff — tradelab 2026 Roadmap
|
|
4
|
-
|
|
5
|
-
**For: Codex (incoming executor).** Opus was orchestrating Sonnet subagents; the session is ending, so you (Codex) are now the executor. This doc is your single source of truth for where things stand and how to continue.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## TL;DR
|
|
10
|
-
|
|
11
|
-
- **Branch:** `feat/2026-roadmap` (NOT `main`). Stay on it. Tree is clean.
|
|
12
|
-
- **Done & verified:** Plan 1 (metrics) ✅, Plan 2 (indicator library) ✅.
|
|
13
|
-
- **Remaining, in this order:** Plan 4 → Plan 5 → Plan 3 → Plan 6 → Plan 7 → Plan 8.
|
|
14
|
-
- **Full test suite is green right now: 77 pass, 0 fail** (`node --test`).
|
|
15
|
-
- Every plan lives in `docs/superpowers/plans/2026-0X-*.md` and is a complete, literal, TDD spec (exact tests, exact code, exact commands, a commit per task). Execute them verbatim.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## What's been completed
|
|
20
|
-
|
|
21
|
-
### Plan 1 — Metrics correctness ✅ (commits `9e60909`..`806e2ac`)
|
|
22
|
-
|
|
23
|
-
- New: `src/metrics/finite.js` (`clampFinite`, `BIG_NUMBER=1e9`), `src/metrics/annualize.js` (`periodsPerYear`), `src/metrics/benchmark.js` (`benchmarkStats`).
|
|
24
|
-
- `buildMetrics` now: annualizes (`sharpeAnnualized`, `sortinoAnnualized`, `annualizationPeriods`), clamps all ratios to finite (no more `Infinity`/`NaN` in metrics JSON), and emits a `benchmark` block when `benchmarkReturns` is passed.
|
|
25
|
-
- `interval` is threaded into `buildMetrics` from `backtest.js`, `barSystemRunner.js`, `backtestTicks.js`, `walkForward.js`.
|
|
26
|
-
- Exports added to `src/index.js`: `benchmarkStats`, `clampFinite`, `BIG_NUMBER`, `periodsPerYear`.
|
|
27
|
-
- **Known small gap (intentional, not in plan scope):** `src/engine/portfolio.js` does not thread `interval` into its aggregate `buildMetrics` call. Leave it unless you do a dedicated cleanup.
|
|
28
|
-
|
|
29
|
-
### Plan 2 — Indicator library `tradelab/ta` ✅ (commits `b91675c`..`b3ec605`)
|
|
30
|
-
|
|
31
|
-
- New subpath `tradelab/ta` wired in `package.json` exports + `scripts/build-cjs.mjs` (produces `dist/cjs/ta.cjs`) + `types/ta.d.ts`.
|
|
32
|
-
- `src/ta/`: `oscillators.js` (rsi, macd, stochastic), `channels.js` (bollinger, donchian, keltner), `trend.js` (supertrend, vwap), `index.js` re-exports those + existing ema/atr/swings/FVG.
|
|
33
|
-
- All indicators return full-length arrays aligned to input (warmup = `undefined`).
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## Remaining plans (execute in this order)
|
|
38
|
-
|
|
39
|
-
| Order | Plan file | Notes / gotchas |
|
|
40
|
-
| ----- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
41
|
-
| 1 | `2026-04-async-signals-seeding.md` | **Has a real refactor**: splits `BarSystemRunner.step()` into `_preSignal()` + `_applyRawSignal()` + `step()`/`stepAsync()`. The plan gives exact mechanical edit instructions. The regression suites (`test/backtest.test.js`, `test/portfolio.test.js`) are the guard — they MUST stay green; behavior of sync `step()` must not change. Also creates `src/utils/random.js`. |
|
|
42
|
-
| 2 | `2026-05-mcp-server.md` | Adds runtime deps `@modelcontextprotocol/sdk` + `zod` (`npm install`). Builds a name-addressable strategy registry first (agents can't pass closures). Soft-depends on Plan 1 (metrics) ✅ and Plan 4 (async). |
|
|
43
|
-
| 3 | `2026-03-overfitting-toolkit.md` | **Also creates `src/utils/random.js`** — if Plan 4 already created it, Task 1 says verify content matches and SKIP re-creating (don't clobber). Math-heavy (CPCV, PBO, deflated Sharpe, Monte Carlo); the plan has the full formulas + tests. |
|
|
44
|
-
| 4 | `2026-06-parallel-param-sweep.md` | `worker_threads` pool. Signal crosses the boundary as a **module path** exporting `createSignal(params)`, not a function. `optimize()` is ESM-only (documented). Independent. |
|
|
45
|
-
| 5 | `2026-07-funding-carry-costs.md` | Adds time-based `financingCost` (carry + perp funding) to `execution.js`, deducted in the leg-close of all 3 engines. Defaults to zero so nothing else changes. Independent. |
|
|
46
|
-
| 6 | `2026-08-live-dashboard.md` | Zero-dep `node:http` + SSE dashboard for `LiveEngine`/`LiveOrchestrator`. Uses `eventBus.onAny()` + `getStatus()`. Independent. |
|
|
47
|
-
|
|
48
|
-
**Dependency reminders:** Plan 4 before Plan 5 (MCP wants async). Plans 3/6/7/8 are independent and can be reordered freely. Both Plan 3 and Plan 4 create `src/utils/random.js` with **byte-identical** content — whichever runs first wins; the other verifies-and-skips.
|
|
49
|
-
|
|
50
|
-
---
|
|
51
|
-
|
|
52
|
-
## How to execute each plan (the process Opus was using)
|
|
53
|
-
|
|
54
|
-
For each plan file, task by task:
|
|
55
|
-
|
|
56
|
-
1. **TDD, literally.** Write the failing test from the step → run `node --test test/<file>` to confirm it fails → implement the exact code → run again to confirm pass.
|
|
57
|
-
2. **"Find this string" edits:** the plans quote real source. If a quoted string doesn't match the current file (e.g. because an earlier plan changed it), **re-read the actual file** and adapt the edit minimally. Don't blindly trust the quoted snippet over the real file.
|
|
58
|
-
3. **After each task:** commit with the plan's exact message. **Every commit message must end with the trailer:**
|
|
59
|
-
```
|
|
60
|
-
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
61
|
-
```
|
|
62
|
-
(Codex: keep this trailer for consistency, or swap to your own co-author line if the user prefers — ask if unsure.)
|
|
63
|
-
4. **Gates that must pass before moving to the next plan:** `node --test` (full suite green), `npm run lint`, and where the plan touches subpaths/types: `npm run build` and `npm run typecheck`.
|
|
64
|
-
5. **Each plan ends with a self-review checklist** — actually run through it.
|
|
65
|
-
|
|
66
|
-
### Repo conventions
|
|
67
|
-
|
|
68
|
-
- ESM only (`"type":"module"`), Node >=18. No transpile for `src/`; CJS is generated by `npm run build` (esbuild) — re-run it whenever you touch a subpath entry or its exports.
|
|
69
|
-
- Tests: `node:test` + `node:assert/strict`, discovered by filename under `test/`. No runner config.
|
|
70
|
-
- New top-level exports go in `src/index.js`. New subpaths need: `package.json` `exports` entry + a `dist/cjs/*.cjs` bundle in `scripts/build-cjs.mjs` + a `types/*.d.ts`.
|
|
71
|
-
- **Never break** the canonical result shape `{ symbol, interval, range, trades, positions, openPositions, metrics, eqSeries, replay }` or the `signal(context)` contract. Metric changes are additive only.
|
|
72
|
-
- Prettier may reformat docs/tables on commit — run `npm run format` and re-add if `format:check` fails (this already happened twice; harmless).
|
|
73
|
-
|
|
74
|
-
---
|
|
75
|
-
|
|
76
|
-
## Useful state
|
|
77
|
-
|
|
78
|
-
- Overview / sequencing / shared conventions: `docs/superpowers/plans/2026-00-overview.md`.
|
|
79
|
-
- Branch log so far: `git log --oneline main..HEAD`.
|
|
80
|
-
- Last green checkpoint commit: `chore: rebuild cjs bundles and format plan docs`.
|
|
81
|
-
- When ALL plans are done: run the full suite + lint + build + typecheck once more, then use a finishing flow (PR or merge) — confirm with the user first.
|
|
82
|
-
|
|
83
|
-
---
|
|
84
|
-
|
|
85
|
-
## Two review-quality lessons from Plan 1 (apply going forward)
|
|
86
|
-
|
|
87
|
-
1. **Clamp invariants must hold after every transform.** Plan 1's first pass clamped Sharpe before the `*sqrt(periods)` multiply, letting `1e9` leak to `~1.6e10`. Fix was to clamp again after. When you add derived metrics, clamp the final value.
|
|
88
|
-
2. **Test fixtures must actually exercise the code path.** A benchmark test used single-session intraday bars, which collapse to one daily-return bucket and make `beta` always `null`. Use multi-day daily candles when asserting daily-bucketed stats. Watch for assertions hidden behind `if (...)` guards that make them silently optional.
|