Almaatla commited on
Commit
09ff30e
·
verified ·
1 Parent(s): f8c3d6a

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +261 -285
index.html CHANGED
@@ -11,22 +11,61 @@
11
  }
12
  *{ box-sizing:border-box; }
13
  body{ margin:0; font-family:system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; background:var(--bg); color:var(--text); }
14
- header{ height:52px; display:flex; align-items:center; justify-content:space-between; padding:0 14px; border-bottom:1px solid var(--line); background:rgba(0,0,0,0.15); }
15
- .title{ font-weight:800; letter-spacing:0.3px; }
16
- .status{ font-size:12px; color:var(--muted); display:flex; align-items:center; gap:8px; }
 
 
 
 
 
 
17
  .dot{ width:10px; height:10px; border-radius:999px; background:#6b7280; }
18
  .dot.connected{ background:var(--green); }
19
  .dot.connecting{ background:var(--yellow); }
20
  .dot.error{ background:var(--red); }
21
 
22
- main{ display:grid; grid-template-columns: 1.4fr 1fr; gap:12px; padding:12px; height: calc(100vh - 52px); }
23
- .col{ display:flex; flex-direction:column; gap:12px; min-height:0; }
24
- .card{ background:var(--panel); border:1px solid var(--line); border-radius:10px; overflow:hidden; display:flex; flex-direction:column; min-height:0; }
25
- .card h2{ margin:0; padding:10px 12px; font-size:13px; color:var(--muted); text-transform:uppercase; letter-spacing:0.08em; border-bottom:1px solid var(--line); background:var(--panel2); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  .content{ padding:12px; overflow:auto; min-height:0; }
27
 
28
  .grid3{ display:grid; grid-template-columns: 1fr 1fr 1fr; gap:10px; }
29
  .grid2{ display:grid; grid-template-columns: 1fr 1fr; gap:10px; }
 
30
  .kpi{ padding:10px; border:1px solid var(--line); border-radius:10px; background:rgba(255,255,255,0.02); }
31
  .kpi .label{ color:var(--muted); font-size:12px; }
32
  .kpi .value{ font-size:18px; font-weight:800; margin-top:4px; }
@@ -37,7 +76,7 @@
37
  font:inherit; border-radius:10px; border:1px solid var(--line);
38
  background:#0c1430; color:var(--text); padding:10px 12px;
39
  }
40
- input{ width:120px; }
41
  button{ cursor:pointer; }
42
  button.buy{ border-color:rgba(34,197,94,0.5); }
43
  button.sell{ border-color:rgba(239,68,68,0.5); }
@@ -51,7 +90,8 @@
51
  .small{ font-size:12px; color:var(--muted); }
52
  .mono{ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
53
 
54
- canvas{ width:100%; height:220px; background:rgba(0,0,0,0.10); border:1px solid rgba(35,48,85,0.8); border-radius:10px; display:block; }
 
55
 
56
  table{ width:100%; border-collapse:collapse; font-size:13px; }
57
  th, td{ text-align:left; padding:8px 6px; border-bottom:1px solid rgba(35,48,85,0.6); vertical-align:top; }
@@ -61,15 +101,25 @@
61
  .pill.buy{ color:var(--green); border-color:rgba(34,197,94,0.5); }
62
  .pill.sell{ color:var(--red); border-color:rgba(239,68,68,0.5); }
63
 
64
- @media (max-width: 900px){
65
- main{ grid-template-columns: 1fr; height:auto; }
 
 
66
  canvas{ height:200px; }
67
  }
68
  </style>
69
  </head>
 
70
  <body>
71
  <header>
72
- <div class="title">MPTrading</div>
 
 
 
 
 
 
 
73
  <div class="status">
74
  <span id="dot" class="dot"></span>
75
  <span id="statusText">Disconnected</span>
@@ -78,97 +128,139 @@
78
  </header>
79
 
80
  <main>
81
- <div class="col">
82
- <section class="card">
83
- <h2>Market</h2>
84
- <div class="content">
85
- <div class="grid3">
86
- <div class="kpi">
87
- <div class="label">Day / Price</div>
88
- <div class="value" id="kpiPrice">--</div>
89
- <div class="sub"><span class="muted">Day:</span> <span id="kpiDay">--</span></div>
90
- </div>
91
- <div class="kpi">
92
- <div class="label">Indicators</div>
93
- <div class="sub"><span class="muted">MA20:</span> <span id="kpiMA20">--</span></div>
94
- <div class="sub"><span class="muted">MA50:</span> <span id="kpiMA50">--</span></div>
95
- <div class="sub"><span class="muted">RSI14:</span> <span id="kpiRSI">--</span></div>
96
- </div>
97
- <div class="kpi">
98
- <div class="label">News</div>
99
- <div id="newsText" class="sub">No news yet.</div>
 
 
 
 
100
  </div>
101
- </div>
102
 
103
- <div style="height:10px"></div>
104
- <canvas id="priceCanvas" width="1200" height="440"></canvas>
105
- <div class="small muted" style="margin-top:8px;">
106
- Price (white) · MA20 (blue) · MA50 (yellow) · RSI14 shown in metrics.
 
107
  </div>
108
  </div>
109
- </section>
110
 
111
- <section class="card">
112
- <h2>Trading & Risk</h2>
113
- <div class="content">
114
- <div class="grid3">
115
- <div class="kpi">
116
- <div class="label">Cash / Equity</div>
117
- <div class="sub"><span class="muted">Cash:</span> <span id="kpiCash">--</span></div>
118
- <div class="sub"><span class="muted">Equity:</span> <span id="kpiEquity">--</span></div>
119
- <div class="sub"><span class="muted">ROI:</span> <span id="kpiRoi">--</span></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  </div>
121
- <div class="kpi">
122
- <div class="label">Position</div>
123
- <div class="sub"><span class="muted">Shares:</span> <span id="kpiPos">--</span></div>
124
- <div class="sub"><span class="muted">Avg cost:</span> <span id="kpiAvg">--</span></div>
125
- <div class="sub"><span class="muted">Break-even:</span> <span id="kpiBE">--</span></div>
126
- <div class="sub"><span class="muted">Unrealized P&L:</span> <span id="kpiUPnL">--</span></div>
127
  </div>
128
- <div class="kpi">
129
- <div class="label">Margin</div>
130
- <div class="sub"><span class="muted">Max leverage:</span> <span class="mono" id="kpiLevMax">--</span></div>
131
- <div class="sub"><span class="muted">Used:</span> <span class="mono" id="kpiMarginUsed">--</span></div>
132
- <div class="sub"><span class="muted">Utilization:</span> <span class="mono" id="kpiMarginPct">--</span></div>
133
- <div class="sub"><span class="muted">Borrow fee (short):</span> <span class="mono" id="kpiBorrowFee">--</span></div>
134
  </div>
135
- </div>
136
 
137
- <div style="height:12px"></div>
 
 
 
 
 
 
 
 
 
138
 
139
- <div id="warnBox" class="warn" style="display:none;">
140
- <div class="small"><b>Margin warning</b>: utilization is high. Consider reducing position.</div>
 
141
  </div>
142
- <div id="liqBox" class="danger" style="display:none;">
143
- <div class="small"><b>Liquidated</b>: equity is below zero. Trading is disabled for this player.</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  </div>
 
145
 
146
- <div style="height:12px"></div>
147
-
148
- <div class="row">
149
- <label class="small muted">Qty</label>
150
- <input id="qty" type="number" value="100" min="1" step="1"/>
151
- <button class="buy" id="buyBtn">Buy (B)</button>
152
- <button class="sell" id="sellBtn">Sell (S)</button>
153
- <button class="secondary" id="closeBtn">Close pos (C)</button>
154
- <button class="secondary" id="resetBtn" title="Reset local portfolio only">Reset local</button>
155
- </div>
156
 
157
- <div style="height:12px"></div>
158
-
159
- <div class="grid2">
160
- <div class="kpi">
161
- <div class="label">Stops</div>
162
- <div class="row" style="margin-top:8px;">
163
- <label class="small muted">Stop-loss</label>
164
- <input id="stopLoss" type="number" placeholder="price" step="0.01"/>
165
- <label class="small muted">Take-profit</label>
166
- <input id="takeProfit" type="number" placeholder="price" step="0.01"/>
167
- <button class="secondary" id="setStopsBtn">Set</button>
168
- <button class="secondary" id="clearStopsBtn">Clear</button>
169
- </div>
170
- <div class="sub"><span class="muted">Active SL:</span> <span id="kpiSL">--</span> · <span class="muted">TP:</span> <span id="kpiTP">--</span></div>
171
- </div>
172
 
173
  <div class="kpi">
174
  <div class="label">Performance (session)</div>
@@ -178,80 +270,43 @@
178
  <div class="sub"><span class="muted">Sharpe (est):</span> <span id="mSharpe">--</span></div>
179
  </div>
180
  </div>
181
-
182
- <div style="height:10px"></div>
183
- <canvas id="equityCanvas" width="1200" height="320"></canvas>
184
- <div class="small muted" style="margin-top:8px;">Equity curve (last 200 points).</div>
185
  </div>
186
- </section>
187
- </div>
188
 
189
- <div class="col">
190
- <section class="card">
191
- <h2>Leaderboard</h2>
192
- <div class="content">
193
- <table>
194
- <thead>
195
- <tr>
196
- <th>Name</th>
197
- <th class="right">Equity</th>
198
- <th class="right">ROI %</th>
199
- </tr>
200
- </thead>
201
- <tbody id="lbBody">
202
- <tr><td colspan="3" class="muted">Waiting for ticks…</td></tr>
203
- </tbody>
204
- </table>
205
- </div>
206
- </section>
207
-
208
- <section class="card">
209
- <h2>Trade history</h2>
210
- <div class="content">
211
- <table>
212
- <thead>
213
- <tr>
214
- <th>Action</th>
215
- <th class="right">Qty</th>
216
- <th class="right">Price</th>
217
- <th class="right">Day</th>
218
- <th class="right">Realized P&L</th>
219
- </tr>
220
- </thead>
221
- <tbody id="tradesBody">
222
- <tr><td colspan="5" class="muted">No trades yet.</td></tr>
223
- </tbody>
224
- </table>
225
  </div>
226
- </section>
227
- </div>
228
  </main>
229
 
230
  <script>
231
- // -----------------------------
232
- // Constants / risk parameters
233
- // -----------------------------
234
  const INITIAL_CASH = 10000;
235
  const COMMISSION = 5;
236
 
237
- // Leverage / margin model (simple, game-friendly):
238
- // - Exposure = |shares| * price
239
- // - Required margin = Exposure / MAX_LEVERAGE
240
- // - Used margin % = Required margin / equity
241
- // - Margin warning above 80%, liquidation at equity <= 0
242
  const MAX_LEVERAGE = 3.0;
 
243
 
244
- // Borrow cost for short positions (simple):
245
- // daily borrow fee = BORROW_RATE_PER_DAY * exposure
246
- const BORROW_RATE_PER_DAY = 0.0002; // 2 bps per day on short exposure
247
-
248
- // Chart window sizes
249
  const PRICE_WINDOW = 120;
250
  const EQUITY_WINDOW = 200;
251
 
252
- // -----------------------------
253
- // State
254
- // -----------------------------
255
  const state = {
256
  ws: null,
257
  wsStatus: "DISCONNECTED",
@@ -262,7 +317,7 @@
262
  price: 0,
263
 
264
  cash: INITIAL_CASH,
265
- shares: 0, // can be negative (short)
266
  avgCost: 0,
267
 
268
  equity: INITIAL_CASH,
@@ -272,9 +327,9 @@
272
  stopLoss: null,
273
  takeProfit: null,
274
 
275
- equityCurve: [], // {day, equity}
276
- tradeLog: [], // executed trades
277
- openLots: [], // FIFO lots for realized P&L: [{qty, price}] qty sign follows position direction
278
 
279
  leaderboard: [],
280
 
@@ -282,9 +337,7 @@
282
  liquidationDay: null
283
  };
284
 
285
- // -----------------------------
286
- // DOM helpers
287
- // -----------------------------
288
  const $ = (id) => document.getElementById(id);
289
 
290
  function fmtMoney(x){ return Number.isFinite(x) ? x.toLocaleString(undefined,{maximumFractionDigits:0}) : "--"; }
@@ -311,9 +364,27 @@
311
  }[c]));
