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
@@ -3,136 +3,594 @@
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>tradelab live</title>
6
+ <title>tradelab · live</title>
7
7
  <style>
8
8
  :root {
9
9
  color-scheme: dark;
10
+ --bg: #0b0d12;
11
+ --surface: #12151c;
12
+ --surface-2: #171b24;
13
+ --line: rgba(255, 255, 255, 0.06);
14
+ --line-2: rgba(255, 255, 255, 0.1);
15
+ --text: #e7ebf0;
16
+ --muted: #8a93a3;
17
+ --up: #3fb950;
18
+ --down: #f85149;
19
+ --warn: #d29922;
20
+ --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
21
+ --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
22
+ }
23
+ * {
24
+ box-sizing: border-box;
10
25
  }
11
-
12
26
  body {
13
27
  margin: 0;
14
- font:
15
- 14px/1.5 ui-monospace,
16
- SFMono-Regular,
17
- Menlo,
18
- monospace;
19
- background: #0f172a;
20
- color: #e2e8f0;
28
+ background: var(--bg);
29
+ color: var(--text);
30
+ font-family: var(--sans);
31
+ font-size: 13px;
32
+ -webkit-font-smoothing: antialiased;
33
+ }
34
+ .num {
35
+ font-family: var(--mono);
36
+ font-variant-numeric: tabular-nums;
37
+ }
38
+ .up {
39
+ color: var(--up);
40
+ }
41
+ .down {
42
+ color: var(--down);
43
+ }
44
+ .muted {
45
+ color: var(--muted);
21
46
  }
22
47
 
23
48
  header {
24
- padding: 16px 24px;
25
- border-bottom: 1px solid #1e293b;
26
49
  display: flex;
27
- gap: 24px;
28
- align-items: baseline;
50
+ align-items: center;
51
+ gap: 16px;
52
+ padding: 0 20px;
53
+ height: 56px;
54
+ border-bottom: 1px solid var(--line);
55
+ background: linear-gradient(var(--surface), var(--bg));
56
+ position: sticky;
57
+ top: 0;
58
+ z-index: 10;
59
+ }
60
+ .brand {
61
+ font-weight: 650;
62
+ letter-spacing: -0.01em;
63
+ font-size: 15px;
64
+ }
65
+ .brand b {
66
+ color: var(--up);
67
+ }
68
+ .badge {
69
+ font-family: var(--mono);
70
+ font-size: 10.5px;
71
+ text-transform: uppercase;
72
+ letter-spacing: 0.06em;
73
+ padding: 3px 8px;
74
+ border-radius: 999px;
75
+ border: 1px solid var(--line-2);
76
+ color: var(--muted);
77
+ }
78
+ .badge.live {
79
+ color: var(--down);
80
+ border-color: rgba(248, 81, 73, 0.4);
81
+ }
82
+ .badge.paper {
83
+ color: var(--up);
84
+ border-color: rgba(63, 185, 80, 0.35);
85
+ }
86
+ .spacer {
87
+ flex: 1;
88
+ }
89
+ .pill {
90
+ display: inline-flex;
91
+ align-items: center;
92
+ gap: 6px;
93
+ font-size: 12px;
94
+ color: var(--muted);
95
+ font-family: var(--mono);
96
+ }
97
+ .dot {
98
+ width: 7px;
99
+ height: 7px;
100
+ border-radius: 999px;
101
+ background: var(--muted);
102
+ }
103
+ .dot.on {
104
+ background: var(--up);
105
+ box-shadow: 0 0 0 3px rgba(63, 185, 80, 0.15);
106
+ }
107
+ .dot.off {
108
+ background: var(--down);
109
+ }
110
+ button.ctl {
111
+ font-family: var(--sans);
112
+ font-size: 12px;
113
+ font-weight: 550;
114
+ color: var(--text);
115
+ background: var(--surface-2);
116
+ border: 1px solid var(--line-2);
117
+ border-radius: 6px;
118
+ padding: 7px 12px;
119
+ cursor: pointer;
120
+ transition:
121
+ transform 0.12s ease,
122
+ background 0.12s ease,
123
+ border-color 0.12s ease;
124
+ }
125
+ button.ctl:hover {
126
+ background: #1d2230;
127
+ border-color: rgba(255, 255, 255, 0.18);
128
+ }
129
+ button.ctl:active {
130
+ transform: translateY(1px);
131
+ }
132
+ button.ctl.danger {
133
+ color: #ffb4ad;
134
+ border-color: rgba(248, 81, 73, 0.35);
135
+ }
136
+ button.ctl.danger:hover {
137
+ background: rgba(248, 81, 73, 0.12);
29
138
  }
