tradelab 1.0.1 → 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.
Files changed (66) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +188 -328
  3. package/bin/tradelab-mcp.js +7 -0
  4. package/bin/tradelab.js +29 -0
  5. package/dist/cjs/data.cjs +149 -26
  6. package/dist/cjs/index.cjs +1917 -1005
  7. package/dist/cjs/live.cjs +536 -25
  8. package/dist/cjs/ta.cjs +339 -0
  9. package/docs/README.md +32 -66
  10. package/docs/api-reference.md +283 -112
  11. package/docs/backtest-engine.md +210 -252
  12. package/docs/data-reporting-cli.md +114 -156
  13. package/docs/examples.md +6 -6
  14. package/docs/live-trading.md +263 -92
  15. package/docs/mcp.md +285 -0
  16. package/docs/research.md +157 -0
  17. package/examples/liveDashboard.js +33 -0
  18. package/examples/llmSignal.js +33 -0
  19. package/examples/mcpLiveTrading.js +77 -0
  20. package/examples/optimize.js +25 -0
  21. package/package.json +26 -4
  22. package/src/engine/asyncSignal.js +28 -0
  23. package/src/engine/backtest.js +13 -1
  24. package/src/engine/backtestAsync.js +27 -0
  25. package/src/engine/backtestTicks.js +13 -2
  26. package/src/engine/barSystemRunner.js +96 -41
  27. package/src/engine/execution.js +39 -0
  28. package/src/engine/grid.js +15 -0
  29. package/src/engine/llmSignal.js +84 -0
  30. package/src/engine/optimize.js +110 -0
  31. package/src/engine/optimizeWorker.js +67 -0
  32. package/src/engine/portfolio.js +4 -1
  33. package/src/engine/walkForward.js +1 -0
  34. package/src/index.js +9 -0
  35. package/src/live/dashboard/server.js +179 -0
  36. package/src/live/engine/liveEngine.js +2 -2
  37. package/src/live/engine/paperEngine.js +5 -0
  38. package/src/live/index.js +3 -0
  39. package/src/live/session.js +402 -0
  40. package/src/mcp/liveTools.js +179 -0
  41. package/src/mcp/schemas.js +167 -0
  42. package/src/mcp/server.js +35 -0
  43. package/src/mcp/tools.js +265 -0
  44. package/src/metrics/annualize.js +32 -0
  45. package/src/metrics/benchmark.js +55 -0
  46. package/src/metrics/buildMetrics.js +34 -13
  47. package/src/metrics/finite.js +17 -0
  48. package/src/research/combinations.js +18 -0
  49. package/src/research/cpcv.js +47 -0
  50. package/src/research/deflatedSharpe.js +35 -0
  51. package/src/research/index.js +6 -0
  52. package/src/research/monteCarlo.js +88 -0
  53. package/src/research/pbo.js +69 -0
  54. package/src/research/stats.js +78 -0
  55. package/src/strategies/builtins.js +96 -0
  56. package/src/strategies/index.js +30 -0
  57. package/src/ta/channels.js +67 -0
  58. package/src/ta/index.js +16 -0
  59. package/src/ta/oscillators.js +70 -0
  60. package/src/ta/trend.js +78 -0
  61. package/src/utils/random.js +33 -0
  62. package/templates/dashboard.html +661 -0
  63. package/types/index.d.ts +179 -0
  64. package/types/live.d.ts +114 -0
  65. package/types/mcp.d.ts +17 -0
  66. package/types/ta.d.ts +45 -0
@@ -0,0 +1,661 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>tradelab · live</title>
7
+ <style>
8
+ :root {
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;
25
+ }
26
+ body {
27
+ margin: 0;
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);
46
+ }
47
+
48
+ header {
49
+ display: flex;
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);
138
+ }
139
+
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;
152
+ }
153
+
154
+ main {
155
+ padding: 20px;
156
+ display: grid;
157
+ gap: 16px;
158
+ max-width: 1400px;
159
+ margin: 0 auto;
160
+ }
161
+ .kpis {
162
+ display: grid;
163
+ grid-template-columns: repeat(4, 1fr);
164
+ gap: 12px;
165
+ }
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;
174
+ text-transform: uppercase;
175
+ letter-spacing: 0.08em;
176
+ color: var(--muted);
177
+ }
178
+ .kpi .val {
179
+ font-family: var(--mono);
180
+ font-size: 28px;
181
+ font-weight: 600;
182
+ margin-top: 6px;
183
+ font-variant-numeric: tabular-nums;
184
+ }
185
+ .kpi .sub {
186
+ font-family: var(--mono);
187
+ font-size: 12px;
188
+ margin-top: 2px;
189
+ color: var(--muted);
190
+ }
191
+
192
+ .card {
193
+ background: var(--surface);
194
+ border: 1px solid var(--line);
195
+ border-radius: 10px;
196
+ }
197
+ .card > h2 {
198
+ margin: 0;
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);
206
+ display: flex;
207
+ justify-content: space-between;
208
+ align-items: center;
209
+ }
210
+ #chartCard canvas {
211
+ display: block;
212
+ width: 100%;
213
+ height: 200px;
214
+ }
215
+
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);
273
+ }
274
+
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
+ }
328
+ }
329
+ </style>
330
+ </head>
331
+ <body>
332
+ <header>
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>
342
+ </header>
343
+
344
+ <div class="halt-banner" id="halt">
345
+ <span>&#9888;</span><span id="haltMsg">Risk halt active</span>
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
+
395
+ <script>
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
+ }
513
+
514
+ function applyState(s) {
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
+ );
594
+ }
595
+
596
+ async function pollState() {
597
+ try {
598
+ applyState(await (await fetch("/state")).json());
599
+ } catch {}
600
+ }
601
+
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
+ });
616
+
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");
628
+ const es = new EventSource("/events");
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
+ };
637
+ es.onmessage = (e) => {
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 =
644
+ "<time>" +
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 ?? "") +
650
+ "</span>";
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))
654
+ pollState();
655
+ };
656
+
657
+ pollState();
658
+ setInterval(pollState, 3000);
659
+ </script>
660
+ </body>
661
+ </html>