312
  }
313
 
314
- // -----------------------------
315
- // Indicators
316
- // -----------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  function sma(values, period){
318
  if (values.length < period) return null;
319
  let sum = 0;
@@ -336,19 +407,13 @@
336
  return 100 - (100 / (1 + rs));
337
  }
338
 
339
- // -----------------------------
340
- // Charts (Canvas API)
341
- // -----------------------------
342
- // Canvas is designed for drawing/visualization via JS. [web:1]
343
  function drawLineChart(canvas, series, opts){
344
  const ctx = canvas.getContext("2d");
345
  const w = canvas.width, h = canvas.height;
346
  ctx.clearRect(0,0,w,h);
347
-
348
- // background
349
  ctx.fillStyle = "rgba(0,0,0,0.08)";
350
  ctx.fillRect(0,0,w,h);
351
-
352
  if (!series || series.length < 2) return;
353
 
354
  const pad = 30;
@@ -360,7 +425,6 @@
360
  const yr = (ymax - ymin) || 1;
361
  const xr = (xmax - xmin) || 1;
362
 
363
- // grid
364
  ctx.strokeStyle = "rgba(170,178,213,0.12)";
365
  ctx.lineWidth = 1;
366
  for (let i=0;i<=5;i++){
@@ -374,7 +438,6 @@
374
  const toX = (x) => pad + (w-2*pad) * ((x - xmin)/xr);
375
  const toY = (y) => h - pad - (h-2*pad) * ((y - ymin)/yr);
376
 
377
- // main line
378
  ctx.strokeStyle = opts.color || "#ffffff";
379
  ctx.lineWidth = opts.width || 2;
380
  ctx.beginPath();
@@ -386,7 +449,6 @@
386
  }
387
  ctx.stroke();
388
 
389
- // optional overlays
390
  if (opts.overlays){
391
  for (const ov of opts.overlays){
392
  if (!ov.series || ov.series.length < 2) continue;
@@ -403,27 +465,15 @@
403
  }
404
  }
405
 
406
- // label
407
  ctx.fillStyle = "rgba(170,178,213,0.8)";
408
  ctx.font = "18px ui-monospace, Menlo, Consolas";
409
  ctx.fillText(opts.label || "", pad, 22);
410
  }