30
139
 
31
- h1 {
32
- font-size: 16px;
33
- margin: 0;
34
- color: #38bdf8;
140
+ .halt-banner {
141
+ display: none;
142
+ align-items: center;
143
+ gap: 10px;
144
+ padding: 10px 20px;
145
+ background: rgba(210, 153, 34, 0.12);
146
+ border-bottom: 1px solid rgba(210, 153, 34, 0.35);
147
+ color: var(--warn);
148
+ font-size: 12.5px;
149
+ }
150
+ .halt-banner.show {
151
+ display: flex;
35
152
  }
36
153
 
37
- .grid {
154
+ main {
155
+ padding: 20px;
38
156
  display: grid;
39
- grid-template-columns: 1fr 1fr;
40
157
  gap: 16px;
41
- padding: 24px;
158
+ max-width: 1400px;
159
+ margin: 0 auto;
42
160
  }
43
-
44
- .card {
45
- background: #111827;
46
- border: 1px solid #1e293b;
47
- border-radius: 8px;
48
- padding: 16px;
161
+ .kpis {
162
+ display: grid;
163
+ grid-template-columns: repeat(4, 1fr);
164
+ gap: 12px;
49
165
  }
50
-
51
- .card h2 {
52
- font-size: 12px;
166
+ .kpi {
167
+ background: var(--surface);
168
+ border: 1px solid var(--line);
169
+ border-radius: 10px;
170
+ padding: 14px 16px;
171
+ }
172
+ .kpi .label {
173
+ font-size: 11px;
53
174
  text-transform: uppercase;
54
175
  letter-spacing: 0.08em;
55
- color: #64748b;
56
- margin: 0 0 12px;
176
+ color: var(--muted);
57
177
  }
58
-
59
- .big {
178
+ .kpi .val {
179
+ font-family: var(--mono);
60
180
  font-size: 28px;
61
181
  font-weight: 600;
182
+ margin-top: 6px;
183
+ font-variant-numeric: tabular-nums;
62
184
  }
63
-
64
- .pos {
65
- color: #4ade80;
185
+ .kpi .sub {
186
+ font-family: var(--mono);
187
+ font-size: 12px;
188
+ margin-top: 2px;
189
+ color: var(--muted);
66
190
  }
67
191
 
68
- .neg {
69
- color: #f87171;
192
+ .card {
193
+ background: var(--surface);
194
+ border: 1px solid var(--line);
195
+ border-radius: 10px;
70
196
  }
71
-
72
- ul {
73
- list-style: none;
197
+ .card > h2 {
74
198
  margin: 0;
75
- padding: 0;
76
- max-height: 320px;
77
- overflow: auto;
78
- }
79
-
80
- li {
81
- padding: 6px 0;
82
- border-bottom: 1px solid #1e293b;
199
+ padding: 13px 16px;
200
+ font-size: 11px;
201
+ text-transform: uppercase;
202
+ letter-spacing: 0.08em;
203
+ color: var(--muted);
204
+ font-weight: 600;
205
+ border-bottom: 1px solid var(--line);
83
206
  display: flex;
84
- gap: 12px;
207
+ justify-content: space-between;
208
+ align-items: center;
209
+ }
210
+ #chartCard canvas {
211
+ display: block;
212
+ width: 100%;
213
+ height: 200px;
85
214
  }
86
215
 
87
- .halt {
88
- color: #fbbf24;
216
+ .cols {
217
+ display: grid;
218
+ grid-template-columns: 1.4fr 1fr;
219
+ gap: 16px;
220
+ }
221
+ table {
222
+ width: 100%;
223
+ border-collapse: collapse;
224
+ font-size: 12.5px;
225
+ }
226
+ th {
227
+ text-align: left;
228
+ font-weight: 500;
229
+ color: var(--muted);
230
+ font-size: 11px;
231
+ text-transform: uppercase;
232
+ letter-spacing: 0.05em;
233
+ padding: 9px 16px;
234
+ border-bottom: 1px solid var(--line);
235
+ }
236
+ td {
237
+ padding: 9px 16px;
238
+ border-bottom: 1px solid var(--line);
239
+ font-family: var(--mono);
240
+ font-variant-numeric: tabular-nums;
241
+ }
242
+ tr:last-child td {
243
+ border-bottom: none;
244
+ }
245
+ .side {
246
+ font-family: var(--mono);
247
+ font-size: 10.5px;
248
+ text-transform: uppercase;
249
+ letter-spacing: 0.05em;
250
+ padding: 2px 7px;
251
+ border-radius: 4px;
252
+ }
253
+ .side.long {
254
+ color: var(--up);
255
+ background: rgba(63, 185, 80, 0.12);
256
+ }
257
+ .side.short {
258
+ color: var(--down);
259
+ background: rgba(248, 81, 73, 0.12);
260
+ }
261
+ .empty {
262
+ padding: 28px 16px;
263
+ text-align: center;
264
+ color: var(--muted);
265
+ font-size: 12.5px;
266
+ }
267
+ td .x {
268
+ color: var(--muted);
269
+ cursor: pointer;
270
+ }
271
+ td .x:hover {
272
+ color: var(--down);
89
273
  }
90
274
 
91
- time {
92
- color: #475569;
275
+ .feed {
276
+ max-height: 420px;
277
+ overflow: auto;
278
+ }
279
+ .ev {
280
+ display: grid;
281
+ grid-template-columns: 64px 1fr auto;
282
+ gap: 10px;
283
+ align-items: baseline;
284
+ padding: 7px 16px;
285
+ border-bottom: 1px solid var(--line);
286
+ font-size: 12px;
287
+ }
288
+ .ev time {
289
+ font-family: var(--mono);
290
+ color: #5b6573;
291
+ font-size: 11px;
292
+ }
293
+ .ev .name {
294
+ font-family: var(--mono);
295
+ }
296
+ .ev.fill .name {
297
+ color: var(--up);
298
+ }
299
+ .ev.exit .name,
300
+ .ev.risk .name {
301
+ color: var(--warn);
302
+ }
303
+ .ev.reject .name {
304
+ color: var(--down);
305
+ }
306
+ @media (prefers-reduced-motion: no-preference) {
307
+ .ev {
308
+ animation: slideIn 0.25s ease;
309
+ }
310
+ @keyframes slideIn {
311
+ from {
312
+ opacity: 0;
313
+ transform: translateY(-3px);
314
+ }
315
+ to {
316
+ opacity: 1;
317
+ transform: none;
318
+ }
319
+ }
320
+ }
321
+ @media (max-width: 860px) {
322
+ .kpis {
323
+ grid-template-columns: repeat(2, 1fr);
324
+ }
325
+ .cols {
326
+ grid-template-columns: 1fr;
327
+ }
93
328
  }
94
329
  </style>
95
330
  </head>
96
331
  <body>
97
332
  <header>
98
- <h1>tradelab live</h1>
99
- <span id="symbol">-</span>
100
- <span id="conn">connecting...</span>
333
+ <span class="brand">trade<b>lab</b></span>
334
+ <span class="badge" id="mode">paper</span>
335
+ <span class="num muted" id="symbol">-</span>
336
+ <span class="spacer"></span>
337
+ <span class="pill"
338
+ ><span class="dot" id="connDot"></span><span id="conn">connecting</span></span
339
+ >
340
+ <button class="ctl" id="btnStop">Stop</button>
341
+ <button class="ctl danger" id="btnFlatten">Flatten all</button>
101
342
  </header>
102
- <div class="grid">
103
- <div class="card">
104
- <h2>Equity</h2>
105
- <div class="big" id="equity">-</div>
106
- <div>Day PnL: <span id="dayPnl">-</span></div>
107
- </div>
108
- <div class="card">
109
- <h2>Open position</h2>
110
- <div id="position">flat</div>
111
- </div>
112
- <div class="card">
113
- <h2>Risk</h2>
114
- <div id="risk">-</div>
115
- </div>
116
- <div class="card">
117
- <h2>Recent events</h2>
118
- <ul id="events"></ul>
119
- </div>
343
+
344
+ <div class="halt-banner" id="halt">
345
+ <span>&#9888;</span><span id="haltMsg">Risk halt active</span>
120
346
  </div>
347
+
348
+ <main>
349
+ <section class="kpis">
350
+ <div class="kpi">
351
+ <div class="label">Equity</div>
352
+ <div class="val" id="equity">-</div>
353
+ <div class="sub" id="equitySub">&nbsp;</div>
354
+ </div>
355
+ <div class="kpi">
356
+ <div class="label">Day P&amp;L</div>
357
+ <div class="val" id="dayPnl">-</div>
358
+ <div class="sub" id="dayPnlPct">&nbsp;</div>
359
+ </div>
360
+ <div class="kpi">
361
+ <div class="label">Open position</div>
362
+ <div class="val" id="posVal">flat</div>
363
+ <div class="sub" id="posSub">&nbsp;</div>
364
+ </div>
365
+ <div class="kpi">
366
+ <div class="label">Last price</div>
367
+ <div class="val" id="lastPrice">-</div>
368
+ <div class="sub" id="priceSub">&nbsp;</div>
369
+ </div>
370
+ </section>
371
+
372
+ <section class="card" id="chartCard">
373
+ <h2>Equity curve <span class="num muted" id="chartRange">&nbsp;</span></h2>
374
+ <canvas id="chart"></canvas>
375
+ </section>
376
+
377
+ <section class="cols">
378
+ <div style="display: grid; gap: 16px">
379
+ <div class="card">
380
+ <h2>Positions</h2>
381
+ <div id="positions"></div>
382
+ </div>
383
+ <div class="card">
384
+ <h2>Open orders</h2>
385
+ <div id="orders"></div>
386
+ </div>
387
+ </div>
388
+ <div class="card">
389
+ <h2>Events</h2>
390
+ <div class="feed" id="events"></div>
391
+ </div>
392
+ </section>
393
+ </main>
394
+
121
395
  <script>
122
- const fmt = (n) =>
123
- typeof n === "number" ? n.toLocaleString(undefined, { maximumFractionDigits: 2 }) : "-";
396
+ const $ = (id) => document.getElementById(id);
397
+ const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
398
+ const fmt = (n, d = 2) =>
399
+ typeof n === "number" && isFinite(n)
400
+ ? n.toLocaleString(undefined, { minimumFractionDigits: d, maximumFractionDigits: d })
401
+ : "-";
402
+ const fmtSigned = (n) => (typeof n === "number" ? (n >= 0 ? "+" : "") + fmt(n) : "-");
403
+
404
+ // smooth number tween
405
+ const tweens = new Map();
406
+ function setNum(el, value, render) {
407
+ if (typeof value !== "number" || !isFinite(value)) {
408
+ el.textContent = render(value);
409
+ return;
410
+ }
411
+ if (reduce) {
412
+ el.textContent = render(value);
413
+ return;
414
+ }
415
+ const from = tweens.get(el) ?? value;
416
+ const start = performance.now();
417
+ const dur = 280;
418
+ function frame(t) {
419
+ const k = Math.min(1, (t - start) / dur);
420
+ const eased = 1 - Math.pow(1 - k, 3);
421
+ const cur = from + (value - from) * eased;
422
+ el.textContent = render(cur);
423
+ if (k < 1) requestAnimationFrame(frame);
424
+ else tweens.set(el, value);
425
+ }
426
+ tweens.set(el, value);
427
+ requestAnimationFrame(frame);
428
+ }
429
+
430
+ // equity curve
431
+ const series = [];
432
+ let startEquity = null;
433
+ let upState = true; // colour the curve by actual session P&L, not first-seen point
434
+ const canvas = $("chart");
435
+ const ctx = canvas.getContext("2d");
436
+ function drawChart() {
437
+ const dpr = window.devicePixelRatio || 1;
438
+ const w = canvas.clientWidth,
439
+ h = canvas.clientHeight;
440
+ canvas.width = w * dpr;
441
+ canvas.height = h * dpr;
442
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
443
+ ctx.clearRect(0, 0, w, h);
444
+ if (series.length < 2) return;
445
+ const pad = 8;
446
+ const ys = series.map((p) => p.v);
447
+ let lo = Math.min(...ys),
448
+ hi = Math.max(...ys);
449
+ if (hi === lo) {
450
+ hi += 1;
451
+ lo -= 1;
452
+ }
453
+ const x = (i) => pad + (i / (series.length - 1)) * (w - pad * 2);
454
+ const y = (v) => h - pad - ((v - lo) / (hi - lo)) * (h - pad * 2);
455
+ const up = upState;
456
+ const color = up ? "#3fb950" : "#f85149";
457
+ // baseline grid
458
+ ctx.strokeStyle = "rgba(255,255,255,0.05)";
459
+ ctx.lineWidth = 1;
460
+ for (let g = 0; g <= 3; g++) {
461
+ const gy = pad + (g / 3) * (h - pad * 2);
462
+ ctx.beginPath();
463
+ ctx.moveTo(pad, gy);
464
+ ctx.lineTo(w - pad, gy);
465
+ ctx.stroke();
466
+ }
467
+ // area
468
+ const grad = ctx.createLinearGradient(0, pad, 0, h);
469
+ grad.addColorStop(0, up ? "rgba(63,185,80,0.22)" : "rgba(248,81,73,0.22)");
470
+ grad.addColorStop(1, "rgba(0,0,0,0)");
471
+ ctx.beginPath();
472
+ ctx.moveTo(x(0), y(series[0].v));
473
+ series.forEach((p, i) => ctx.lineTo(x(i), y(p.v)));
474
+ ctx.lineTo(x(series.length - 1), h - pad);
475
+ ctx.lineTo(x(0), h - pad);
476
+ ctx.closePath();
477
+ ctx.fillStyle = grad;
478
+ ctx.fill();
479
+ // line
480
+ ctx.beginPath();
481
+ ctx.moveTo(x(0), y(series[0].v));
482
+ series.forEach((p, i) => ctx.lineTo(x(i), y(p.v)));
483
+ ctx.strokeStyle = color;
484
+ ctx.lineWidth = 1.75;
485
+ ctx.lineJoin = "round";
486
+ ctx.stroke();
487
+ // head dot
488
+ const lx = x(series.length - 1),
489
+ ly = y(series[series.length - 1].v);
490
+ ctx.beginPath();
491
+ ctx.arc(lx, ly, 3, 0, Math.PI * 2);
492
+ ctx.fillStyle = color;
493
+ ctx.fill();
494
+ }
495
+ window.addEventListener("resize", drawChart);
496
+
497
+ function pushEquity(v, t) {
498
+ if (typeof v !== "number" || !isFinite(v)) return;
499
+ if (startEquity === null) startEquity = v;
500
+ series.push({ v, t: t ?? Date.now() });
501
+ if (series.length > 600) series.shift();
502
+ $("chartRange").textContent = series.length + " pts";
503
+ drawChart();
504
+ }
505
+
506
+ function rows(el, items, render, empty) {
507
+ if (!items || !items.length) {
508
+ el.innerHTML = '<div class="empty">' + empty + "</div>";
509
+ return;
510
+ }
511
+ el.innerHTML = "<table>" + render + "</table>";
512
+ }
124
513
 
125
514
  function applyState(s) {
126
- document.getElementById("symbol").textContent = s.symbol ?? s.id ?? "portfolio";
127
- document.getElementById("equity").textContent = fmt(s.equity);
128
- const dp = document.getElementById("dayPnl");
129
- dp.textContent = fmt(s.dayPnl);
130
- dp.className = (s.dayPnl ?? 0) >= 0 ? "pos" : "neg";
131
- const p = s.openPosition;
132
- document.getElementById("position").textContent = p
133
- ? `${p.side} ${fmt(p.size)} @ ${fmt(p.entryFill ?? p.entry)} | uPnL ${fmt(p.unrealizedPnl)}`
134
- : "flat";
135
- document.getElementById("risk").textContent = s.risk ? JSON.stringify(s.risk) : "ok";
515
+ $("symbol").textContent = s.symbol ?? s.id ?? "portfolio";
516
+ const mode = $("mode");
517
+ mode.textContent = s.mode ?? "paper";
518
+ mode.className = "badge " + (s.mode === "live" ? "live" : "paper");
519
+ setNum($("equity"), s.equity, (v) => fmt(v));
520
+ if (typeof s.equity === "number") pushEquity(s.equity, Date.now());
521
+ const dp = s.dayPnl ?? 0;
522
+ upState = dp >= 0;
523
+ const dpEl = $("dayPnl");
524
+ dpEl.className = "val num " + (dp >= 0 ? "up" : "down");
525
+ setNum(dpEl, dp, (v) => fmtSigned(v));
526
+ const base = (s.equity ?? 0) - dp;
527
+ $("dayPnlPct").textContent = base ? fmtSigned((dp / base) * 100) + "%" : " ";
528
+ const p = (s.positions && s.positions[0]) || s.openPosition;
529
+ if (p) {
530
+ $("posVal").innerHTML =
531
+ '<span class="side ' + p.side + '">' + p.side + "</span> " + fmt(p.qty, 3);
532
+ $("posSub").textContent = "@ " + fmt(p.avgEntry ?? p.entryFill ?? p.entry);
533
+ } else {
534
+ $("posVal").textContent = "flat";
535
+ $("posSub").innerHTML = "&nbsp;";
536
+ }
537
+ setNum($("lastPrice"), s.lastPrice, (v) => fmt(v));
538
+ const halted = s.risk && s.risk.halted;
539
+ $("halt").className = "halt-banner" + (halted ? " show" : "");
540
+ if (halted)
541
+ $("haltMsg").textContent = "Risk halt active. New entries blocked for the session.";
542
+
543
+ rows(
544
+ $("positions"),
545
+ s.positions,
546
+ "<tr><th>Symbol</th><th>Side</th><th>Qty</th><th>Entry</th><th>Mark</th><th>uPnL</th></tr>" +
547
+ (s.positions || [])
548
+ .map(
549
+ (p) =>
550
+ "<tr><td>" +
551
+ p.symbol +
552
+ '</td><td><span class="side ' +
553
+ p.side +
554
+ '">' +
555
+ p.side +
556
+ "</span></td><td>" +
557
+ fmt(p.qty, 3) +
558
+ "</td><td>" +
559
+ fmt(p.avgEntry ?? p.entry) +
560
+ "</td><td>" +
561
+ fmt(p.mark ?? p.markPrice ?? p.avgEntry) +
562
+ '</td><td class="' +
563
+ ((p.unrealizedPnl ?? 0) >= 0 ? "up" : "down") +
564
+ '">' +
565
+ fmtSigned(p.unrealizedPnl ?? 0) +
566
+ "</td></tr>"
567
+ )
568
+ .join(""),
569
+ "No open positions"
570
+ );
571
+
572
+ rows(
573
+ $("orders"),
574
+ s.openOrders,
575
+ "<tr><th>Type</th><th>Side</th><th>Qty</th><th>Price</th><th></th></tr>" +
576
+ (s.openOrders || [])
577
+ .map(
578
+ (o) =>
579
+ "<tr><td>" +
580
+ o.type +
581
+ "</td><td>" +
582
+ (o.side || "") +
583
+ "</td><td>" +
584
+ fmt(o.qty, 3) +
585
+ "</td><td>" +
586
+ fmt(o.limitPrice ?? o.stopPrice ?? 0) +
587
+ '</td><td><span class="x" data-cancel="' +
588
+ o.orderId +
589
+ '">cancel</span></td></tr>'
590
+ )
591
+ .join(""),
592
+ "No open orders"
593
+ );
136
594
  }
137
595
 
138
596
  async function pollState() {
@@ -141,34 +599,63 @@
141
599
  } catch {}
142
600
  }
143
601
 
144
- pollState();
145
- setInterval(pollState, 3000);
602
+ async function command(type, extra = {}) {
603
+ await fetch("/command", {
604
+ method: "POST",
605
+ headers: { "Content-Type": "application/json" },
606
+ body: JSON.stringify({ type, ...extra }),
607
+ }).catch(() => {});
608
+ pollState();
609
+ }
610
+ $("btnFlatten").onclick = () => command("flatten");
611
+ $("btnStop").onclick = () => command("stop");
612
+ document.addEventListener("click", (e) => {
613
+ const id = e.target?.dataset?.cancel;
614
+ if (id) command("cancelOrder", { orderId: id });
615
+ });
146
616
 
147
- const list = document.getElementById("events");
617
+ const evClass = (name) =>
618
+ name.includes("filled")
619
+ ? "fill"
620
+ : name.startsWith("risk")
621
+ ? "risk"
622
+ : name.includes("closed") || name.includes("canceled")
623
+ ? "exit"
624
+ : name.includes("reject")
625
+ ? "reject"
626
+ : "";
627
+ const feed = $("events");
148
628
  const es = new EventSource("/events");
149
- es.onopen = () => (document.getElementById("conn").textContent = "live");
150
- es.onerror = () => (document.getElementById("conn").textContent = "disconnected");
629
+ es.onopen = () => {
630
+ $("conn").textContent = "live";
631
+ $("connDot").className = "dot on";
632
+ };
633
+ es.onerror = () => {
634
+ $("conn").textContent = "disconnected";
635
+ $("connDot").className = "dot off";
636
+ };
151
637
  es.onmessage = (e) => {
152
- const msg = JSON.parse(e.data);
153
- const li = document.createElement("li");
154
- const halt = msg.event.startsWith("risk:");
155
- li.innerHTML =
638
+ const m = JSON.parse(e.data);
639
+ if (m.event === "equity:update" && typeof m.payload?.equity === "number")
640
+ pushEquity(m.payload.equity, m.t);
641
+ const row = document.createElement("div");
642
+ row.className = "ev " + evClass(m.event);
643
+ row.innerHTML =
156
644
  "<time>" +
157
- new Date(msg.t).toLocaleTimeString() +
158
- "</time>" +
159
- '<span class="' +
160
- (halt ? "halt" : "") +
161
- '">' +
162
- msg.event +
163
- "</span>" +
164
- "<span>" +
165
- (msg.payload?.symbol ?? "") +
645
+ new Date(m.t).toLocaleTimeString([], { hour12: false }) +
646
+ '</time><span class="name">' +
647
+ m.event +
648
+ '</span><span class="num muted">' +
649
+ (m.payload?.symbol ?? "") +
166
650
  "</span>";
167
- list.prepend(li);
168
- while (list.children.length > 100) list.removeChild(list.lastChild);
169
- if (["equity:update", "position:opened", "position:closed"].includes(msg.event))
651
+ feed.prepend(row);
652
+ while (feed.children.length > 120) feed.removeChild(feed.lastChild);
653
+ if (["order:filled", "position:opened", "position:closed", "risk:halt"].includes(m.event))
170
654
  pollState();
171
655
  };
656
+
657
+ pollState();
658
+ setInterval(pollState, 3000);
172
659
  </script>
173
660
  </body>
174
661
  </html>