Update index.html
Browse files- 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 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 23 |
-
.
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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:
|
|
|
|
| 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:
|
| 65 |
-
|
|
|
|
|
|
|
| 66 |
canvas{ height:200px; }
|
| 67 |
}
|
| 68 |
</style>
|
| 69 |
</head>
|
|
|
|
| 70 |
<body>
|
| 71 |
<header>
|
| 72 |
-
<div class="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
<div class="
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
<div class="
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
</div>
|
| 101 |
-
</div>
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
|
|
|
| 107 |
</div>
|
| 108 |
</div>
|
| 109 |
-
</section>
|
| 110 |
|
| 111 |
-
|
| 112 |
-
<
|
| 113 |
-
|
| 114 |
-
<div class="
|
| 115 |
-
<div class="
|
| 116 |
-
<div class="
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
</div>
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
<div class="
|
| 126 |
-
<div class="sub"><span class="muted">Unrealized P&L:</span> <span id="kpiUPnL">--</span></div>
|
| 127 |
</div>
|
| 128 |
-
<div class="
|
| 129 |
-
<div class="
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
-
|
| 140 |
-
|
|
|
|
| 141 |
</div>
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
</div>
|
|
|
|
| 145 |
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
<div class="
|
| 149 |
-
<
|
| 150 |
-
<
|
| 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 |
-
|
| 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 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
<
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 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 |
-
</
|
| 227 |
-
</
|
| 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,
|
| 266 |
avgCost: 0,
|
| 267 |
|
| 268 |
equity: INITIAL_CASH,
|
|
@@ -272,9 +327,9 @@
|
|
| 272 |
stopLoss: null,
|
| 273 |
takeProfit: null,
|
| 274 |
|
| 275 |
-
equityCurve: [],
|
| 276 |
-
tradeLog: [],
|
| 277 |
-
openLots: [],
|
| 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 |
-
|
| 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 |
-
|
| 414 |
-
|
| 415 |
-
function
|
| 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 |
-
|
| 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 |
-
|
| 482 |
-
|
| 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,
|
| 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
|
| 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 |
-
|
| 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);
|
| 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 |
-
//
|
| 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();
|