411
 
412
- // -----------------------------
413
- // Portfolio / risk computations
414
- // -----------------------------
415
- function exposure(){
416
- return Math.abs(state.shares) * state.price;
417
- }
418
-
419
- function requiredMargin(){
420
- return exposure() / MAX_LEVERAGE;
421
- }
422
-
423
- function marginUtil(){
424
- if (state.equity <= 0) return 1;
425
- return requiredMargin() / state.equity;
426
- }
427
 
428
  function applyBorrowFeeOneDay(){
429
  if (state.shares < 0){
@@ -432,20 +482,22 @@
432
  }
433
  }
434
 
 
 
 
 
 
435
  function recalc(){
436
- // borrow fee once per tick/day
437
  applyBorrowFeeOneDay();
438
 
439
- // unrealized P&L; works for long and short
440
  state.upnl = (state.price - state.avgCost) * state.shares;
441
-
442
  state.equity = state.cash + state.shares * state.price;
443
  state.roi = ((state.equity - INITIAL_CASH) / INITIAL_CASH) * 100;
444
 
445
- // liquidation: equity <= 0 => stop trading and stop reporting
446
  if (!state.liquidated && state.equity <= 0){
447
  state.liquidated = true;
448
  state.liquidationDay = state.day;
 
449
  $("liqBox").style.display = "block";
450
  $("buyBtn").disabled = true;
451
  $("sellBtn").disabled = true;
@@ -457,15 +509,11 @@
457
  $("takeProfit").disabled = true;
458
  }
459
 
460
- // margin warning
461
- const mu = marginUtil();
462
- $("warnBox").style.display = (!state.liquidated && mu >= 0.80) ? "block" : "none";
463
 
464
- // push equity curve
465
  state.equityCurve.push({ day: state.day, equity: state.equity });
466
  if (state.equityCurve.length > 5000) state.equityCurve.shift();
467
 
468
- // stops: only when position exists and not liquidated
469
  if (!state.liquidated && state.shares !== 0){
470
  if (state.stopLoss !== null){
471
  if (state.shares > 0 && state.price <= state.stopLoss) closePosition("STOP");
@@ -478,25 +526,10 @@
478
  }
479
  }
480
 
481
- function breakEvenPrice(){
482
- if (state.shares === 0) return null;
483
- // With commission and borrow fee, BE is approximate; use avgCost as BE reference.
484
- return state.avgCost;
485
- }
486
-
487
- // -----------------------------
488
- // Realized P&L (FIFO lots)
489
- // -----------------------------
490
- // openLots contains lots with qty sign matching the position direction when opened:
491
- // long lots: qty > 0, short lots: qty < 0
492
- function addLot(qty, price){
493
- state.openLots.push({ qty, price });
494
- }
495
 
496
  function consumeLots(closeQty, closePrice){
497
- // closeQty has opposite sign of the lots being closed? We'll pass closeQty with same sign as reduction:
498
- // Example: if closing a long by selling 100, closeQty = -100; we reduce lots with qty>0.
499
- // Example: if closing a short by buying 100, closeQty = +100; we reduce lots with qty<0.
500
  let remaining = Math.abs(closeQty);
501
  let realized = 0;
502
 
@@ -505,12 +538,9 @@
505
  const lotAbs = Math.abs(lot.qty);
506
  const take = Math.min(lotAbs, remaining);
507
 
508
- // long lot (qty>0) closed by sell at closePrice => pnl = (closePrice - lot.price)*take
509
- // short lot (qty<0) closed by buy at closePrice => pnl = (lot.price - closePrice)*take
510
  if (lot.qty > 0) realized += (closePrice - lot.price) * take;
511
  else realized += (lot.price - closePrice) * take;
512
 
513
- // shrink lot
514
  const newAbs = lotAbs - take;
515
  if (newAbs === 0) state.openLots.splice(i,1);
516
  else {
@@ -522,25 +552,18 @@
522
  return realized;
523
  }
524
 
525
- // -----------------------------
526
- // Trading
527
- // -----------------------------
528
  function canPlaceTrade(tradeShares){
529
  if (state.liquidated) return false;
530
  if (!Number.isFinite(state.price) || state.price <= 0) return false;
531
 
532
- // prospective state to enforce max leverage (simple)
533
  const px = state.price;
534
  const newShares = state.shares + tradeShares;
535
  const newExposure = Math.abs(newShares) * px;
536
-
537
- // Use current equity (approx) as base; after cash delta it changes slightly,
538
- // but this is good enough for a game.
539
  const req = newExposure / MAX_LEVERAGE;
 
540
  if (state.equity <= 0) return false;
541
- // Require equity >= required margin
542
  if (state.equity < req) return false;
543
-
544
  return true;
545
  }
546
 
@@ -554,27 +577,20 @@
554
 
555
  const px = state.price;
556
 
557
- // cashDelta = -(tradeShares*px) - commission
558
  const cashDelta = -(tradeShares * px) - COMMISSION;
559
 
560
- // Realized P&L occurs when trade reduces opposite direction position
561
  let realized = 0;
562
-
563
  const prevShares = state.shares;
564
  const newShares = prevShares + tradeShares;
565
-
566
  const prevDir = Math.sign(prevShares);
567
  const tradeDir = Math.sign(tradeShares);
568
 
569
- // If position existed and trade is opposite direction => closing some
570
  if (prevShares !== 0 && prevDir !== tradeDir){
571
  realized = consumeLots(tradeShares, px);
572
  }
573
 
574
- // Update cash
575
  state.cash += cashDelta;
576
 
577
- // Update shares + avgCost and openLots
578
  if (newShares === 0){
579
  state.shares = 0;
580
  state.avgCost = 0;
@@ -587,33 +603,26 @@
587
  const sameDir = (prevShares > 0 && newShares > 0) || (prevShares < 0 && newShares < 0);
588
 
589
  if (sameDir && prevDir === tradeDir){
590
- // adding to same direction
591
  addLot(tradeShares, px);
592
- // recompute avgCost from lots (robust)
593
  const totAbs = state.openLots.reduce((s,l)=>s+Math.abs(l.qty),0);
594
  const wavg = state.openLots.reduce((s,l)=>s+Math.abs(l.qty)*l.price,0) / (totAbs || 1);
595
  state.shares = newShares;
596
  state.avgCost = wavg;
597
  } else if (!sameDir){
598
- // we either reduced or flipped
599
  state.shares = newShares;
600
 
601
  if (Math.sign(newShares) === 0){
602
  state.avgCost = 0;
603
  state.openLots = [];
604
  } else if (Math.sign(newShares) !== prevDir){
605
- // flipped: open a new lot for the remainder beyond zero
606
- // Determine remainder size: abs(newShares)
607
  state.openLots = [{ qty: newShares, price: px }];
608
  state.avgCost = px;
609
  } else {
610
- // reduced but still same original direction; avgCost from remaining lots
611
  const totAbs = state.openLots.reduce((s,l)=>s+Math.abs(l.qty),0);
612
  const wavg = totAbs ? (state.openLots.reduce((s,l)=>s+Math.abs(l.qty)*l.price,0) / totAbs) : 0;
613
  state.avgCost = wavg;
614
  }
615
  } else {
616
- // reduced but stayed same direction (tradeDir != prevDir can't happen in sameDir)
617
  state.shares = newShares;
618
  }
619
  }
@@ -621,7 +630,6 @@
621
  state.tradeLog.push({ action, qty, price: px, day: state.day, realized: realized - COMMISSION });
622
  if (state.tradeLog.length > 5000) state.tradeLog.shift();
623
 
624
- // Recalc (includes liquidation checks & stops)
625
  recalc();
626
  renderAll();
627
  }
@@ -630,20 +638,14 @@
630
  if (state.liquidated) return;
631
  if (state.shares === 0) return;
632
 
633
- // To close: trade opposite direction of current shares
634
  const qty = Math.abs(state.shares);
635
  const action = (state.shares > 0) ? "SELL" : "BUY";
636
- // Mark it as a special trade entry by adding reason into action field
637
- // We'll still call trade() to do proper P&L.
638
  trade(action, qty);
639
- // tag last entry
640
  const last = state.tradeLog[state.tradeLog.length - 1];
641
  if (last) last.action = reason;
642
  }
