Download index.html from Almaatla/MPTrading: direct link, hf CLI and curl.
- Browser
- Download file 34.1 kB
-
https://huggingface.co/spaces/Almaatla/MPTrading/resolve/main/index.html
- Command line
-
hf download hf://spaces/Almaatla/MPTrading/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Almaatla/MPTrading/resolve/main/index.html
34.1 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"/> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"/> | |
| <title>MPTrading</title> | |
| <style> | |
| :root{ | |
| --bg:#0b1020; --panel:#111a33; --panel2:#0f1730; --text:#e7eaf3; | |
| --muted:#aab2d5; --line:#233055; --green:#22c55e; --red:#ef4444; --yellow:#f59e0b; --blue:#60a5fa; | |
| } | |
| *{ box-sizing:border-box; } | |
| body{ margin:0; font-family:system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; background:var(--bg); color:var(--text); } | |
| 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); | |
| gap:10px; | |
| } | |
| .leftHead{ display:flex; align-items:center; gap:12px; min-width: 0; } | |
| .title{ font-weight:800; letter-spacing:0.3px; white-space:nowrap; } | |
| .status{ font-size:12px; color:var(--muted); display:flex; align-items:center; gap:8px; white-space:nowrap; } | |
| .dot{ width:10px; height:10px; border-radius:999px; background:#6b7280; } | |
| .dot.connected{ background:var(--green); } | |
| .dot.connecting{ background:var(--yellow); } | |
| .dot.error{ background:var(--red); } | |
| .tabs{ display:flex; gap:8px; flex-wrap:wrap; } | |
| .tabBtn{ | |
| font:inherit; border-radius:10px; border:1px solid var(--line); | |
| background:#0c1430; color:var(--text); padding:8px 10px; cursor:pointer; | |
| font-size:13px; | |
| } | |
| .tabBtn.active{ background:rgba(96,165,250,0.14); border-color:rgba(96,165,250,0.55); } | |
| main{ padding:12px; height: calc(100vh - 52px); } | |
| .tabPanel{ display:none; height:100%; } | |
| .tabPanel.active{ display:block; } | |
| .gridMain{ | |
| display:grid; | |
| grid-template-columns: 1.35fr 1fr; /* Market + Trading side by side */ | |
| gap:12px; | |
| height:100%; | |
| min-height:0; | |
| } | |
| .gridSecondary{ | |
| display:grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap:12px; | |
| height:100%; | |
| min-height:0; | |
| } | |
| .card{ | |
| background:var(--panel); border:1px solid var(--line); border-radius:10px; | |
| overflow:hidden; display:flex; flex-direction:column; min-height:0; | |
| } | |
| .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); | |
| } | |
| .content{ padding:12px; overflow:auto; min-height:0; } | |
| .grid3{ display:grid; grid-template-columns: 1fr 1fr 1fr; gap:10px; } | |
| .grid2{ display:grid; grid-template-columns: 1fr 1fr; gap:10px; } | |
| .kpi{ padding:10px; border:1px solid var(--line); border-radius:10px; background:rgba(255,255,255,0.02); } | |
| .kpi .label{ color:var(--muted); font-size:12px; } | |
| .kpi .value{ font-size:18px; font-weight:800; margin-top:4px; } | |
| .kpi .sub{ color:var(--muted); font-size:12px; margin-top:4px; } | |
| .row{ display:flex; gap:10px; align-items:center; flex-wrap:wrap; } | |
| input, button, select{ | |
| font:inherit; border-radius:10px; border:1px solid var(--line); | |
| background:#0c1430; color:var(--text); padding:10px 12px; | |
| } | |
| input{ width:130px; } | |
| button{ cursor:pointer; } | |
| button.buy{ border-color:rgba(34,197,94,0.5); } | |
| button.sell{ border-color:rgba(239,68,68,0.5); } | |
| button.buy:hover{ background:rgba(34,197,94,0.12); } | |
| button.sell:hover{ background:rgba(239,68,68,0.12); } | |
| button.secondary:hover{ background:rgba(255,255,255,0.06); } | |
| .warn{ border:1px solid rgba(245,158,11,0.45); background:rgba(245,158,11,0.07); border-radius:10px; padding:10px; } | |
| .danger{ border:1px solid rgba(239,68,68,0.55); background:rgba(239,68,68,0.07); border-radius:10px; padding:10px; } | |
| .muted{ color:var(--muted); } | |
| .small{ font-size:12px; color:var(--muted); } | |
| .mono{ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; } | |
| 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; } | |
| #equityCanvas{ height:240px; } | |
| table{ width:100%; border-collapse:collapse; font-size:13px; } | |
| th, td{ text-align:left; padding:8px 6px; border-bottom:1px solid rgba(35,48,85,0.6); vertical-align:top; } | |
| th{ color:var(--muted); font-weight:700; } | |
| td.right, th.right{ text-align:right; } | |
| .pill{ display:inline-block; padding:2px 8px; border-radius:999px; font-size:12px; border:1px solid var(--line); } | |
| .pill.buy{ color:var(--green); border-color:rgba(34,197,94,0.5); } | |
| .pill.sell{ color:var(--red); border-color:rgba(239,68,68,0.5); } | |
| @media (max-width: 1000px){ | |
| .gridMain{ grid-template-columns: 1fr; height:auto; } | |
| .gridSecondary{ grid-template-columns: 1fr; height:auto; } | |
| main{ height:auto; } | |
| canvas{ height:200px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="leftHead"> | |
| <div class="title">MPTrading</div> | |
| <div class="tabs"> | |
| <button class="tabBtn active" id="tabBtnTrade" type="button">Trading</button> | |
| <button class="tabBtn" id="tabBtnStats" type="button">Stats</button> | |
| </div> | |
| </div> | |
| <div class="status"> | |
| <span id="dot" class="dot"></span> | |
| <span id="statusText">Disconnected</span> | |
| <span class="small" id="clientIdText"></span> | |
| </div> | |
| </header> | |
| <main> | |
| <!-- TAB 1: Trading (Primary) --> | |
| <section class="tabPanel active" id="tabTrade"> | |
| <div class="gridMain"> | |
| <!-- Market side --> | |
| <div class="card"> | |
| <h2>Price & Chart</h2> | |
| <div class="content"> | |
| <div class="grid3"> | |
| <div class="kpi"> | |
| <div class="label">Day / Price</div> | |
| <div class="value" id="kpiPrice">--</div> | |
| <div class="sub"><span class="muted">Day:</span> <span id="kpiDay">--</span></div> | |
| </div> | |
| <div class="kpi"> | |
| <div class="label">Indicators</div> | |
| <div class="sub"><span class="muted">MA20:</span> <span id="kpiMA20">--</span></div> | |
| <div class="sub"><span class="muted">MA50:</span> <span id="kpiMA50">--</span></div> | |
| <div class="sub"><span class="muted">RSI14:</span> <span id="kpiRSI">--</span></div> | |
| </div> | |
| <div class="kpi"> | |
| <div class="label">Latest news</div> | |
| <div id="newsText" class="sub">No news yet.</div> | |
| </div> | |
| </div> | |
| <div style="height:10px"></div> | |
| <canvas id="priceCanvas" width="1200" height="440"></canvas> | |
| <div class="small muted" style="margin-top:8px;"> | |
| Price (white) · MA20 (blue) · MA50 (yellow). | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Trading side --> | |
| <div class="card"> | |
| <h2>Trading</h2> | |
| <div class="content"> | |
| <div class="grid2"> | |
| <div class="kpi"> | |
| <div class="label">Cash / Equity</div> | |
| <div class="sub"><span class="muted">Cash:</span> <span id="kpiCash">--</span></div> | |
| <div class="sub"><span class="muted">Equity:</span> <span id="kpiEquity">--</span></div> | |
| <div class="sub"><span class="muted">ROI:</span> <span id="kpiRoi">--</span></div> | |
| </div> | |
| <div class="kpi"> | |
| <div class="label">Position</div> | |
| <div class="sub"><span class="muted">Shares:</span> <span id="kpiPos">--</span></div> | |
| <div class="sub"><span class="muted">Avg cost:</span> <span id="kpiAvg">--</span></div> | |
| <div class="sub"><span class="muted">Break-even:</span> <span id="kpiBE">--</span></div> | |
| <div class="sub"><span class="muted">Unrealized P&L:</span> <span id="kpiUPnL">--</span></div> | |
| </div> | |
| <div class="kpi"> | |
| <div class="label">Margin</div> | |
| <div class="sub"><span class="muted">Max leverage:</span> <span class="mono" id="kpiLevMax">--</span></div> | |
| <div class="sub"><span class="muted">Used:</span> <span class="mono" id="kpiMarginUsed">--</span></div> | |
| <div class="sub"><span class="muted">Utilization:</span> <span class="mono" id="kpiMarginPct">--</span></div> | |
| <div class="sub"><span class="muted">Borrow fee (short):</span> <span class="mono" id="kpiBorrowFee">--</span></div> | |
| </div> | |
| <div class="kpi"> | |
| <div class="label">Stops</div> | |
| <div class="row" style="margin-top:8px;"> | |
| <label class="small muted">Stop</label> | |
| <input id="stopLoss" type="number" placeholder="price" step="0.01"/> | |
| <label class="small muted">TP</label> | |
| <input id="takeProfit" type="number" placeholder="price" step="0.01"/> | |
| </div> | |
| <div class="row" style="margin-top:8px;"> | |
| <button class="secondary" id="setStopsBtn" type="button">Set</button> | |
| <button class="secondary" id="clearStopsBtn" type="button">Clear</button> | |
| </div> | |
| <div class="sub"><span class="muted">Active SL:</span> <span id="kpiSL">--</span> · <span class="muted">TP:</span> <span id="kpiTP">--</span></div> | |
| </div> | |
| </div> | |
| <div style="height:12px"></div> | |
| <div id="warnBox" class="warn" style="display:none;"> | |
| <div class="small"><b>Margin warning</b>: utilization is high. Reduce exposure.</div> | |
| </div> | |
| <div id="liqBox" class="danger" style="display:none;"> | |
| <div class="small"><b>Liquidated</b>: equity is below zero. Trading disabled.</div> | |
| </div> | |
| <div style="height:12px"></div> | |
| <div class="row"> | |
| <label class="small muted">Qty</label> | |
| <input id="qty" type="number" value="100" min="1" step="1"/> | |
| <button class="buy" id="buyBtn" type="button">Buy (B)</button> | |
| <button class="sell" id="sellBtn" type="button">Sell (S)</button> | |
| <button class="secondary" id="closeBtn" type="button">Close pos (C)</button> | |
| <button class="secondary" id="resetBtn" type="button" title="Reset local portfolio only">Reset</button> | |
| </div> | |
| <div class="small muted" style="margin-top:10px;"> | |
| Shorting enabled. Leverage-limited. Keyboard: B/S/C, and 1..5 set quick quantities. | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- TAB 2: Stats (Secondary) --> | |
| <section class="tabPanel" id="tabStats"> | |
| <div class="gridSecondary"> | |
| <div class="card"> | |
| <h2>Leaderboard</h2> | |
| <div class="content"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Name</th> | |
| <th class="right">Equity</th> | |
| <th class="right">ROI %</th> | |
| </tr> | |
| </thead> | |
| <tbody id="lbBody"> | |
| <tr><td colspan="3" class="muted">Waiting for ticks…</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h2>Equity curve</h2> | |
| <div class="content"> | |
| <canvas id="equityCanvas" width="1200" height="320"></canvas> | |
| <div class="small muted" style="margin-top:8px;">Equity curve (last 200 points).</div> | |
| <div style="height:10px"></div> | |
| <div class="kpi"> | |
| <div class="label">Performance (session)</div> | |
| <div class="sub"><span class="muted">Trades:</span> <span id="mTrades">0</span> · <span class="muted">Win rate:</span> <span id="mWinRate">--</span></div> | |
| <div class="sub"><span class="muted">Avg win:</span> <span id="mAvgWin">--</span> · <span class="muted">Avg loss:</span> <span id="mAvgLoss">--</span></div> | |
| <div class="sub"><span class="muted">Best:</span> <span id="mBest">--</span> · <span class="muted">Worst:</span> <span id="mWorst">--</span></div> | |
| <div class="sub"><span class="muted">Sharpe (est):</span> <span id="mSharpe">--</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card" style="grid-column: 1 / -1;"> | |
| <h2>Trade history</h2> | |
| <div class="content"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Action</th> | |
| <th class="right">Qty</th> | |
| <th class="right">Price</th> | |
| <th class="right">Day</th> | |
| <th class="right">Realized P&L</th> | |
| </tr> | |
| </thead> | |
| <tbody id="tradesBody"> | |
| <tr><td colspan="5" class="muted">No trades yet.</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <script> | |
| // --------- Constants / risk parameters ---------- | |
| const INITIAL_CASH = 10000; | |
| const COMMISSION = 5; | |
| const MAX_LEVERAGE = 3.0; | |
| const BORROW_RATE_PER_DAY = 0.0002; | |
| const PRICE_WINDOW = 120; | |
| const EQUITY_WINDOW = 200; | |
| // --------- State ---------- | |
| const state = { | |
| ws: null, | |
| wsStatus: "DISCONNECTED", | |
| clientId: "user_" + Math.random().toString(36).slice(2, 7), | |
| market: [], | |
| day: 0, | |
| price: 0, | |
| cash: INITIAL_CASH, | |
| shares: 0, | |
| avgCost: 0, | |
| equity: INITIAL_CASH, | |
| roi: 0, | |
| upnl: 0, | |
| stopLoss: null, | |
| takeProfit: null, | |
| equityCurve: [], | |
| tradeLog: [], | |
| openLots: [], | |
| leaderboard: [], | |
| liquidated: false, | |
| liquidationDay: null | |
| }; | |
| // --------- DOM helpers ---------- | |
| const $ = (id) => document.getElementById(id); | |
| function fmtMoney(x){ return Number.isFinite(x) ? x.toLocaleString(undefined,{maximumFractionDigits:0}) : "--"; } | |
| function fmtPrice(x){ return Number.isFinite(x) ? x.toFixed(2) : "--"; } | |
| function fmtPct(x){ return Number.isFinite(x) ? (x.toFixed(2) + "%") : "--"; } | |
| function setStatus(status){ | |
| state.wsStatus = status; | |
| const dot = $("dot"); | |
| const txt = $("statusText"); | |
| dot.className = "dot"; | |
| if (status === "CONNECTED") dot.classList.add("connected"); | |
| else if (status === "CONNECTING") dot.classList.add("connecting"); | |
| else if (status === "ERROR") dot.classList.add("error"); | |
| txt.textContent = status[0] + status.slice(1).toLowerCase(); | |
| $("clientIdText").textContent = "· " + state.clientId; | |
| } | |
| function escapeHtml(s){ | |
| return String(s).replace(/[&<>"']/g, (c) => ({ | |
| "&":"&","<":"<",">":">",'"':""","'":"'" | |
| }[c])); | |
| } | |
| // --------- Tabs ---------- | |
| function setTab(which){ | |
| const tTrade = $("tabTrade"), tStats = $("tabStats"); | |
| const bTrade = $("tabBtnTrade"), bStats = $("tabBtnStats"); | |
| if (which === "trade"){ | |
| tTrade.classList.add("active"); | |
| tStats.classList.remove("active"); | |
| bTrade.classList.add("active"); | |
| bStats.classList.remove("active"); | |
| } else { | |
| tStats.classList.add("active"); | |
| tTrade.classList.remove("active"); | |
| bStats.classList.add("active"); | |
| bTrade.classList.remove("active"); | |
| } | |
| } | |
| $("tabBtnTrade").addEventListener("click", () => setTab("trade")); | |
| $("tabBtnStats").addEventListener("click", () => setTab("stats")); | |
| // --------- Indicators ---------- | |
| function sma(values, period){ | |
| if (values.length < period) return null; | |
| let sum = 0; | |
| for (let i = values.length - period; i < values.length; i++) sum += values[i]; | |
| return sum / period; | |
| } | |
| function rsi14(values, period=14){ | |
| if (values.length < period + 1) return null; | |
| let gains = 0, losses = 0; | |
| for (let i = values.length - period; i < values.length; i++){ | |
| const chg = values[i] - values[i - 1]; | |
| if (chg >= 0) gains += chg; | |
| else losses -= chg; | |
| } | |
| const avgGain = gains / period; | |
| const avgLoss = losses / period; | |
| if (avgLoss === 0) return 100; | |
| const rs = avgGain / avgLoss; | |
| return 100 - (100 / (1 + rs)); | |
| } | |
| // --------- Charts (Canvas API) ---------- | |
| function drawLineChart(canvas, series, opts){ | |
| const ctx = canvas.getContext("2d"); | |
| const w = canvas.width, h = canvas.height; | |
| ctx.clearRect(0,0,w,h); | |
| ctx.fillStyle = "rgba(0,0,0,0.08)"; | |
| ctx.fillRect(0,0,w,h); | |
| if (!series || series.length < 2) return; | |
| const pad = 30; | |
| const xs = series.map(p => p.x); | |
| const ys = series.map(p => p.y); | |
| const xmin = Math.min(...xs), xmax = Math.max(...xs); | |
| const ymin = Math.min(...ys), ymax = Math.max(...ys); | |
| const yr = (ymax - ymin) || 1; | |
| const xr = (xmax - xmin) || 1; | |
| ctx.strokeStyle = "rgba(170,178,213,0.12)"; | |
| ctx.lineWidth = 1; | |
| for (let i=0;i<=5;i++){ | |
| const yy = pad + (h-2*pad) * (i/5); | |
| ctx.beginPath(); | |
| ctx.moveTo(pad, yy); | |
| ctx.lineTo(w-pad, yy); | |
| ctx.stroke(); | |
| } | |
| const toX = (x) => pad + (w-2*pad) * ((x - xmin)/xr); | |
| const toY = (y) => h - pad - (h-2*pad) * ((y - ymin)/yr); | |
| ctx.strokeStyle = opts.color || "#ffffff"; | |
| ctx.lineWidth = opts.width || 2; | |
| ctx.beginPath(); | |
| for (let i=0;i<series.length;i++){ | |
| const px = toX(series[i].x); | |
| const py = toY(series[i].y); | |
| if (i===0) ctx.moveTo(px,py); | |
| else ctx.lineTo(px,py); | |
| } | |
| ctx.stroke(); | |
| if (opts.overlays){ | |
| for (const ov of opts.overlays){ | |
| if (!ov.series || ov.series.length < 2) continue; | |
| ctx.strokeStyle = ov.color || "#60a5fa"; | |
| ctx.lineWidth = ov.width || 2; | |
| ctx.beginPath(); | |
| for (let i=0;i<ov.series.length;i++){ | |
| const px = toX(ov.series[i].x); | |
| const py = toY(ov.series[i].y); | |
| if (i===0) ctx.moveTo(px,py); | |
| else ctx.lineTo(px,py); | |
| } | |
| ctx.stroke(); | |
| } | |
| } | |
| ctx.fillStyle = "rgba(170,178,213,0.8)"; | |
| ctx.font = "18px ui-monospace, Menlo, Consolas"; | |
| ctx.fillText(opts.label || "", pad, 22); | |
| } | |
| // --------- Portfolio / risk ---------- | |
| function exposure(){ return Math.abs(state.shares) * state.price; } | |
| function requiredMargin(){ return exposure() / MAX_LEVERAGE; } | |
| function marginUtil(){ if (state.equity <= 0) return 1; return requiredMargin() / state.equity; } | |
| function applyBorrowFeeOneDay(){ | |
| if (state.shares < 0){ | |
| const fee = BORROW_RATE_PER_DAY * exposure(); | |
| state.cash -= fee; | |
| } | |
| } | |
| function breakEvenPrice(){ | |
| if (state.shares === 0) return null; | |
| return state.avgCost; | |
| } | |
| function recalc(){ | |
| applyBorrowFeeOneDay(); | |
| state.upnl = (state.price - state.avgCost) * state.shares; | |
| state.equity = state.cash + state.shares * state.price; | |
| state.roi = ((state.equity - INITIAL_CASH) / INITIAL_CASH) * 100; | |
| if (!state.liquidated && state.equity <= 0){ | |
| state.liquidated = true; | |
| state.liquidationDay = state.day; | |
| $("liqBox").style.display = "block"; | |
| $("buyBtn").disabled = true; | |
| $("sellBtn").disabled = true; | |
| $("closeBtn").disabled = true; | |
| $("setStopsBtn").disabled = true; | |
| $("clearStopsBtn").disabled = true; | |
| $("qty").disabled = true; | |
| $("stopLoss").disabled = true; | |
| $("takeProfit").disabled = true; | |
| } | |
| $("warnBox").style.display = (!state.liquidated && marginUtil() >= 0.80) ? "block" : "none"; | |
| state.equityCurve.push({ day: state.day, equity: state.equity }); | |
| if (state.equityCurve.length > 5000) state.equityCurve.shift(); | |
| if (!state.liquidated && state.shares !== 0){ | |
| if (state.stopLoss !== null){ | |
| if (state.shares > 0 && state.price <= state.stopLoss) closePosition("STOP"); | |
| if (state.shares < 0 && state.price >= state.stopLoss) closePosition("STOP"); | |
| } | |
| if (state.takeProfit !== null){ | |
| if (state.shares > 0 && state.price >= state.takeProfit) closePosition("TP"); | |
| if (state.shares < 0 && state.price <= state.takeProfit) closePosition("TP"); | |
| } | |
| } | |
| } | |
| // --------- FIFO lots for realized P&L ---------- | |
| function addLot(qty, price){ state.openLots.push({ qty, price }); } | |
| function consumeLots(closeQty, closePrice){ | |
| let remaining = Math.abs(closeQty); | |
| let realized = 0; | |
| for (let i=0; i<state.openLots.length && remaining>0; ){ | |
| const lot = state.openLots[i]; | |
| const lotAbs = Math.abs(lot.qty); | |
| const take = Math.min(lotAbs, remaining); | |
| if (lot.qty > 0) realized += (closePrice - lot.price) * take; | |
| else realized += (lot.price - closePrice) * take; | |
| const newAbs = lotAbs - take; | |
| if (newAbs === 0) state.openLots.splice(i,1); | |
| else { | |
| lot.qty = (lot.qty > 0) ? newAbs : -newAbs; | |
| i++; | |
| } | |
| remaining -= take; | |
| } | |
| return realized; | |
| } | |
| // --------- Trading ---------- | |
| function canPlaceTrade(tradeShares){ | |
| if (state.liquidated) return false; | |
| if (!Number.isFinite(state.price) || state.price <= 0) return false; | |
| const px = state.price; | |
| const newShares = state.shares + tradeShares; | |
| const newExposure = Math.abs(newShares) * px; | |
| const req = newExposure / MAX_LEVERAGE; | |
| if (state.equity <= 0) return false; | |
| if (state.equity < req) return false; | |
| return true; | |
| } | |
| function trade(action, qty){ | |
| qty = Math.floor(Number(qty)); | |
| if (!Number.isFinite(qty) || qty <= 0) return; | |
| if (state.liquidated) return; | |
| const tradeShares = (action === "BUY") ? qty : -qty; | |
| if (!canPlaceTrade(tradeShares)) return; | |
| const px = state.price; | |
| const cashDelta = -(tradeShares * px) - COMMISSION; | |
| let realized = 0; | |
| const prevShares = state.shares; | |
| const newShares = prevShares + tradeShares; | |
| const prevDir = Math.sign(prevShares); | |
| const tradeDir = Math.sign(tradeShares); | |
| if (prevShares !== 0 && prevDir !== tradeDir){ | |
| realized = consumeLots(tradeShares, px); | |
| } | |
| state.cash += cashDelta; | |
| if (newShares === 0){ | |
| state.shares = 0; | |
| state.avgCost = 0; | |
| state.openLots = []; | |
| } else if (prevShares === 0){ | |
| state.shares = newShares; | |
| state.avgCost = px; | |
| addLot(tradeShares, px); | |
| } else { | |
| const sameDir = (prevShares > 0 && newShares > 0) || (prevShares < 0 && newShares < 0); | |
| if (sameDir && prevDir === tradeDir){ | |
| addLot(tradeShares, px); | |
| const totAbs = state.openLots.reduce((s,l)=>s+Math.abs(l.qty),0); | |
| const wavg = state.openLots.reduce((s,l)=>s+Math.abs(l.qty)*l.price,0) / (totAbs || 1); | |
| state.shares = newShares; | |
| state.avgCost = wavg; | |
| } else if (!sameDir){ | |
| state.shares = newShares; | |
| if (Math.sign(newShares) === 0){ | |
| state.avgCost = 0; | |
| state.openLots = []; | |
| } else if (Math.sign(newShares) !== prevDir){ | |
| state.openLots = [{ qty: newShares, price: px }]; | |
| state.avgCost = px; | |
| } else { | |
| const totAbs = state.openLots.reduce((s,l)=>s+Math.abs(l.qty),0); | |
| const wavg = totAbs ? (state.openLots.reduce((s,l)=>s+Math.abs(l.qty)*l.price,0) / totAbs) : 0; | |
| state.avgCost = wavg; | |
| } | |
| } else { | |
| state.shares = newShares; | |
| } | |
| } | |
| state.tradeLog.push({ action, qty, price: px, day: state.day, realized: realized - COMMISSION }); | |
| if (state.tradeLog.length > 5000) state.tradeLog.shift(); | |
| recalc(); | |
| renderAll(); | |
| } | |
| function closePosition(reason="CLOSE"){ | |
| if (state.liquidated) return; | |
| if (state.shares === 0) return; | |
| const qty = Math.abs(state.shares); | |
| const action = (state.shares > 0) ? "SELL" : "BUY"; | |
| trade(action, qty); | |
| const last = state.tradeLog[state.tradeLog.length - 1]; | |
| if (last) last.action = reason; | |
| } | |
| // --------- Rendering ---------- | |
| function renderLeaderboard(){ | |
| const lb = $("lbBody"); | |
| lb.innerHTML = ""; | |
| if (!state.leaderboard || !state.leaderboard.length){ | |
| lb.innerHTML = `<tr><td colspan="3" class="muted">Waiting for ticks…</td></tr>`; | |
| return; | |
| } | |
| for (const p of state.leaderboard.slice(0,50)){ | |
| const tr = document.createElement("tr"); | |
| const roiStyle = p.roi >= 0 ? `style="color:var(--green)"` : `style="color:var(--red)"`; | |
| tr.innerHTML = ` | |
| <td>${escapeHtml(p.name)}</td> | |
| <td class="right mono">${fmtMoney(p.equity)}</td> | |
| <td class="right mono" ${roiStyle}>${fmtPct(p.roi)}</td> | |
| `; | |
| lb.appendChild(tr); | |
| } | |
| } | |
| function renderTrades(){ | |
| const tb = $("tradesBody"); | |
| tb.innerHTML = ""; | |
| if (!state.tradeLog.length){ | |
| tb.innerHTML = `<tr><td colspan="5" class="muted">No trades yet.</td></tr>`; | |
| return; | |
| } | |
| for (const t of state.tradeLog.slice().reverse().slice(0,250)){ | |
| const tr = document.createElement("tr"); | |
| const side = (t.action === "BUY") ? `<span class="pill buy">BUY</span>` : | |
| (t.action === "SELL") ? `<span class="pill sell">SELL</span>` : | |
| `<span class="pill">${escapeHtml(t.action)}</span>`; | |
| const pnlStyle = t.realized >= 0 ? `style="color:var(--green)"` : `style="color:var(--red)"`; | |
| tr.innerHTML = ` | |
| <td>${side}</td> | |
| <td class="right mono">${t.qty}</td> | |
| <td class="right mono">${fmtPrice(t.price)}</td> | |
| <td class="right mono">${t.day}</td> | |
| <td class="right mono" ${pnlStyle}>${fmtMoney(t.realized)}</td> | |
| `; | |
| tb.appendChild(tr); | |
| } | |
| } | |
| function renderMetrics(){ | |
| const realized = state.tradeLog.map(t => t.realized); | |
| const n = realized.length; | |
| $("mTrades").textContent = n; | |
| if (!n){ | |
| $("mWinRate").textContent = "--"; | |
| $("mAvgWin").textContent = "--"; | |
| $("mAvgLoss").textContent = "--"; | |
| $("mBest").textContent = "--"; | |
| $("mWorst").textContent = "--"; | |
| $("mSharpe").textContent = "--"; | |
| return; | |
| } | |
| const wins = realized.filter(x => x > 0); | |
| const losses = realized.filter(x => x < 0); | |
| const winRate = (wins.length / n) * 100; | |
| const avgWin = wins.length ? wins.reduce((a,b)=>a+b,0)/wins.length : 0; | |
| const avgLoss = losses.length ? losses.reduce((a,b)=>a+b,0)/losses.length : 0; | |
| const best = Math.max(...realized); | |
| const worst = Math.min(...realized); | |
| $("mWinRate").textContent = fmtPct(winRate); | |
| $("mAvgWin").textContent = fmtMoney(avgWin); | |
| $("mAvgLoss").textContent = fmtMoney(avgLoss); | |
| $("mBest").textContent = fmtMoney(best); | |
| $("mWorst").textContent = fmtMoney(worst); | |
| const eq = state.equityCurve.slice(-300).map(p => p.equity); | |
| if (eq.length < 3){ $("mSharpe").textContent = "--"; return; } | |
| const rets = []; | |
| for (let i=1;i<eq.length;i++){ | |
| const r = (eq[i] - eq[i-1]) / Math.max(1e-9, eq[i-1]); | |
| rets.push(r); | |
| } | |
| const mean = rets.reduce((a,b)=>a+b,0)/rets.length; | |
| const varr = rets.reduce((s,x)=>s+(x-mean)*(x-mean),0)/Math.max(1, rets.length-1); | |
| const sd = Math.sqrt(varr); | |
| const sharpe = sd === 0 ? 0 : (mean / sd) * Math.sqrt(252); | |
| $("mSharpe").textContent = sharpe.toFixed(2); | |
| } | |
| function renderKPIs(){ | |
| $("kpiDay").textContent = state.day; | |
| $("kpiPrice").textContent = fmtPrice(state.price); | |
| $("kpiCash").textContent = fmtMoney(state.cash); | |
| $("kpiEquity").textContent = fmtMoney(state.equity); | |
| const roiEl = $("kpiRoi"); | |
| roiEl.textContent = fmtPct(state.roi); | |
| roiEl.style.color = (state.roi >= 0) ? "var(--green)" : "var(--red)"; | |
| $("kpiPos").textContent = state.shares.toLocaleString(); | |
| $("kpiAvg").textContent = fmtPrice(state.avgCost); | |
| const be = breakEvenPrice(); | |
| $("kpiBE").textContent = (be === null) ? "--" : fmtPrice(be); | |
| const up = $("kpiUPnL"); | |
| up.textContent = fmtMoney(state.upnl); | |
| up.style.color = (state.upnl >= 0) ? "var(--green)" : "var(--red)"; | |
| $("kpiLevMax").textContent = MAX_LEVERAGE.toFixed(1) + "x"; | |
| $("kpiMarginUsed").textContent = fmtMoney(requiredMargin()); | |
| $("kpiMarginPct").textContent = fmtPct(marginUtil() * 100); | |
| const borrowFee = (state.shares < 0) ? (BORROW_RATE_PER_DAY * exposure()) : 0; | |
| $("kpiBorrowFee").textContent = fmtMoney(borrowFee) + " / day"; | |
| $("kpiSL").textContent = (state.stopLoss === null) ? "--" : fmtPrice(state.stopLoss); | |
| $("kpiTP").textContent = (state.takeProfit === null) ? "--" : fmtPrice(state.takeProfit); | |
| } | |
| function renderCharts(){ | |
| if (!state.market.length) return; | |
| const closes = state.market.map(p => p.close); | |
| const start = Math.max(0, state.day - PRICE_WINDOW + 1); | |
| const end = state.day + 1; | |
| const windowData = state.market.slice(start, end); | |
| const priceSeries = windowData.map(p => ({ x: p.i, y: p.close })); | |
| const ma20 = []; | |
| const ma50 = []; | |
| for (let i=start;i<end;i++){ | |
| const slice = closes.slice(0, i+1); | |
| const s20 = sma(slice, 20); | |
| const s50 = sma(slice, 50); | |
| if (s20 !== null) ma20.push({ x: i, y: s20 }); | |
| if (s50 !== null) ma50.push({ x: i, y: s50 }); | |
| } | |
| drawLineChart($("priceCanvas"), priceSeries, { | |
| label: "PRICE", | |
| color: "#ffffff", | |
| overlays: [ | |
| { series: ma20, color: "#60a5fa", width: 2 }, | |
| { series: ma50, color: "#f59e0b", width: 2 } | |
| ] | |
| }); | |
| const eqWin = state.equityCurve.slice(-EQUITY_WINDOW); | |
| const eqSeries = eqWin.map(p => ({ x: p.day, y: p.equity })); | |
| drawLineChart($("equityCanvas"), eqSeries, { label: "EQUITY", color: "#22c55e", overlays: [] }); | |
| const slice = closes.slice(0, state.day+1); | |
| const s20 = sma(slice, 20); | |
| const s50 = sma(slice, 50); | |
| const rsi = rsi14(slice, 14); | |
| $("kpiMA20").textContent = (s20 === null) ? "--" : fmtPrice(s20); | |
| $("kpiMA50").textContent = (s50 === null) ? "--" : fmtPrice(s50); | |
| $("kpiRSI").textContent = (rsi === null) ? "--" : rsi.toFixed(1); | |
| } | |
| function renderAll(){ | |
| renderKPIs(); | |
| renderCharts(); | |
| renderLeaderboard(); | |
| renderTrades(); | |
| renderMetrics(); | |
| } | |
| // --------- Stops UI ---------- | |
| $("setStopsBtn").addEventListener("click", () => { | |
| if (state.liquidated) return; | |
| const sl = ($("stopLoss").value || "").trim(); | |
| const tp = ($("takeProfit").value || "").trim(); | |
| state.stopLoss = (sl === "") ? null : Number(sl); | |
| state.takeProfit = (tp === "") ? null : Number(tp); | |
| renderKPIs(); | |
| }); | |
| $("clearStopsBtn").addEventListener("click", () => { | |
| state.stopLoss = null; | |
| state.takeProfit = null; | |
| $("stopLoss").value = ""; | |
| $("takeProfit").value = ""; | |
| renderKPIs(); | |
| }); | |
| // --------- Controls + shortcuts ---------- | |
| function qtyVal(){ return Number($("qty").value); } | |
| $("buyBtn").addEventListener("click", () => trade("BUY", qtyVal())); | |
| $("sellBtn").addEventListener("click", () => trade("SELL", qtyVal())); | |
| $("closeBtn").addEventListener("click", () => closePosition("CLOSE")); | |
| $("resetBtn").addEventListener("click", () => { | |
| state.cash = INITIAL_CASH; | |
| state.shares = 0; | |
| state.avgCost = 0; | |
| state.equity = INITIAL_CASH; | |
| state.roi = 0; | |
| state.upnl = 0; | |
| state.stopLoss = null; | |
| state.takeProfit = null; | |
| state.tradeLog = []; | |
| state.openLots = []; | |
| state.equityCurve = []; | |
| state.liquidated = false; | |
| state.liquidationDay = null; | |
| $("liqBox").style.display = "none"; | |
| $("buyBtn").disabled = false; | |
| $("sellBtn").disabled = false; | |
| $("closeBtn").disabled = false; | |
| $("setStopsBtn").disabled = false; | |
| $("clearStopsBtn").disabled = false; | |
| $("qty").disabled = false; | |
| $("stopLoss").disabled = false; | |
| $("takeProfit").disabled = false; | |
| $("stopLoss").value = ""; | |
| $("takeProfit").value = ""; | |
| recalc(); | |
| renderAll(); | |
| }); | |
| document.addEventListener("keydown", (e) => { | |
| if (e.target && (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")) return; | |
| const k = e.key.toLowerCase(); | |
| if (k === "b") trade("BUY", qtyVal()); | |
| if (k === "s") trade("SELL", qtyVal()); | |
| if (k === "c") closePosition("CLOSE"); | |
| if (k === "1") $("qty").value = "10"; | |
| if (k === "2") $("qty").value = "50"; | |
| if (k === "3") $("qty").value = "100"; | |
| if (k === "4") $("qty").value = "250"; | |
| if (k === "5") $("qty").value = "500"; | |
| }); | |
| $("qty").addEventListener("input", () => { | |
| let v = Math.floor(Number($("qty").value)); | |
| if (!Number.isFinite(v) || v < 1) v = 1; | |
| if (v > 1000000) v = 1000000; | |
| $("qty").value = String(v); | |
| }); | |
| // --------- WebSocket ---------- | |
| function connectWS(){ | |
| const isLocalhost = (location.hostname === "localhost" || location.hostname === "127.0.0.1"); | |
| const protocol = isLocalhost ? "ws" : "wss"; | |
| const wsUrl = `${protocol}://${location.host}/ws/${state.clientId}`; | |
| setStatus("CONNECTING"); | |
| const ws = new WebSocket(wsUrl); | |
| state.ws = ws; | |
| ws.onmessage = (ev) => { | |
| let msg; | |
| try { msg = JSON.parse(ev.data); } catch { return; } | |
| if (msg.type === "INIT") { | |
| state.market = Array.isArray(msg.payload?.market) ? msg.payload.market : []; | |
| state.day = Number(msg.payload?.startDay ?? 0); | |
| state.price = Number(state.market?.[state.day]?.close ?? 0); | |
| recalc(); | |
| setStatus("CONNECTED"); | |
| renderAll(); | |
| return; | |
| } | |
| if (msg.type === "TICK") { | |
| const d = Number(msg.payload?.day); | |
| if (Number.isFinite(d)) state.day = d; | |
| const px = Number(state.market?.[state.day]?.close ?? state.price); | |
| if (Number.isFinite(px)) state.price = px; | |
| state.leaderboard = Array.isArray(msg.payload?.leaderboard) ? msg.payload.leaderboard : state.leaderboard; | |
| recalc(); | |
| renderAll(); | |
| return; | |
| } | |
| if (msg.type === "NEWS") { | |
| const txt = msg.payload?.text ?? ""; | |
| const day = msg.payload?.day ?? state.day; | |
| $("newsText").textContent = txt ? (`[Day ${day}] ${txt}`) : "No news yet."; | |
| } | |
| }; | |
| ws.onerror = () => setStatus("ERROR"); | |
| ws.onclose = () => setStatus("DISCONNECTED"); | |
| } | |
| // Heartbeat: UPDATE_EQUITY every second when connected, unless liquidated | |
| setInterval(() => { | |
| if (state.liquidated) return; | |
| if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return; | |
| state.ws.send(JSON.stringify({ | |
| type: "UPDATE_EQUITY", | |
| payload: { name: state.clientId, equity: state.equity, roi: state.roi } | |
| })); | |
| }, 1000); | |
| // --------- Boot ---------- | |
| setStatus("DISCONNECTED"); | |
| renderAll(); | |
| connectWS(); | |
| </script> | |
| </body> | |
| </html> | |