MPTrading / index.html
Almaatla's picture
Update index.html
09ff30e verified
<!doctype html>
<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) => ({
"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"
}[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>