643
 
644
- // -----------------------------
645
- // Rendering
646
- // -----------------------------
647
  function renderLeaderboard(){
648
  const lb = $("lbBody");
649
  lb.innerHTML = "";
@@ -670,7 +672,7 @@
670
  tb.innerHTML = `<tr><td colspan="5" class="muted">No trades yet.</td></tr>`;
671
  return;
672
  }
673
- for (const t of state.tradeLog.slice().reverse().slice(0,200)){
674
  const tr = document.createElement("tr");
675
  const side = (t.action === "BUY") ? `<span class="pill buy">BUY</span>` :
676
  (t.action === "SELL") ? `<span class="pill sell">SELL</span>` :
@@ -688,9 +690,7 @@
688
  }
689
 
690
  function renderMetrics(){
691
- const trades = state.tradeLog.filter(t => ["BUY","SELL","CLOSE","STOP","TP"].includes(t.action) || t.action === "BUY" || t.action === "SELL" || t.action === "STOP" || t.action === "TP");
692
- const realized = trades.map(t => t.realized);
693
-
694
  const n = realized.length;
695
  $("mTrades").textContent = n;
696
 
@@ -719,12 +719,9 @@
719
  $("mBest").textContent = fmtMoney(best);
720
  $("mWorst").textContent = fmtMoney(worst);
721
 
722
- // Simple Sharpe estimate from equity curve returns
723
  const eq = state.equityCurve.slice(-300).map(p => p.equity);
724
- if (eq.length < 3){
725
- $("mSharpe").textContent = "--";
726
- return;
727
- }
728
  const rets = [];
729
  for (let i=1;i<eq.length;i++){
730
  const r = (eq[i] - eq[i-1]) / Math.max(1e-9, eq[i-1]);
@@ -733,7 +730,7 @@
733
  const mean = rets.reduce((a,b)=>a+b,0)/rets.length;
734
  const varr = rets.reduce((s,x)=>s+(x-mean)*(x-mean),0)/Math.max(1, rets.length-1);
735
  const sd = Math.sqrt(varr);
736
- const sharpe = sd === 0 ? 0 : (mean / sd) * Math.sqrt(252); // annualized-ish
737
  $("mSharpe").textContent = sharpe.toFixed(2);
738
  }
739
 
@@ -770,16 +767,16 @@
770
  }
771
 
772
  function renderCharts(){
 
 
773
  const closes = state.market.map(p => p.close);
774
 
775
- // price window
776
  const start = Math.max(0, state.day - PRICE_WINDOW + 1);
777
  const end = state.day + 1;
778
  const windowData = state.market.slice(start, end);
779
 
780
  const priceSeries = windowData.map(p => ({ x: p.i, y: p.close }));
781
 
782
- // MA overlays
783
  const ma20 = [];
784
  const ma50 = [];
785
  for (let i=start;i<end;i++){
@@ -799,16 +796,10 @@
799
  ]
800
  });
801
 
802
- // equity curve
803
  const eqWin = state.equityCurve.slice(-EQUITY_WINDOW);
804
  const eqSeries = eqWin.map(p => ({ x: p.day, y: p.equity }));
805
- drawLineChart($("equityCanvas"), eqSeries, {
806
- label: "EQUITY",
807
- color: "#22c55e",
808
- overlays: []
809
- });
810
 
811
- // indicator KPI values
812
  const slice = closes.slice(0, state.day+1);
813
  const s20 = sma(slice, 20);
814
  const s50 = sma(slice, 50);
@@ -826,9 +817,7 @@
826
  renderMetrics();
827
  }
828
 
829
- // -----------------------------
830
- // Stops UI
831
- // -----------------------------
832
  $("setStopsBtn").addEventListener("click", () => {
833
  if (state.liquidated) return;
834
  const sl = ($("stopLoss").value || "").trim();
@@ -846,9 +835,7 @@
846
  renderKPIs();
847
  });
848
 
849
- // -----------------------------
850
- // Controls + shortcuts
851
- // -----------------------------
852
  function qtyVal(){ return Number($("qty").value); }
853
 
854
  $("buyBtn").addEventListener("click", () => trade("BUY", qtyVal()));
@@ -856,7 +843,6 @@
856
  $("closeBtn").addEventListener("click", () => closePosition("CLOSE"));
857
 
858
  $("resetBtn").addEventListener("click", () => {
859
- // local reset only
860
  state.cash = INITIAL_CASH;
861
  state.shares = 0;
862
  state.avgCost = 0;
@@ -883,7 +869,6 @@
883
 
884
  $("stopLoss").value = "";
885
  $("takeProfit").value = "";
886
- $("newsText").textContent = "No news yet.";
887
 
888
  recalc();
889
  renderAll();
@@ -902,7 +887,6 @@
902
  if (k === "5") $("qty").value = "500";
903
  });
904
 
905
- // Make quantity input less "buggy": clamp and sanitize
906
  $("qty").addEventListener("input", () => {
907
  let v = Math.floor(Number($("qty").value));
908
  if (!Number.isFinite(v) || v < 1) v = 1;
@@ -910,9 +894,7 @@
910
  $("qty").value = String(v);
911
  });
912
 
913
- // -----------------------------
914
- // WebSocket
915
- // -----------------------------
916
  function connectWS(){
917
  const isLocalhost = (location.hostname === "localhost" || location.hostname === "127.0.0.1");
918
  const protocol = isLocalhost ? "ws" : "wss";
@@ -961,24 +943,18 @@
961
  ws.onclose = () => setStatus("DISCONNECTED");
962
  }
963
 
964
- // Send UPDATE_EQUITY every second when open, unless liquidated
965
  setInterval(() => {
966
  if (state.liquidated) return;
967
  if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return;
968
 
969
  state.ws.send(JSON.stringify({
970
  type: "UPDATE_EQUITY",
971
- payload: {
972
- name: state.clientId,
973
- equity: state.equity,
974
- roi: state.roi
975
- }
976
  }));
977
  }, 1000);
978
 
979
- // -----------------------------
980
- // Boot
981
- // -----------------------------
982
  setStatus("DISCONNECTED");
983
  renderAll();
984
  connectWS();
 
11
  }
12
  *{ box-sizing:border-box; }
13
  body{ margin:0; font-family:system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; background:var(--bg); color:var(--text); }
14
+
15
+ header{
16
+ height:52px; display:flex; align-items:center; justify-content:space-between;
17
+ padding:0 14px; border-bottom:1px solid var(--line); background:rgba(0,0,0,0.15);
18
+ gap:10px;
19
+ }
20
+ .leftHead{ display:flex; align-items:center; gap:12px; min-width: 0; }
21
+ .title{ font-weight:800; letter-spacing:0.3px; white-space:nowrap; }
22
+ .status{ font-size:12px; color:var(--muted); display:flex; align-items:center; gap:8px; white-space:nowrap; }
23
  .dot{ width:10px; height:10px; border-radius:999px; background:#6b7280; }
24
  .dot.connected{ background:var(--green); }
25
  .dot.connecting{ background:var(--yellow); }
26
  .dot.error{ background:var(--red); }
27
 
28
+ .tabs{ display:flex; gap:8px; flex-wrap:wrap; }
29
+ .tabBtn{
30
+ font:inherit; border-radius:10px; border:1px solid var(--line);
31
+ background:#0c1430; color:var(--text); padding:8px 10px; cursor:pointer;
32
+ font-size:13px;
33
+ }
34
+ .tabBtn.active{ background:rgba(96,165,250,0.14); border-color:rgba(96,165,250,0.55); }
35
+
36
+ main{ padding:12px; height: calc(100vh - 52px); }
37
+ .tabPanel{ display:none; height:100%; }
38
+ .tabPanel.active{ display:block; }
39
+
40
+ .gridMain{
41
+ display:grid;
42
+ grid-template-columns: 1.35fr 1fr; /* Market + Trading side by side */
43
+ gap:12px;
44
+ height:100%;
45
+ min-height:0;
46
+ }
47
+ .gridSecondary{
48
+ display:grid;
49
+ grid-template-columns: 1fr 1fr;
50
+ gap:12px;
51
+ height:100%;
52
+ min-height:0;
53
+ }
54
+
55
+ .card{
56
+ background:var(--panel); border:1px solid var(--line); border-radius:10px;
57
+ overflow:hidden; display:flex; flex-direction:column; min-height:0;
58
+ }
59
+ .card h2{
60
+ margin:0; padding:10px 12px; font-size:13px; color:var(--muted);
61
+ text-transform:uppercase; letter-spacing:0.08em;
62
+ border-bottom:1px solid var(--line); background:var(--panel2);
63
+ }
64
  .content{ padding:12px; overflow:auto; min-height:0; }
65
 
66
  .grid3{ display:grid; grid-template-columns: 1fr 1fr 1fr; gap:10px; }
67
  .grid2{ display:grid; grid-template-columns: 1fr 1fr; gap:10px; }
68
+
69
  .kpi{ padding:10px; border:1px solid var(--line); border-radius:10px; background:rgba(255,255,255,0.02); }
70
  .kpi .label{ color:var(--muted); font-size:12px; }
71
  .kpi .value{ font-size:18px; font-weight:800; margin-top:4px; }
 
76
  font:inherit; border-radius:10px; border:1px solid var(--line);
77
  background:#0c1430; color:var(--text); padding:10px 12px;
78
  }
79
+ input{ width:130px; }
80
  button{ cursor:pointer; }
81
  button.buy{ border-color:rgba(34,197,94,0.5); }
82
  button.sell{ border-color:rgba(239,68,68,0.5); }
 
90
  .small{ font-size:12px; color:var(--muted); }
91
  .mono{ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
92
 
93
+ canvas{ width:100%; height:240px; background:rgba(0,0,0,0.10); border:1px solid rgba(35,48,85,0.8); border-radius:10px; display:block; }
94
+ #equityCanvas{ height:240px; }
95
 
96
  table{ width:100%; border-collapse:collapse; font-size:13px; }
97
  th, td{ text-align:left; padding:8px 6px; border-bottom:1px solid rgba(35,48,85,0.6); vertical-align:top; }
 
101
  .pill.buy{ color:var(--green); border-color:rgba(34,197,94,0.5); }
102
  .pill.sell{ color:var(--red); border-color:rgba(239,68,68,0.5); }
103
 
104
+ @media (max-width: 1000px){
105
+ .gridMain{ grid-template-columns: 1fr; height:auto; }
106
+ .gridSecondary{ grid-template-columns: 1fr; height:auto; }
107
+ main{ height:auto; }
108
  canvas{ height:200px; }
109
  }
110
  </style>
111
  </head>
112
+
113
  <body>
114
  <header>
115
+ <div class="leftHead">
116
+ <div class="title">MPTrading</div>
117
+ <div class="tabs">
118
+ <button class="tabBtn active" id="tabBtnTrade" type="button">Trading</button>
119
+ <button class="tabBtn" id="tabBtnStats" type="button">Stats</button>
120
+ </div>
121
+ </div>
122
+
123
  <div class="status">
124
  <span id="dot" class="dot"></span>
125
  <span id="statusText">Disconnected</span>
 
128
  </header>
129
 
130
  <main>
131
+ <!-- TAB 1: Trading (Primary) -->
132
+ <section class="tabPanel active" id="tabTrade">
133
+ <div class="gridMain">
134
+ <!-- Market side -->
135
+ <div class="card">
136
+ <h2>Price & Chart</h2>
137
+ <div class="content">
138
+ <div class="grid3">
139
+ <div class="kpi">
140
+ <div class="label">Day / Price</div>
141
+ <div class="value" id="kpiPrice">--</div>
142
+ <div class="sub"><span class="muted">Day:</span> <span id="kpiDay">--</span></div>
143
+ </div>
144
+ <div class="kpi">
145
+ <div class="label">Indicators</div>
146
+ <div class="sub"><span class="muted">MA20:</span> <span id="kpiMA20">--</span></div>
147
+ <div class="sub"><span class="muted">MA50:</span> <span id="kpiMA50">--</span></div>
148
+ <div class="sub"><span class="muted">RSI14:</span> <span id="kpiRSI">--</span></div>
149
+ </div>
150
+ <div class="kpi">
151
+ <div class="label">Latest news</div>
152
+ <div id="newsText" class="sub">No news yet.</div>
153
+ </div>
154
  </div>
 
155
 
156
+ <div style="height:10px"></div>
157
+ <canvas id="priceCanvas" width="1200" height="440"></canvas>
158
+ <div class="small muted" style="margin-top:8px;">
159
+ Price (white) · MA20 (blue) · MA50 (yellow).
160
+ </div>
161
  </div>
162
  </div>
 
163
 
164
+ <!-- Trading side -->
165
+ <div class="card">
166
+ <h2>Trading</h2>
167
+ <div class="content">
168
+ <div class="grid2">
169
+ <div class="kpi">
170
+ <div class="label">Cash / Equity</div>
171
+ <div class="sub"><span class="muted">Cash:</span> <span id="kpiCash">--</span></div>
172
+ <div class="sub"><span class="muted">Equity:</span> <span id="kpiEquity">--</span></div>
173
+ <div class="sub"><span class="muted">ROI:</span> <span id="kpiRoi">--</span></div>
174
+ </div>
175
+
176
+ <div class="kpi">
177
+ <div class="label">Position</div>
178
+ <div class="sub"><span class="muted">Shares:</span> <span id="kpiPos">--</span></div>
179
+ <div class="sub"><span class="muted">Avg cost:</span> <span id="kpiAvg">--</span></div>
180
+ <div class="sub"><span class="muted">Break-even:</span> <span id="kpiBE">--</span></div>
181
+ <div class="sub"><span class="muted">Unrealized P&L:</span> <span id="kpiUPnL">--</span></div>
182
+ </div>
183
+
184
+ <div class="kpi">
185
+ <div class="label">Margin</div>
186
+ <div class="sub"><span class="muted">Max leverage:</span> <span class="mono" id="kpiLevMax">--</span></div>
187
+ <div class="sub"><span class="muted">Used:</span> <span class="mono" id="kpiMarginUsed">--</span></div>
188
+ <div class="sub"><span class="muted">Utilization:</span> <span class="mono" id="kpiMarginPct">--</span></div>
189
+ <div class="sub"><span class="muted">Borrow fee (short):</span> <span class="mono" id="kpiBorrowFee">--</span></div>
190
+ </div>
191
+
192
+ <div class="kpi">
193
+ <div class="label">Stops</div>
194
+ <div class="row" style="margin-top:8px;">
195
+ <label class="small muted">Stop</label>
196
+ <input id="stopLoss" type="number" placeholder="price" step="0.01"/>
197
+ <label class="small muted">TP</label>
198
+ <input id="takeProfit" type="number" placeholder="price" step="0.01"/>
199
+ </div>
200
+ <div class="row" style="margin-top:8px;">
201
+ <button class="secondary" id="setStopsBtn" type="button">Set</button>
202
+ <button class="secondary" id="clearStopsBtn" type="button">Clear</button>
203
+ </div>
204
+ <div class="sub"><span class="muted">Active SL:</span> <span id="kpiSL">--</span> · <span class="muted">TP:</span> <span id="kpiTP">--</span></div>
205
+ </div>
206
  </div>
207
+
208
+ <div style="height:12px"></div>
209
+
210
+ <div id="warnBox" class="warn" style="display:none;">
211
+ <div class="small"><b>Margin warning</b>: utilization is high. Reduce exposure.</div>
 
212
  </div>
213
+ <div id="liqBox" class="danger" style="display:none;">
214
+ <div class="small"><b>Liquidated</b>: equity is below zero. Trading disabled.</div>
 
 
 
 
215
  </div>
 
216
 
217
+ <div style="height:12px"></div>
218
+
219
+ <div class="row">
220
+ <label class="small muted">Qty</label>
221
+ <input id="qty" type="number" value="100" min="1" step="1"/>
222
+ <button class="buy" id="buyBtn" type="button">Buy (B)</button>
223
+ <button class="sell" id="sellBtn" type="button">Sell (S)</button>
224
+ <button class="secondary" id="closeBtn" type="button">Close pos (C)</button>
225
+ <button class="secondary" id="resetBtn" type="button" title="Reset local portfolio only">Reset</button>
226
+ </div>
227
 
228
+ <div class="small muted" style="margin-top:10px;">
229
+ Shorting enabled. Leverage-limited. Keyboard: B/S/C, and 1..5 set quick quantities.
230
+ </div>
231
  </div>
232
+ </div>
233
+ </div>
234
+ </section>
235
+
236
+ <!-- TAB 2: Stats (Secondary) -->
237
+ <section class="tabPanel" id="tabStats">
238
+ <div class="gridSecondary">
239
+ <div class="card">
240
+ <h2>Leaderboard</h2>
241
+ <div class="content">
242
+ <table>
243
+ <thead>
244
+ <tr>
245
+ <th>Name</th>
246
+ <th class="right">Equity</th>
247
+ <th class="right">ROI %</th>
248
+ </tr>
249
+ </thead>
250
+ <tbody id="lbBody">
251
+ <tr><td colspan="3" class="muted">Waiting for ticks…</td></tr>
252
+ </tbody>
253
+ </table>
254
  </div>
255
+ </div>
256
 
257
+ <div class="card">
258
+ <h2>Equity curve</h2>
259
+ <div class="content">
260
+ <canvas id="equityCanvas" width="1200" height="320"></canvas>
261
+ <div class="small muted" style="margin-top:8px;">Equity curve (last 200 points).</div>
 
 
 
 
 
262
 
263
+ <div style="height:10px"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
  <div class="kpi">
266
  <div class="label">Performance (session)</div>
 
270
  <div class="sub"><span class="muted">Sharpe (est):</span> <span id="mSharpe">--</span></div>
271
  </div>
272
  </div>
 
 
 
 
273
  </div>
 
 
274
 
275
+ <div class="card" style="grid-column: 1 / -1;">
276
+ <h2>Trade history</h2>
277
+ <div class="content">
278
+ <table>
279
+ <thead>
280
+ <tr>
281
+ <th>Action</th>
282
+ <th class="right">Qty</th>
283
+ <th class="right">Price</th>
284
+ <th class="right">Day</th>
285
+ <th class="right">Realized P&L</th>
286
+ </tr>
287
+ </thead>
288
+ <tbody id="tradesBody">
289
+ <tr><td colspan="5" class="muted">No trades yet.</td></tr>
290
+ </tbody>
291
+ </table>
292
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  </div>
294
+ </div>
295
+ </section>
296
  </main>
297
 
298
  <script>
299
+ // --------- Constants / risk parameters ----------
 
 
300
  const INITIAL_CASH = 10000;
301
  const COMMISSION = 5;
302
 
 
 
 
 
 
303
  const MAX_LEVERAGE = 3.0;
304
+ const BORROW_RATE_PER_DAY = 0.0002;
305
 
 
 
 
 
 
306
  const PRICE_WINDOW = 120;
307
  const EQUITY_WINDOW = 200;
308
 
309
+ // --------- State ----------
 
 
310
  const state = {
311
  ws: null,
312
  wsStatus: "DISCONNECTED",
 
317
  price: 0,
318
 
319
  cash: INITIAL_CASH,
320
+ shares: 0,
321
  avgCost: 0,
322
 
323
  equity: INITIAL_CASH,
 
327
  stopLoss: null,
328
  takeProfit: null,
329
 
330
+ equityCurve: [],
331
+ tradeLog: [],
332
+ openLots: [],
333
 
334
  leaderboard: [],
335
 
 
337
  liquidationDay: null
338
  };
339
 
340
+ // --------- DOM helpers ----------
 
 
341
  const $ = (id) => document.getElementById(id);
342
 
343
  function fmtMoney(x){ return Number.isFinite(x) ? x.toLocaleString(undefined,{maximumFractionDigits:0}) : "--"; }
 
364
  }[c]));
365
  }
366
 
367
+ // --------- Tabs ----------
368
+ function setTab(which){
369
+ const tTrade = $("tabTrade"), tStats = $("tabStats");
370
+ const bTrade = $("tabBtnTrade"), bStats = $("tabBtnStats");
371
+
372
+ if (which === "trade"){
373
+ tTrade.classList.add("active");
374
+ tStats.classList.remove("active");
375
+ bTrade.classList.add("active");
376
+ bStats.classList.remove("active");
377
+ } else {
378
+ tStats.classList.add("active");
379
+ tTrade.classList.remove("active");
380
+ bStats.classList.add("active");
381
+ bTrade.classList.remove("active");
382
+ }
383
+ }
384
+ $("tabBtnTrade").addEventListener("click", () => setTab("trade"));
385
+ $("tabBtnStats").addEventListener("click", () => setTab("stats"));
386
+
387
+ // --------- Indicators ----------
388
  function sma(values, period){
389
  if (values.length < period) return null;
390
  let sum = 0;
 
407
  return 100 - (100 / (1 + rs));
408
  }
409
 
410
+ // --------- Charts (Canvas API) ----------
 
 
 
411
  function drawLineChart(canvas, series, opts){
412
  const ctx = canvas.getContext("2d");
413
  const w = canvas.width, h = canvas.height;
414
  ctx.clearRect(0,0,w,h);
 
 
415
  ctx.fillStyle = "rgba(0,0,0,0.08)";
416
  ctx.fillRect(0,0,w,h);
 
417
  if (!series || series.length < 2) return;
418
 
419
  const pad = 30;
 
425
  const yr = (ymax - ymin) || 1;
426
  const xr = (xmax - xmin) || 1;
427
 
 
428
  ctx.strokeStyle = "rgba(170,178,213,0.12)";
429
  ctx.lineWidth = 1;
430
  for (let i=0;i<=5;i++){
 
438
  const toX = (x) => pad + (w-2*pad) * ((x - xmin)/xr);
439
  const toY = (y) => h - pad - (h-2*pad) * ((y - ymin)/yr);
440
 
 
441
  ctx.strokeStyle = opts.color || "#ffffff";
442
  ctx.lineWidth = opts.width || 2;
443
  ctx.beginPath();
 
449
  }
450
  ctx.stroke();
451
 
 
452
  if (opts.overlays){
453
  for (const ov of opts.overlays){
454
  if (!ov.series || ov.series.length < 2) continue;
 
465
  }
466
  }
467
 
 
468
  ctx.fillStyle = "rgba(170,178,213,0.8)";
469
  ctx.font = "18px ui-monospace, Menlo, Consolas";
470
  ctx.fillText(opts.label || "", pad, 22);
471
  }
472
 
473
+ // --------- Portfolio / risk ----------
474
+ function exposure(){ return Math.abs(state.shares) * state.price; }
475
+ function requiredMargin(){ return exposure() / MAX_LEVERAGE; }
476
+ function marginUtil(){ if (state.equity <= 0) return 1; return requiredMargin() / state.equity; }
 
 
 
 
 
 
 
 
 
 
 
477
 
478
  function applyBorrowFeeOneDay(){
479
  if (state.shares < 0){
 
482
  }
483
  }
484
 
485
+ function breakEvenPrice(){
486
+ if (state.shares === 0) return null;
487
+ return state.avgCost;
488
+ }
489
+
490
  function recalc(){
 
491
  applyBorrowFeeOneDay();
492
 
 
493
  state.upnl = (state.price - state.avgCost) * state.shares;
 
494
  state.equity = state.cash + state.shares * state.price;
495
  state.roi = ((state.equity - INITIAL_CASH) / INITIAL_CASH) * 100;
496
 
 
497
  if (!state.liquidated && state.equity <= 0){
498
  state.liquidated = true;
499
  state.liquidationDay = state.day;
500
+
501
  $("liqBox").style.display = "block";
502
  $("buyBtn").disabled = true;
503
  $("sellBtn").disabled = true;
 
509
  $("takeProfit").disabled = true;
510
  }
511
 
512
+ $("warnBox").style.display = (!state.liquidated && marginUtil() >= 0.80) ? "block" : "none";
 
 
513
 
 
514
  state.equityCurve.push({ day: state.day, equity: state.equity });
515
  if (state.equityCurve.length > 5000) state.equityCurve.shift();
516
 
 
517
  if (!state.liquidated && state.shares !== 0){
518
  if (state.stopLoss !== null){
519
  if (state.shares > 0 && state.price <= state.stopLoss) closePosition("STOP");
 
526
  }
527
  }
528
 
529
+ // --------- FIFO lots for realized P&L ----------
530
+ function addLot(qty, price){ state.openLots.push({ qty, price }); }
 
 
 
 
 
 
 
 
 
 
 
 
531
 
532
  function consumeLots(closeQty, closePrice){
 
 
 
533
  let remaining = Math.abs(closeQty);
534
  let realized = 0;
535
 
 
538
  const lotAbs = Math.abs(lot.qty);
539
  const take = Math.min(lotAbs, remaining);
540
 
 
 
541
  if (lot.qty > 0) realized += (closePrice - lot.price) * take;
542
  else realized += (lot.price - closePrice) * take;
543
 
 
544
  const newAbs = lotAbs - take;
545
  if (newAbs === 0) state.openLots.splice(i,1);
546
  else {
 
552
  return realized;
553
  }
554
 
555
+ // --------- Trading ----------
 
 
556
  function canPlaceTrade(tradeShares){
557
  if (state.liquidated) return false;
558
  if (!Number.isFinite(state.price) || state.price <= 0) return false;
559
 
 
560
  const px = state.price;
561
  const newShares = state.shares + tradeShares;
562
  const newExposure = Math.abs(newShares) * px;
 
 
 
563
  const req = newExposure / MAX_LEVERAGE;
564
+
565
  if (state.equity <= 0) return false;
 
566
  if (state.equity < req) return false;
 
567
  return true;
568
  }
569
 
 
577
 
578
  const px = state.price;
579
 
 
580
  const cashDelta = -(tradeShares * px) - COMMISSION;
581
 
 
582
  let realized = 0;
 
583
  const prevShares = state.shares;
584
  const newShares = prevShares + tradeShares;
 
585
  const prevDir = Math.sign(prevShares);
586
  const tradeDir = Math.sign(tradeShares);
587
 
 
588
  if (prevShares !== 0 && prevDir !== tradeDir){
589
  realized = consumeLots(tradeShares, px);
590
  }
591
 
 
592
  state.cash += cashDelta;
593
 
 
594
  if (newShares === 0){
595
  state.shares = 0;
596
  state.avgCost = 0;
 
603
  const sameDir = (prevShares > 0 && newShares > 0) || (prevShares < 0 && newShares < 0);
604
 
605
  if (sameDir && prevDir === tradeDir){
 
606
  addLot(tradeShares, px);
 
607
  const totAbs = state.openLots.reduce((s,l)=>s+Math.abs(l.qty),0);
608
  const wavg = state.openLots.reduce((s,l)=>s+Math.abs(l.qty)*l.price,0) / (totAbs || 1);
609
  state.shares = newShares;
610
  state.avgCost = wavg;
611
  } else if (!sameDir){
 
612
  state.shares = newShares;
613
 
614
  if (Math.sign(newShares) === 0){
615
  state.avgCost = 0;
616
  state.openLots = [];
617
  } else if (Math.sign(newShares) !== prevDir){
 
 
618
  state.openLots = [{ qty: newShares, price: px }];
619
  state.avgCost = px;
620
  } else {
 
621
  const totAbs = state.openLots.reduce((s,l)=>s+Math.abs(l.qty),0);
622
  const wavg = totAbs ? (state.openLots.reduce((s,l)=>s+Math.abs(l.qty)*l.price,0) / totAbs) : 0;
623
  state.avgCost = wavg;
624
  }
625
  } else {
 
626
  state.shares = newShares;
627
  }
628
  }
 
630
  state.tradeLog.push({ action, qty, price: px, day: state.day, realized: realized - COMMISSION });
631
  if (state.tradeLog.length > 5000) state.tradeLog.shift();
632
 
 
633
  recalc();
634
  renderAll();
635
  }
 
638
  if (state.liquidated) return;
639
  if (state.shares === 0) return;
640
 
 
641
  const qty = Math.abs(state.shares);
642
  const action = (state.shares > 0) ? "SELL" : "BUY";
 
 
643
  trade(action, qty);
 
644
  const last = state.tradeLog[state.tradeLog.length - 1];
645
  if (last) last.action = reason;
646
  }
647
 
648
+ // --------- Rendering ----------
 
 
649
  function renderLeaderboard(){
650
  const lb = $("lbBody");
651
  lb.innerHTML = "";
 
672
  tb.innerHTML = `<tr><td colspan="5" class="muted">No trades yet.</td></tr>`;
673
  return;
674
  }
675
+ for (const t of state.tradeLog.slice().reverse().slice(0,250)){
676
  const tr = document.createElement("tr");
677
  const side = (t.action === "BUY") ? `<span class="pill buy">BUY</span>` :
678
  (t.action === "SELL") ? `<span class="pill sell">SELL</span>` :
 
690
  }
691
 
692
  function renderMetrics(){
693
+ const realized = state.tradeLog.map(t => t.realized);
 
 
694
  const n = realized.length;
695
  $("mTrades").textContent = n;
696
 
 
719
  $("mBest").textContent = fmtMoney(best);
720
  $("mWorst").textContent = fmtMoney(worst);
721
 
 
722
  const eq = state.equityCurve.slice(-300).map(p => p.equity);
723
+ if (eq.length < 3){ $("mSharpe").textContent = "--"; return; }
724
+
 
 
725
  const rets = [];
726
  for (let i=1;i<eq.length;i++){
727
  const r = (eq[i] - eq[i-1]) / Math.max(1e-9, eq[i-1]);
 
730
  const mean = rets.reduce((a,b)=>a+b,0)/rets.length;
731
  const varr = rets.reduce((s,x)=>s+(x-mean)*(x-mean),0)/Math.max(1, rets.length-1);
732
  const sd = Math.sqrt(varr);
733
+ const sharpe = sd === 0 ? 0 : (mean / sd) * Math.sqrt(252);
734
  $("mSharpe").textContent = sharpe.toFixed(2);
735
  }
736
 
 
767
  }
768
 
769
  function renderCharts(){
770
+ if (!state.market.length) return;
771
+
772
  const closes = state.market.map(p => p.close);
773
 
 
774
  const start = Math.max(0, state.day - PRICE_WINDOW + 1);
775
  const end = state.day + 1;
776
  const windowData = state.market.slice(start, end);
777
 
778
  const priceSeries = windowData.map(p => ({ x: p.i, y: p.close }));
779
 
 
780
  const ma20 = [];
781
  const ma50 = [];
782
  for (let i=start;i<end;i++){
 
796
  ]
797
  });
798
 
 
799
  const eqWin = state.equityCurve.slice(-EQUITY_WINDOW);
800
  const eqSeries = eqWin.map(p => ({ x: p.day, y: p.equity }));
801
+ drawLineChart($("equityCanvas"), eqSeries, { label: "EQUITY", color: "#22c55e", overlays: [] });
 
 
 
 
802
 
 
803
  const slice = closes.slice(0, state.day+1);
804
  const s20 = sma(slice, 20);
805
  const s50 = sma(slice, 50);
 
817
  renderMetrics();
818
  }
819
 
820
+ // --------- Stops UI ----------
 
 
821
  $("setStopsBtn").addEventListener("click", () => {
822
  if (state.liquidated) return;
823
  const sl = ($("stopLoss").value || "").trim();
 
835
  renderKPIs();
836
  });
837
 
838
+ // --------- Controls + shortcuts ----------
 
 
839
  function qtyVal(){ return Number($("qty").value); }
840
 
841
  $("buyBtn").addEventListener("click", () => trade("BUY", qtyVal()));
 
843
  $("closeBtn").addEventListener("click", () => closePosition("CLOSE"));
844
 
845
  $("resetBtn").addEventListener("click", () => {
 
846
  state.cash = INITIAL_CASH;
847
  state.shares = 0;
848
  state.avgCost = 0;
 
869
 
870
  $("stopLoss").value = "";
871
  $("takeProfit").value = "";
 
872
 
873
  recalc();
874
  renderAll();
 
887
  if (k === "5") $("qty").value = "500";
888
  });
889
 
 
890
  $("qty").addEventListener("input", () => {
891
  let v = Math.floor(Number($("qty").value));
892
  if (!Number.isFinite(v) || v < 1) v = 1;
 
894
  $("qty").value = String(v);
895
  });
896
 
897
+ // --------- WebSocket ----------
 
 
898
  function connectWS(){
899
  const isLocalhost = (location.hostname === "localhost" || location.hostname === "127.0.0.1");
900
  const protocol = isLocalhost ? "ws" : "wss";
 
943
  ws.onclose = () => setStatus("DISCONNECTED");
944
  }
945
 
946
+ // Heartbeat: UPDATE_EQUITY every second when connected, unless liquidated
947
  setInterval(() => {
948
  if (state.liquidated) return;
949
  if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return;
950
 
951
  state.ws.send(JSON.stringify({
952
  type: "UPDATE_EQUITY",
953
+ payload: { name: state.clientId, equity: state.equity, roi: state.roi }
 
 
 
 
954
  }));
955
  }, 1000);
956
 
957
+ // --------- Boot ----------
 
 
958
  setStatus("DISCONNECTED");
959
  renderAll();
960
  connectWS();