Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>HFT Exchange Simulator</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Space+Mono:wght@400;700&display=swap'); | |
| :root { | |
| --bg: #080c10; | |
| --bg1: #0d1219; | |
| --bg2: #121820; | |
| --bg3: #1a2230; | |
| --border: #1e2d3d; | |
| --border2: #253545; | |
| --text: #c8d8e8; | |
| --dim: #5a7a90; | |
| --green: #00e676; | |
| --green2: #00c853; | |
| --red: #ff1744; | |
| --red2: #d50000; | |
| --yellow: #ffea00; | |
| --blue: #40c4ff; | |
| --purple: #ea80fc; | |
| --orange: #ff9100; | |
| --cyan: #18ffff; | |
| --accent: #00e5ff; | |
| --glow: 0 0 12px rgba(0,229,255,0.25); | |
| --glow-g: 0 0 12px rgba(0,230,118,0.3); | |
| --glow-r: 0 0 12px rgba(255,23,68,0.3); | |
| --mono: 'JetBrains Mono', 'Space Mono', monospace; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| font-family: var(--mono); | |
| background: var(--bg); | |
| color: var(--text); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| font-size: 12px; | |
| } | |
| /* Scanline overlay */ | |
| body::before { | |
| content: ''; | |
| position: fixed; inset: 0; | |
| background: repeating-linear-gradient(0deg, | |
| transparent, transparent 2px, | |
| rgba(0,0,0,0.03) 2px, rgba(0,0,0,0.03) 4px); | |
| pointer-events: none; z-index: 9999; | |
| } | |
| /* Header */ | |
| .header { | |
| display: flex; align-items: center; justify-content: space-between; | |
| padding: 10px 20px; | |
| background: linear-gradient(90deg, #050a10 0%, #0d1825 50%, #050a10 100%); | |
| border-bottom: 1px solid var(--accent); | |
| box-shadow: 0 0 30px rgba(0,229,255,0.1); | |
| position: sticky; top: 0; z-index: 100; | |
| } | |
| .logo { | |
| font-size: 15px; font-weight: 700; letter-spacing: 3px; | |
| color: var(--accent); | |
| text-shadow: 0 0 20px rgba(0,229,255,0.6); | |
| } | |
| .logo span { color: var(--green); } | |
| .header-stats { | |
| display: flex; gap: 24px; | |
| } | |
| .hstat { text-align: center; } | |
| .hstat-label { font-size: 9px; color: var(--dim); text-transform: uppercase; letter-spacing: 1px; } | |
| .hstat-val { font-size: 13px; font-weight: 600; } | |
| .conn-dot { | |
| width: 8px; height: 8px; border-radius: 50%; | |
| background: var(--green); | |
| box-shadow: 0 0 8px var(--green); | |
| animation: pulse 2s ease-in-out infinite; | |
| } | |
| @keyframes pulse { | |
| 0%,100% { opacity: 1; } 50% { opacity: 0.4; } | |
| } | |
| /* Main grid */ | |
| .grid { | |
| display: grid; | |
| grid-template-columns: 320px 1fr 300px; | |
| grid-template-rows: auto auto auto; | |
| gap: 1px; | |
| background: var(--border); | |
| height: calc(100vh - 48px); | |
| } | |
| .panel { | |
| background: var(--bg1); | |
| overflow: hidden; | |
| display: flex; flex-direction: column; | |
| } | |
| .panel-title { | |
| font-size: 9px; font-weight: 700; | |
| text-transform: uppercase; letter-spacing: 2px; | |
| color: var(--dim); | |
| padding: 8px 14px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; align-items: center; justify-content: space-between; | |
| background: var(--bg2); | |
| flex-shrink: 0; | |
| } | |
| .panel-title .tag { | |
| font-size: 8px; padding: 2px 6px; | |
| background: var(--bg3); border-radius: 2px; | |
| color: var(--accent); letter-spacing: 1px; | |
| } | |
| .panel-body { flex: 1; overflow-y: auto; padding: 12px; } | |
| .panel-body::-webkit-scrollbar { width: 3px; } | |
| .panel-body::-webkit-scrollbar-track { background: var(--bg1); } | |
| .panel-body::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; } | |
| /* ORDER BOOK */ | |
| .book-header { | |
| display: grid; grid-template-columns: 1fr 80px 80px; | |
| font-size: 9px; color: var(--dim); letter-spacing: 1px; | |
| padding: 4px 12px; border-bottom: 1px solid var(--border); | |
| text-transform: uppercase; | |
| } | |
| .book-row { | |
| display: grid; grid-template-columns: 1fr 80px 80px; | |
| padding: 3px 12px; | |
| position: relative; cursor: default; | |
| transition: background 0.1s; | |
| } | |
| .book-row:hover { background: rgba(255,255,255,0.03); } | |
| .book-bar { | |
| position: absolute; top: 0; bottom: 0; | |
| opacity: 0.12; pointer-events: none; | |
| transition: width 0.3s ease; | |
| } | |
| .ask-row .book-bar { background: var(--red); right: 0; } | |
| .bid-row .book-bar { background: var(--green); left: 0; } | |
| .ask-row .price { color: var(--red); font-weight: 600; } | |
| .bid-row .price { color: var(--green); font-weight: 600; } | |
| .qty-val { text-align: right; color: var(--text); } | |
| .orders-val { text-align: right; color: var(--dim); } | |
| .spread-row { | |
| display: flex; align-items: center; justify-content: center; | |
| padding: 4px; font-size: 9px; color: var(--yellow); | |
| border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); | |
| background: var(--bg2); | |
| letter-spacing: 2px; | |
| } | |
| /* ORDER ENTRY */ | |
| .tabs { | |
| display: flex; border-bottom: 1px solid var(--border); | |
| flex-shrink: 0; | |
| } | |
| .tab { | |
| flex: 1; padding: 8px; font-family: var(--mono); font-size: 9px; | |
| font-weight: 700; letter-spacing: 1px; text-transform: uppercase; | |
| border: none; cursor: pointer; | |
| transition: all 0.2s; | |
| background: var(--bg2); color: var(--dim); | |
| } | |
| .tab.active-buy { background: rgba(0,230,118,0.1); color: var(--green); border-bottom: 2px solid var(--green); } | |
| .tab.active-sell { background: rgba(255,23,68,0.1); color: var(--red); border-bottom: 2px solid var(--red); } | |
| .tab:hover { background: var(--bg3); } | |
| .form-group { margin-bottom: 10px; } | |
| label { font-size: 9px; color: var(--dim); text-transform: uppercase; letter-spacing: 1px; display: block; margin-bottom: 4px; } | |
| input, select { | |
| width: 100%; padding: 8px 10px; | |
| background: var(--bg3); border: 1px solid var(--border2); | |
| color: var(--text); font-family: var(--mono); font-size: 12px; | |
| border-radius: 2px; outline: none; | |
| transition: border-color 0.2s, box-shadow 0.2s; | |
| } | |
| input:focus, select:focus { | |
| border-color: var(--accent); | |
| box-shadow: var(--glow); | |
| } | |
| select option { background: var(--bg2); } | |
| .btn { | |
| width: 100%; padding: 10px; | |
| font-family: var(--mono); font-size: 11px; font-weight: 700; | |
| letter-spacing: 2px; text-transform: uppercase; | |
| border: none; border-radius: 2px; cursor: pointer; | |
| transition: all 0.2s; margin-top: 4px; | |
| } | |
| .btn-buy { | |
| background: linear-gradient(135deg, #00c853, #00e676); | |
| color: #001a0a; | |
| box-shadow: 0 0 15px rgba(0,230,118,0.3); | |
| } | |
| .btn-buy:hover { box-shadow: 0 0 25px rgba(0,230,118,0.5); transform: translateY(-1px); } | |
| .btn-sell { | |
| background: linear-gradient(135deg, #d50000, #ff1744); | |
| color: #1a0000; | |
| box-shadow: 0 0 15px rgba(255,23,68,0.3); | |
| } | |
| .btn-sell:hover { box-shadow: 0 0 25px rgba(255,23,68,0.5); transform: translateY(-1px); } | |
| .btn-neutral { | |
| background: var(--bg3); color: var(--text); | |
| border: 1px solid var(--border2); | |
| } | |
| .btn-neutral:hover { border-color: var(--accent); color: var(--accent); } | |
| .btn-accent { | |
| background: linear-gradient(135deg, #006080, #00acc1); | |
| color: #001a20; | |
| box-shadow: 0 0 15px rgba(0,229,255,0.2); | |
| } | |
| .btn-accent:hover { box-shadow: 0 0 25px rgba(0,229,255,0.4); transform: translateY(-1px); } | |
| /* PRICE CHART */ | |
| canvas { display: block; } | |
| #price-canvas { width: 100%; } | |
| /* LATENCY / STATS */ | |
| .stat-row { | |
| display: flex; justify-content: space-between; | |
| padding: 5px 0; border-bottom: 1px solid rgba(30,45,61,0.5); | |
| } | |
| .stat-key { color: var(--dim); font-size: 10px; } | |
| .stat-val { font-weight: 600; font-size: 11px; } | |
| .green { color: var(--green); } | |
| .red { color: var(--red); } | |
| .yellow { color: var(--yellow); } | |
| .blue { color: var(--blue); } | |
| .cyan { color: var(--cyan); } | |
| /* TRADE LOG */ | |
| .trade-entry { | |
| display: flex; gap: 8px; padding: 5px 0; | |
| border-bottom: 1px solid rgba(30,45,61,0.4); | |
| font-size: 10px; align-items: center; | |
| animation: fadeIn 0.3s ease; | |
| } | |
| @keyframes fadeIn { from { opacity: 0; transform: translateX(-5px); } to { opacity: 1; transform: translateX(0); } } | |
| .trade-side { | |
| padding: 2px 5px; font-size: 8px; font-weight: 700; | |
| letter-spacing: 1px; border-radius: 2px; | |
| min-width: 30px; text-align: center; | |
| } | |
| .buy-badge { background: rgba(0,230,118,0.15); color: var(--green); } | |
| .sell-badge { background: rgba(255,23,68,0.15); color: var(--red); } | |
| /* POSITION TABLE */ | |
| .pos-table { width: 100%; border-collapse: collapse; font-size: 10px; } | |
| .pos-table th { | |
| text-align: left; padding: 4px 6px; | |
| color: var(--dim); font-size: 8px; text-transform: uppercase; letter-spacing: 1px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .pos-table td { padding: 5px 6px; border-bottom: 1px solid rgba(30,45,61,0.3); } | |
| /* BACKTEST PANEL */ | |
| .bt-result { | |
| background: var(--bg2); border: 1px solid var(--border2); | |
| border-radius: 3px; padding: 10px; margin-top: 10px; | |
| } | |
| .bt-metric { | |
| display: flex; justify-content: space-between; | |
| padding: 3px 0; font-size: 10px; | |
| } | |
| /* EQUITY CURVE */ | |
| #equity-canvas { width: 100%; border-top: 1px solid var(--border); } | |
| /* LATENCY BARS */ | |
| .lat-bar-wrap { margin: 4px 0; } | |
| .lat-bar-label { display: flex; justify-content: space-between; font-size: 9px; margin-bottom: 2px; } | |
| .lat-bar { height: 6px; background: var(--bg3); border-radius: 3px; overflow: hidden; } | |
| .lat-bar-fill { height: 100%; border-radius: 3px; transition: width 0.5s ease; } | |
| /* RISK PANEL */ | |
| .risk-meter { | |
| height: 8px; background: var(--bg3); border-radius: 4px; overflow: hidden; | |
| margin: 4px 0 10px; | |
| } | |
| .risk-fill { | |
| height: 100%; border-radius: 4px; transition: width 0.5s ease; | |
| } | |
| /* FEED CONTROL */ | |
| .feed-control { | |
| display: flex; gap: 6px; padding: 8px 12px; | |
| border-top: 1px solid var(--border); | |
| flex-shrink: 0; | |
| } | |
| .feed-control .btn { margin: 0; flex: 1; padding: 7px; font-size: 9px; } | |
| /* Symbol selector strip */ | |
| .sym-strip { | |
| display: flex; gap: 4px; padding: 6px 12px; | |
| border-bottom: 1px solid var(--border); | |
| overflow-x: auto; flex-shrink: 0; | |
| } | |
| .sym-btn { | |
| padding: 3px 10px; font-family: var(--mono); font-size: 9px; | |
| font-weight: 700; letter-spacing: 1px; text-transform: uppercase; | |
| border: 1px solid var(--border2); border-radius: 2px; | |
| cursor: pointer; background: var(--bg3); color: var(--dim); | |
| transition: all 0.15s; white-space: nowrap; | |
| } | |
| .sym-btn.active, .sym-btn:hover { | |
| border-color: var(--accent); color: var(--accent); | |
| background: rgba(0,229,255,0.08); | |
| } | |
| /* Notification toast */ | |
| .toast { | |
| position: fixed; top: 60px; right: 20px; | |
| background: var(--bg2); border: 1px solid var(--accent); | |
| padding: 10px 16px; border-radius: 3px; | |
| font-size: 11px; z-index: 1000; | |
| animation: toastIn 0.3s ease, toastOut 0.3s ease 2.7s forwards; | |
| box-shadow: var(--glow); | |
| } | |
| @keyframes toastIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } } | |
| @keyframes toastOut { to { opacity: 0; transform: translateX(20px); } } | |
| /* Scrollable sections */ | |
| .scroll-area { flex: 1; overflow-y: auto; } | |
| .scroll-area::-webkit-scrollbar { width: 3px; } | |
| .scroll-area::-webkit-scrollbar-thumb { background: var(--border2); } | |
| /* Responsive col spans */ | |
| .span2 { grid-column: span 2; } | |
| .col-left { grid-column: 1; grid-row: 1 / span 3; } | |
| .col-mid { grid-column: 2; } | |
| .col-right { grid-column: 3; grid-row: 1 / span 3; } | |
| .grid-inner { | |
| display: grid; | |
| grid-template-columns: 320px 1fr 300px; | |
| grid-template-rows: 360px 260px 1fr; | |
| gap: 1px; | |
| background: var(--border); | |
| height: calc(100vh - 48px); | |
| } | |
| /* Alert for rejected orders */ | |
| .rejected { border-left: 3px solid var(--red) ; animation: shake 0.3s; } | |
| @keyframes shake { | |
| 0%,100% { transform: translateX(0); } | |
| 25% { transform: translateX(-4px); } | |
| 75% { transform: translateX(4px); } | |
| } | |
| /* Blinking cursor */ | |
| .cursor::after { content: 'β'; animation: blink 1s step-end infinite; color: var(--accent); } | |
| @keyframes blink { 50% { opacity: 0; } } | |
| /* Number animations */ | |
| .num-up { animation: numUp 0.4s ease; } | |
| .num-down { animation: numDown 0.4s ease; } | |
| @keyframes numUp { 0% { color: var(--green); } 100% { color: inherit; } } | |
| @keyframes numDown { 0% { color: var(--red); } 100% { color: inherit; } } | |
| /* Progress ring for latency */ | |
| .ring-wrap { position: relative; display: inline-flex; align-items: center; justify-content: center; } | |
| /* Hover row highlight for order book */ | |
| .book-asks .book-row:hover .price { text-shadow: 0 0 8px var(--red); } | |
| .book-bids .book-row:hover .price { text-shadow: 0 0 8px var(--green); } | |
| /* Section divider */ | |
| .divider { height: 1px; background: var(--border); margin: 10px 0; } | |
| /* Blinking red for reject */ | |
| .flash-red { animation: flashRed 0.5s; } | |
| @keyframes flashRed { 0%,100% { background: var(--bg1); } 50% { background: rgba(255,23,68,0.15); } } | |
| /* Toggle switch */ | |
| .toggle { position: relative; display: inline-flex; align-items: center; gap: 8px; cursor: pointer; } | |
| .toggle input { display: none; } | |
| .toggle-track { | |
| width: 32px; height: 16px; background: var(--bg3); | |
| border: 1px solid var(--border2); border-radius: 8px; | |
| transition: background 0.2s; | |
| position: relative; | |
| } | |
| .toggle input:checked + .toggle-track { background: var(--green2); border-color: var(--green); } | |
| .toggle-thumb { | |
| position: absolute; top: 1px; left: 1px; | |
| width: 12px; height: 12px; border-radius: 6px; | |
| background: var(--dim); transition: transform 0.2s, background 0.2s; | |
| } | |
| .toggle input:checked ~ .toggle-thumb { | |
| transform: translateX(16px); background: white; | |
| } | |
| .mode-tabs { display: flex; gap: 1px; border-bottom: 1px solid var(--border); flex-shrink: 0; } | |
| .mode-tab { | |
| flex: 1; padding: 6px; font-family: var(--mono); font-size: 9px; font-weight: 700; | |
| letter-spacing: 1px; text-transform: uppercase; border: none; cursor: pointer; | |
| background: var(--bg2); color: var(--dim); transition: all 0.15s; | |
| } | |
| .mode-tab.active { background: var(--bg3); color: var(--accent); border-bottom: 1px solid var(--accent); } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- HEADER --> | |
| <div class="header"> | |
| <div style="display:flex;align-items:center;gap:16px"> | |
| <div class="conn-dot" id="conn-dot"></div> | |
| <div class="logo">HFT<span>Β·</span>EXCHANGE<span>Β·</span>SIM</div> | |
| <div style="font-size:9px;color:var(--dim);letter-spacing:1px">C++ MATCHING ENGINE</div> | |
| </div> | |
| <div class="header-stats"> | |
| <div class="hstat"><div class="hstat-label">Orders</div><div class="hstat-val cyan" id="h-orders">0</div></div> | |
| <div class="hstat"><div class="hstat-label">Trades</div><div class="hstat-val green" id="h-trades">0</div></div> | |
| <div class="hstat"><div class="hstat-label">Rejects</div><div class="hstat-val red" id="h-rejects">0</div></div> | |
| <div class="hstat"><div class="hstat-label">P&L</div><div class="hstat-val" id="h-pnl">$0.00</div></div> | |
| <div class="hstat"><div class="hstat-label">p99 Latency</div><div class="hstat-val yellow" id="h-p99">β</div></div> | |
| <div class="hstat"><div class="hstat-label">Throughput</div><div class="hstat-val blue" id="h-thru">β</div></div> | |
| </div> | |
| </div> | |
| <!-- MAIN GRID --> | |
| <div class="grid-inner"> | |
| <!-- COL 1: Order Book + Order Entry --> | |
| <div class="panel" style="grid-column:1;grid-row:1/span 3;display:flex;flex-direction:column"> | |
| <!-- Symbol strip --> | |
| <div class="sym-strip" id="sym-strip"></div> | |
| <!-- Order Book --> | |
| <div class="panel-title"> | |
| Order Book β L2 <span id="book-symbol-tag" class="tag">AAPL</span> | |
| </div> | |
| <div class="book-header"> | |
| <span>PRICE</span><span style="text-align:right">QTY</span><span style="text-align:right">ODS</span> | |
| </div> | |
| <div style="flex:1;overflow-y:auto;display:flex;flex-direction:column"> | |
| <div class="book-asks scroll-area" id="book-asks" style="flex:1;display:flex;flex-direction:column;justify-content:flex-end"></div> | |
| <div class="spread-row" id="spread-row">SPREAD: β</div> | |
| <div class="book-bids scroll-area" id="book-bids" style="flex:1"></div> | |
| </div> | |
| <!-- Order Entry --> | |
| <div class="divider" style="margin:0"></div> | |
| <div class="panel-title">Order Entry</div> | |
| <div class="tabs"> | |
| <button class="tab active-buy" id="tab-buy" onclick="setOrderSide('BUY')">β² BUY</button> | |
| <button class="tab" id="tab-sell" onclick="setOrderSide('SELL')">βΌ SELL</button> | |
| </div> | |
| <div style="padding:12px;flex-shrink:0"> | |
| <div class="form-group"> | |
| <label>Type</label> | |
| <select id="oe-type" onchange="updateOrderForm()"> | |
| <option value="LIMIT">LIMIT</option> | |
| <option value="MARKET">MARKET</option> | |
| <option value="IOC">IOC (Immediate-Or-Cancel)</option> | |
| <option value="FOK">FOK (Fill-Or-Kill)</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label>Symbol</label> | |
| <select id="oe-symbol"> | |
| <option>AAPL</option><option>GOOG</option><option>MSFT</option> | |
| <option>TSLA</option><option>NVDA</option> | |
| </select> | |
| </div> | |
| <div class="form-group" id="oe-price-group"> | |
| <label>Price</label> | |
| <input type="number" id="oe-price" step="0.01" placeholder="0.00"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Quantity</label> | |
| <input type="number" id="oe-qty" value="100" min="1"> | |
| </div> | |
| <div class="form-group" style="display:none"> | |
| <label>Client ID (optional)</label> | |
| <input type="text" id="oe-cid" placeholder="STRAT_01"> | |
| </div> | |
| <button class="btn btn-buy" id="submit-btn" onclick="submitOrder()">β² PLACE BUY ORDER</button> | |
| </div> | |
| <div class="feed-control"> | |
| <button class="btn btn-neutral" onclick="feedStep(10)">βΆ +10 TICKS</button> | |
| <button class="btn btn-neutral" onclick="feedStep(100)">βΆβΆ +100</button> | |
| <button class="btn btn-accent" onclick="autoFeed()" id="auto-btn">AUTO</button> | |
| </div> | |
| </div> | |
| <!-- COL 2, ROW 1: Price Chart --> | |
| <div class="panel" style="grid-column:2;grid-row:1"> | |
| <div class="panel-title"> | |
| Price Feed β Real-time Tick | |
| <div style="display:flex;gap:10px;align-items:center"> | |
| <span id="chart-symbol" class="tag">AAPL</span> | |
| <span id="chart-price" style="font-size:13px;font-weight:700;color:var(--accent)">β</span> | |
| </div> | |
| </div> | |
| <canvas id="price-canvas" height="290"></canvas> | |
| </div> | |
| <!-- COL 2, ROW 2: Latency / Throughput --> | |
| <div class="panel" style="grid-column:2;grid-row:2"> | |
| <div class="panel-title">Latency Profiler <span class="tag">C++ ENGINE</span></div> | |
| <div class="panel-body" style="display:grid;grid-template-columns:1fr 1fr;gap:16px"> | |
| <div> | |
| <div style="font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Order Latency</div> | |
| <div class="lat-bar-wrap"> | |
| <div class="lat-bar-label"><span style="color:var(--dim)">p50</span><span id="lat-o-p50" style="color:var(--green)">β</span></div> | |
| <div class="lat-bar"><div class="lat-bar-fill" id="lat-o-p50-bar" style="background:var(--green);width:0%"></div></div> | |
| </div> | |
| <div class="lat-bar-wrap"> | |
| <div class="lat-bar-label"><span style="color:var(--dim)">p99</span><span id="lat-o-p99" style="color:var(--yellow)">β</span></div> | |
| <div class="lat-bar"><div class="lat-bar-fill" id="lat-o-p99-bar" style="background:var(--yellow);width:0%"></div></div> | |
| </div> | |
| <div class="lat-bar-wrap"> | |
| <div class="lat-bar-label"><span style="color:var(--dim)">avg</span><span id="lat-o-avg" style="color:var(--blue)">β</span></div> | |
| <div class="lat-bar"><div class="lat-bar-fill" id="lat-o-avg-bar" style="background:var(--blue);width:0%"></div></div> | |
| </div> | |
| <div class="divider"></div> | |
| <div class="stat-row"><span class="stat-key">Checked</span><span id="risk-checked" class="stat-val cyan">0</span></div> | |
| <div class="stat-row"><span class="stat-key">Rejected</span><span id="risk-rejected" class="stat-val red">0</span></div> | |
| <div class="stat-row"><span class="stat-key">Fill Rate</span><span id="fill-rate" class="stat-val green">0%</span></div> | |
| </div> | |
| <div> | |
| <div style="font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Tick Latency</div> | |
| <div class="lat-bar-wrap"> | |
| <div class="lat-bar-label"><span style="color:var(--dim)">p50</span><span id="lat-t-p50" style="color:var(--green)">β</span></div> | |
| <div class="lat-bar"><div class="lat-bar-fill" id="lat-t-p50-bar" style="background:var(--green);width:0%"></div></div> | |
| </div> | |
| <div class="lat-bar-wrap"> | |
| <div class="lat-bar-label"><span style="color:var(--dim)">p99</span><span id="lat-t-p99" style="color:var(--yellow)">β</span></div> | |
| <div class="lat-bar"><div class="lat-bar-fill" id="lat-t-p99-bar" style="background:var(--yellow);width:0%"></div></div> | |
| </div> | |
| <div class="divider"></div> | |
| <div class="stat-row"><span class="stat-key">Feed Size</span><span id="feed-size" class="stat-val">β</span></div> | |
| <div class="stat-row"><span class="stat-key">Feed Index</span><span id="feed-idx" class="stat-val">β</span></div> | |
| <div class="stat-row"><span class="stat-key">Total P&L</span><span id="stat-pnl" class="stat-val">β</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- COL 2, ROW 3: Trade Log --> | |
| <div class="panel" style="grid-column:2;grid-row:3"> | |
| <div class="panel-title"> | |
| Trade Log | |
| <div style="display:flex;gap:8px"> | |
| <span id="trade-count" class="tag">0 trades</span> | |
| <button class="btn btn-neutral" style="padding:2px 8px;font-size:8px;margin:0;width:auto" onclick="resetSim()">RESET</button> | |
| </div> | |
| </div> | |
| <div class="scroll-area" style="flex:1" id="trade-log"></div> | |
| </div> | |
| <!-- COL 3: Positions + Backtest + Risk --> | |
| <div class="panel" style="grid-column:3;grid-row:1/span 3;display:flex;flex-direction:column"> | |
| <div class="mode-tabs"> | |
| <button class="mode-tab active" onclick="setRightMode('positions')">POSITIONS</button> | |
| <button class="mode-tab" onclick="setRightMode('backtest')">BACKTEST</button> | |
| <button class="mode-tab" onclick="setRightMode('risk')">RISK</button> | |
| </div> | |
| <!-- POSITIONS VIEW --> | |
| <div id="view-positions" style="flex:1;overflow:hidden;display:flex;flex-direction:column"> | |
| <div class="panel-title">Live Positions</div> | |
| <div class="panel-body" style="overflow:auto"> | |
| <table class="pos-table" id="pos-table"> | |
| <thead><tr> | |
| <th>SYM</th><th>POS</th><th>AVG PX</th><th>RPNL</th><th>UPNL</th> | |
| </tr></thead> | |
| <tbody id="pos-body"></tbody> | |
| </table> | |
| <div class="divider"></div> | |
| <div style="font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px">Recent Orders</div> | |
| <div id="order-log" style="max-height:200px;overflow-y:auto"></div> | |
| </div> | |
| </div> | |
| <!-- BACKTEST VIEW --> | |
| <div id="view-backtest" style="flex:1;overflow:hidden;display:flex;flex-direction:column;display:none"> | |
| <div class="panel-title">Backtest Engine <span class="tag">DETERMINISTIC</span></div> | |
| <div class="panel-body" style="overflow:auto"> | |
| <div class="form-group"> | |
| <label>Strategy</label> | |
| <select id="bt-strategy"> | |
| <option value="MarketMaking">Market Making (MM)</option> | |
| <option value="Momentum">Momentum (EMA Cross)</option> | |
| <option value="MeanReversion">Mean Reversion (Z-Score)</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label>Symbol</label> | |
| <select id="bt-symbol"> | |
| <option>AAPL</option><option>GOOG</option><option>MSFT</option> | |
| <option>TSLA</option><option>NVDA</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label>Ticks to Replay</label> | |
| <input type="number" id="bt-ticks" value="5000" min="200" max="50000"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Start Price</label> | |
| <input type="number" id="bt-price" value="185.00" step="0.01"> | |
| </div> | |
| <button class="btn btn-accent" onclick="runBacktest()" id="bt-btn">βΆ RUN BACKTEST</button> | |
| <div id="bt-result" style="display:none" class="bt-result"> | |
| <div style="font-size:9px;color:var(--dim);letter-spacing:1px;margin-bottom:8px">BACKTEST RESULTS</div> | |
| <div class="bt-metric"><span class="stat-key">Strategy</span><span id="bt-r-strategy" class="stat-val cyan"></span></div> | |
| <div class="bt-metric"><span class="stat-key">Ticks</span><span id="bt-r-ticks" class="stat-val"></span></div> | |
| <div class="bt-metric"><span class="stat-key">Orders</span><span id="bt-r-orders" class="stat-val"></span></div> | |
| <div class="bt-metric"><span class="stat-key">Fills</span><span id="bt-r-fills" class="stat-val green"></span></div> | |
| <div class="bt-metric"><span class="stat-key">Rejected</span><span id="bt-r-rej" class="stat-val red"></span></div> | |
| <div class="bt-metric"><span class="stat-key">Trades</span><span id="bt-r-trades" class="stat-val"></span></div> | |
| <div class="bt-metric"><span class="stat-key">Total P&L</span><span id="bt-r-pnl" class="stat-val"></span></div> | |
| <div class="bt-metric"><span class="stat-key">Max Drawdown</span><span id="bt-r-dd" class="stat-val red"></span></div> | |
| <div class="bt-metric"><span class="stat-key">Sharpe</span><span id="bt-r-sharpe" class="stat-val yellow"></span></div> | |
| <div class="bt-metric"><span class="stat-key">Order p50</span><span id="bt-r-p50" class="stat-val green"></span></div> | |
| <div class="bt-metric"><span class="stat-key">Order p99</span><span id="bt-r-p99" class="stat-val yellow"></span></div> | |
| <div class="bt-metric"><span class="stat-key">Throughput</span><span id="bt-r-thru" class="stat-val blue"></span></div> | |
| <canvas id="equity-canvas" height="100" style="margin-top:10px;width:100%;border:1px solid var(--border)"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- RISK VIEW --> | |
| <div id="view-risk" style="flex:1;overflow:hidden;display:flex;flex-direction:column;display:none"> | |
| <div class="panel-title">Pre-Trade Risk <span class="tag">LIVE</span></div> | |
| <div class="panel-body" style="overflow:auto"> | |
| <div class="form-group"> | |
| <label>Max Order Qty</label> | |
| <input type="number" id="r-max-qty" value="10000"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Max Position</label> | |
| <input type="number" id="r-max-pos" value="500000"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Max Notional ($)</label> | |
| <input type="number" id="r-max-notional" value="50000000"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Max Orders/sec</label> | |
| <input type="number" id="r-max-rate" value="2000"> | |
| </div> | |
| <button class="btn btn-accent" onclick="setRiskLimits()">UPDATE LIMITS</button> | |
| <div class="divider"></div> | |
| <div style="font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Risk Utilization</div> | |
| <div class="lat-bar-wrap"> | |
| <div class="lat-bar-label"><span style="color:var(--dim)">Order Rate</span><span id="r-rate-pct" style="color:var(--yellow)">0%</span></div> | |
| <div class="lat-bar"><div class="lat-bar-fill" id="r-rate-bar" style="width:0%;background:var(--yellow)"></div></div> | |
| </div> | |
| <div class="lat-bar-wrap"> | |
| <div class="lat-bar-label"><span style="color:var(--dim)">Reject Rate</span><span id="r-rej-pct" style="color:var(--red)">0%</span></div> | |
| <div class="lat-bar"><div class="lat-bar-fill" id="r-rej-bar" style="width:0%;background:var(--red)"></div></div> | |
| </div> | |
| <div class="divider"></div> | |
| <div class="stat-row"><span class="stat-key">Checked</span><span id="r-checked" class="stat-val">0</span></div> | |
| <div class="stat-row"><span class="stat-key">Approved</span><span id="r-approved" class="stat-val green">0</span></div> | |
| <div class="stat-row"><span class="stat-key">Rejected</span><span id="r-rejected2" class="stat-val red">0</span></div> | |
| <div class="divider"></div> | |
| <div style="font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Risk Check Types</div> | |
| <div style="font-size:10px;color:var(--text);line-height:1.8"> | |
| β Max Order Size<br> | |
| β Max Position Limit<br> | |
| β Max Notional Exposure<br> | |
| β Max Order Rate (1s window)<br> | |
| β Invalid Price/Qty<br> | |
| β Symbol Validation | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // ββ State βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const API = ''; | |
| let selectedSymbol = 'AAPL'; | |
| let orderSide = 'BUY'; | |
| let autoFeedInterval = null; | |
| let priceHistory = {}; // symbol -> [{ts, price}] | |
| let rightMode = 'positions'; | |
| // Price chart | |
| const priceCtx = document.getElementById('price-canvas'); | |
| let priceChart = null; | |
| // ββ Init ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| window.addEventListener('DOMContentLoaded', () => { | |
| buildSymbolStrip(); | |
| selectSymbol('AAPL'); | |
| startPolling(); | |
| feedStep(50); // warm up | |
| }); | |
| function buildSymbolStrip() { | |
| const syms = ['AAPL','GOOG','MSFT','TSLA','NVDA']; | |
| const strip = document.getElementById('sym-strip'); | |
| strip.innerHTML = syms.map(s => | |
| `<button class="sym-btn ${s===selectedSymbol?'active':''}" onclick="selectSymbol('${s}')">${s}</button>` | |
| ).join(''); | |
| } | |
| function selectSymbol(sym) { | |
| selectedSymbol = sym; | |
| document.getElementById('oe-symbol').value = sym; | |
| document.getElementById('book-symbol-tag').textContent = sym; | |
| document.getElementById('chart-symbol').textContent = sym; | |
| buildSymbolStrip(); | |
| refreshBook(); | |
| refreshChart(); | |
| } | |
| // ββ API calls βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function api(method, path, body=null) { | |
| const opts = { method, headers: {'Content-Type':'application/json'} }; | |
| if (body) opts.body = JSON.stringify(body); | |
| const r = await fetch(API + path, opts); | |
| return r.json(); | |
| } | |
| // ββ Polling βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function startPolling() { | |
| refreshStatus(); | |
| refreshBook(); | |
| refreshPositions(); | |
| refreshTrades(); | |
| setInterval(refreshStatus, 1500); | |
| setInterval(refreshBook, 500); | |
| setInterval(refreshPositions, 2000); | |
| setInterval(refreshTrades, 1000); | |
| } | |
| async function refreshStatus() { | |
| const d = await api('GET', '/api/status'); | |
| if (d.error) return; | |
| setVal('h-orders', d.total_orders); | |
| setVal('h-trades', d.total_trades); | |
| setVal('h-rejects', d.total_rejects); | |
| const pnl = d.total_pnl || 0; | |
| const pnlEl = document.getElementById('h-pnl'); | |
| pnlEl.textContent = '$' + pnl.toFixed(2); | |
| pnlEl.className = 'hstat-val ' + (pnl >= 0 ? 'green' : 'red'); | |
| const p99 = d.order_p99_us || 0; | |
| document.getElementById('h-p99').textContent = p99 > 0 ? p99.toFixed(1)+'Β΅s' : 'β'; | |
| // Latency bars (relative to 1000Β΅s max) | |
| setLatBar('lat-o-p50', 'lat-o-p50-bar', d.order_p50_us, 500); | |
| setLatBar('lat-o-p99', 'lat-o-p99-bar', d.order_p99_us, 500); | |
| setLatBar('lat-t-p50', 'lat-t-p50-bar', d.tick_p50_us, 100); | |
| setLatBar('lat-t-p99', 'lat-t-p99-bar', d.tick_p99_us, 100); | |
| setVal('risk-checked', d.risk_checked); | |
| setVal('risk-rejected', d.risk_rejected); | |
| setVal('r-checked', d.risk_checked); | |
| setVal('r-rejected2', d.risk_rejected); | |
| setVal('r-approved', Math.max(0, d.risk_checked - d.risk_rejected)); | |
| const fillRate = d.total_orders > 0 | |
| ? ((d.total_trades / d.total_orders) * 100).toFixed(1)+'%' | |
| : '0%'; | |
| document.getElementById('fill-rate').textContent = fillRate; | |
| setVal('feed-size', d.feed_size); | |
| setVal('feed-idx', d.feed_idx); | |
| const pnlEl2 = document.getElementById('stat-pnl'); | |
| pnlEl2.textContent = '$' + pnl.toFixed(2); | |
| pnlEl2.className = 'stat-val ' + (pnl >= 0 ? 'green' : 'red'); | |
| // Reject rate bar | |
| if (d.risk_checked > 0) { | |
| const rejPct = (d.risk_rejected / d.risk_checked * 100); | |
| setBarWidth('r-rej-bar', rejPct); | |
| document.getElementById('r-rej-pct').textContent = rejPct.toFixed(1)+'%'; | |
| } | |
| // Connection indicator | |
| document.getElementById('conn-dot').style.background = 'var(--green)'; | |
| } | |
| function setLatBar(valId, barId, val, maxVal) { | |
| const el = document.getElementById(valId); | |
| const bar = document.getElementById(barId); | |
| if (!el || !bar) return; | |
| if (val === undefined || val === null) return; | |
| el.textContent = val > 0 ? val.toFixed(2)+'Β΅s' : '< 1Β΅s'; | |
| const pct = Math.min(100, (val / maxVal) * 100); | |
| bar.style.width = pct + '%'; | |
| } | |
| function setBarWidth(id, pct) { | |
| const el = document.getElementById(id); | |
| if (el) el.style.width = Math.min(100, pct) + '%'; | |
| } | |
| function setVal(id, val) { | |
| const el = document.getElementById(id); | |
| if (el) el.textContent = val; | |
| } | |
| // ββ Order Book ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function refreshBook() { | |
| const d = await api('GET', `/api/book/${selectedSymbol}`); | |
| if (d.error || !d.asks) return; | |
| renderBook(d); | |
| } | |
| function renderBook(d) { | |
| const maxQty = Math.max( | |
| ...(d.bids||[]).map(b=>b.qty), | |
| ...(d.asks||[]).map(a=>a.qty), 1 | |
| ); | |
| // Asks: show in ascending order but display reversed (best ask at bottom) | |
| const asks = (d.asks||[]).slice(0,10); | |
| const asksEl = document.getElementById('book-asks'); | |
| asksEl.innerHTML = [...asks].reverse().map(a => { | |
| const pct = (a.qty / maxQty * 100).toFixed(1); | |
| return `<div class="book-row ask-row"> | |
| <div class="book-bar" style="width:${pct}%"></div> | |
| <span class="price">${a.price.toFixed(4)}</span> | |
| <span class="qty-val">${fmtNum(a.qty)}</span> | |
| <span class="orders-val">${a.orders}</span> | |
| </div>`; | |
| }).join(''); | |
| const spreadEl = document.getElementById('spread-row'); | |
| if (d.spread > 0) { | |
| spreadEl.textContent = `BID ${d.best_bid.toFixed(4)} βββ SPREAD ${d.spread.toFixed(4)} βββ ASK ${d.best_ask.toFixed(4)}`; | |
| } | |
| // Update chart price | |
| const mid = d.mid || d.best_bid; | |
| if (mid > 0) { | |
| document.getElementById('chart-price').textContent = '$' + mid.toFixed(2); | |
| } | |
| const bids = (d.bids||[]).slice(0,10); | |
| const bidsEl = document.getElementById('book-bids'); | |
| bidsEl.innerHTML = bids.map(b => { | |
| const pct = (b.qty / maxQty * 100).toFixed(1); | |
| return `<div class="book-row bid-row"> | |
| <div class="book-bar" style="width:${pct}%"></div> | |
| <span class="price">${b.price.toFixed(4)}</span> | |
| <span class="qty-val">${fmtNum(b.qty)}</span> | |
| <span class="orders-val">${b.orders}</span> | |
| </div>`; | |
| }).join(''); | |
| } | |
| // ββ Price Chart βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const MAX_HISTORY = 300; | |
| async function refreshChart() { | |
| // Just use feed data from status β we'll seed from order book mid | |
| } | |
| function addPricePoint(sym, price, ts) { | |
| if (!priceHistory[sym]) priceHistory[sym] = []; | |
| priceHistory[sym].push({price, ts}); | |
| if (priceHistory[sym].length > MAX_HISTORY) | |
| priceHistory[sym].shift(); | |
| if (sym === selectedSymbol) drawPriceChart(); | |
| } | |
| function drawPriceChart() { | |
| const canvas = document.getElementById('price-canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const W = canvas.parentElement.offsetWidth; | |
| const H = 290; | |
| canvas.width = W; canvas.height = H; | |
| ctx.fillStyle = '#0d1219'; | |
| ctx.fillRect(0, 0, W, H); | |
| const data = priceHistory[selectedSymbol] || []; | |
| if (data.length < 2) { | |
| ctx.fillStyle = '#1e2d3d'; | |
| ctx.font = '11px JetBrains Mono'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('Feed ticks to populate chart', W/2, H/2); | |
| return; | |
| } | |
| const prices = data.map(d => d.price); | |
| const minP = Math.min(...prices); | |
| const maxP = Math.max(...prices); | |
| const range = maxP - minP || 1; | |
| const pad = { t:20, r:60, b:30, l:10 }; | |
| const cW = W - pad.l - pad.r; | |
| const cH = H - pad.t - pad.b; | |
| const xOf = i => pad.l + (i / (data.length-1)) * cW; | |
| const yOf = p => pad.t + (1 - (p - minP) / range) * cH; | |
| // Grid lines | |
| ctx.strokeStyle = '#1a2230'; | |
| ctx.lineWidth = 1; | |
| for (let i = 0; i <= 4; i++) { | |
| const y = pad.t + (i / 4) * cH; | |
| ctx.beginPath(); ctx.moveTo(pad.l, y); ctx.lineTo(W - pad.r, y); ctx.stroke(); | |
| const price = maxP - (i / 4) * range; | |
| ctx.fillStyle = '#5a7a90'; ctx.font = '9px JetBrains Mono'; | |
| ctx.textAlign = 'right'; | |
| ctx.fillText('$' + price.toFixed(2), W - 5, y + 3); | |
| } | |
| // Gradient fill | |
| const grad = ctx.createLinearGradient(0, pad.t, 0, H - pad.b); | |
| const last = data[data.length - 1]; | |
| const first = data[0]; | |
| const isUp = last.price >= first.price; | |
| if (isUp) { | |
| grad.addColorStop(0, 'rgba(0,230,118,0.2)'); | |
| grad.addColorStop(1, 'rgba(0,230,118,0.01)'); | |
| } else { | |
| grad.addColorStop(0, 'rgba(255,23,68,0.2)'); | |
| grad.addColorStop(1, 'rgba(255,23,68,0.01)'); | |
| } | |
| ctx.beginPath(); | |
| ctx.moveTo(xOf(0), yOf(data[0].price)); | |
| for (let i = 1; i < data.length; i++) { | |
| const x1 = xOf(i-1), x2 = xOf(i); | |
| const y1 = yOf(data[i-1].price), y2 = yOf(data[i].price); | |
| const cpX = (x1 + x2) / 2; | |
| ctx.bezierCurveTo(cpX, y1, cpX, y2, x2, y2); | |
| } | |
| ctx.lineTo(xOf(data.length-1), H - pad.b); | |
| ctx.lineTo(xOf(0), H - pad.b); | |
| ctx.closePath(); | |
| ctx.fillStyle = grad; | |
| ctx.fill(); | |
| // Line | |
| ctx.beginPath(); | |
| ctx.moveTo(xOf(0), yOf(data[0].price)); | |
| for (let i = 1; i < data.length; i++) { | |
| const x1 = xOf(i-1), x2 = xOf(i); | |
| const y1 = yOf(data[i-1].price), y2 = yOf(data[i].price); | |
| const cpX = (x1 + x2) / 2; | |
| ctx.bezierCurveTo(cpX, y1, cpX, y2, x2, y2); | |
| } | |
| ctx.strokeStyle = isUp ? '#00e676' : '#ff1744'; | |
| ctx.lineWidth = 2; | |
| ctx.shadowColor = isUp ? '#00e676' : '#ff1744'; | |
| ctx.shadowBlur = 6; | |
| ctx.stroke(); | |
| ctx.shadowBlur = 0; | |
| // Last price dot | |
| const lx = xOf(data.length - 1); | |
| const ly = yOf(last.price); | |
| ctx.beginPath(); ctx.arc(lx, ly, 4, 0, Math.PI*2); | |
| ctx.fillStyle = isUp ? '#00e676' : '#ff1744'; | |
| ctx.fill(); | |
| // OHLC mini labels | |
| ctx.fillStyle = '#5a7a90'; ctx.font = '9px JetBrains Mono'; | |
| ctx.textAlign = 'left'; | |
| ctx.fillText(`β ${maxP.toFixed(2)}`, pad.l + 5, pad.t + 12); | |
| ctx.fillText(`β ${minP.toFixed(2)}`, pad.l + 5, H - pad.b - 5); | |
| } | |
| // ββ Feed ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function feedStep(n) { | |
| const ticks = await api('POST', '/api/feed/step', {n}); | |
| if (!Array.isArray(ticks)) return; | |
| ticks.forEach(t => { | |
| addPricePoint(t.symbol, t.last, t.ts); | |
| if (t.symbol === selectedSymbol) { | |
| document.getElementById('chart-price').textContent = '$' + t.last.toFixed(2); | |
| // Pre-fill order price | |
| const priceInput = document.getElementById('oe-price'); | |
| if (!priceInput.value || priceInput.value == '0') { | |
| priceInput.value = t.bid.toFixed(2); | |
| } | |
| } | |
| }); | |
| refreshBook(); | |
| } | |
| let autoRunning = false; | |
| function autoFeed() { | |
| const btn = document.getElementById('auto-btn'); | |
| if (autoRunning) { | |
| clearInterval(autoFeedInterval); | |
| autoRunning = false; | |
| btn.textContent = 'AUTO'; | |
| btn.classList.remove('btn-sell'); | |
| btn.classList.add('btn-accent'); | |
| } else { | |
| autoRunning = true; | |
| btn.textContent = 'β STOP'; | |
| btn.classList.remove('btn-accent'); | |
| btn.classList.add('btn-sell'); | |
| autoFeedInterval = setInterval(() => feedStep(5), 200); | |
| } | |
| } | |
| // ββ Order Entry βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function setOrderSide(side) { | |
| orderSide = side; | |
| const buyTab = document.getElementById('tab-buy'); | |
| const sellTab = document.getElementById('tab-sell'); | |
| const btn = document.getElementById('submit-btn'); | |
| if (side === 'BUY') { | |
| buyTab.className = 'tab active-buy'; | |
| sellTab.className = 'tab'; | |
| btn.className = 'btn btn-buy'; | |
| btn.textContent = 'β² PLACE BUY ORDER'; | |
| } else { | |
| buyTab.className = 'tab'; | |
| sellTab.className = 'tab active-sell'; | |
| btn.className = 'btn btn-sell'; | |
| btn.textContent = 'βΌ PLACE SELL ORDER'; | |
| } | |
| } | |
| function updateOrderForm() { | |
| const type = document.getElementById('oe-type').value; | |
| const priceGroup = document.getElementById('oe-price-group'); | |
| priceGroup.style.display = (type === 'MARKET') ? 'none' : 'block'; | |
| } | |
| async function submitOrder() { | |
| const sym = document.getElementById('oe-symbol').value; | |
| const type = document.getElementById('oe-type').value; | |
| const price = parseFloat(document.getElementById('oe-price').value) || 0; | |
| const qty = parseInt(document.getElementById('oe-qty').value) || 100; | |
| const cid = document.getElementById('oe-cid').value; | |
| const body = { | |
| symbol: sym, side: orderSide, type, | |
| price, qty, client_id: cid, | |
| market_price: price | |
| }; | |
| const r = await api('POST', '/api/order', body); | |
| if (!r) return; | |
| if (r.approved) { | |
| showToast(`β ${orderSide} ${qty} ${sym} @ ${type === 'MARKET' ? 'MKT' : '$'+price} β ${r.order.status}`, 'green'); | |
| if (r.trades && r.trades.length > 0) { | |
| showToast(`β‘ ${r.trades.length} trade(s) executed!`, 'green'); | |
| } | |
| } else { | |
| showToast(`β REJECTED: ${r.reject_reason}`, 'red'); | |
| document.body.classList.add('flash-red'); | |
| setTimeout(() => document.body.classList.remove('flash-red'), 500); | |
| } | |
| refreshBook(); | |
| refreshPositions(); | |
| refreshTrades(); | |
| } | |
| // ββ Positions βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function refreshPositions() { | |
| const pos = await api('GET', '/api/positions'); | |
| if (!Array.isArray(pos)) return; | |
| const tbody = document.getElementById('pos-body'); | |
| tbody.innerHTML = pos.filter(p => p.net_qty !== 0).map(p => { | |
| const rPnl = p.realized_pnl || 0; | |
| const uPnl = p.unrealized_pnl || 0; | |
| const posDir = p.net_qty > 0 ? '+' : ''; | |
| return `<tr> | |
| <td style="color:var(--cyan);font-weight:700">${p.symbol}</td> | |
| <td style="color:${p.net_qty>0?'var(--green)':'var(--red)'}">${posDir}${fmtNum(p.net_qty)}</td> | |
| <td>$${p.avg_price.toFixed(2)}</td> | |
| <td style="color:${rPnl>=0?'var(--green)':'var(--red)'}">${rPnl>=0?'+':''}$${rPnl.toFixed(2)}</td> | |
| <td style="color:${uPnl>=0?'var(--green)':'var(--red)'}">${uPnl>=0?'+':''}$${uPnl.toFixed(2)}</td> | |
| </tr>`; | |
| }).join('') || '<tr><td colspan="5" style="text-align:center;color:var(--dim);padding:10px">No open positions</td></tr>'; | |
| // Order log | |
| const orders = await api('GET', '/api/orders'); | |
| if (!Array.isArray(orders)) return; | |
| const logEl = document.getElementById('order-log'); | |
| logEl.innerHTML = orders.slice(-20).reverse().map(o => { | |
| const isRej = o.status === 'REJECTED'; | |
| return `<div class="trade-entry" style="${isRej?'border-left:2px solid var(--red)':''}"> | |
| <span class="trade-side ${o.side==='BUY'?'buy-badge':'sell-badge'}">${o.side}</span> | |
| <span style="color:var(--cyan)">${o.symbol}</span> | |
| <span style="color:var(--dim)">${o.type}</span> | |
| <span>$${(o.price||0).toFixed(2)}</span> | |
| <span style="color:var(--dim)">Γ${fmtNum(o.qty)}</span> | |
| <span style="color:${isRej?'var(--red)':o.status==='FILLED'?'var(--green)':'var(--dim)'}"> | |
| ${o.status} | |
| </span> | |
| </div>`; | |
| }).join(''); | |
| } | |
| // ββ Trades ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function refreshTrades() { | |
| const trades = await api('GET', '/api/trades'); | |
| if (!Array.isArray(trades)) return; | |
| document.getElementById('trade-count').textContent = `${trades.length} trades`; | |
| document.getElementById('h-trades').textContent = trades.length; | |
| const logEl = document.getElementById('trade-log'); | |
| logEl.innerHTML = trades.slice().reverse().slice(0, 100).map(t => { | |
| const time = new Date(t.timestamp / 1e6).toISOString().substr(11,12); | |
| return `<div class="trade-entry" style="padding:5px 12px"> | |
| <span style="color:var(--dim);font-size:9px">${time}</span> | |
| <span style="color:var(--cyan);font-weight:700">${t.symbol}</span> | |
| <span style="color:var(--text)">$${t.price.toFixed(4)}</span> | |
| <span style="color:var(--dim)">Γ</span> | |
| <span style="font-weight:600">${fmtNum(t.qty)}</span> | |
| <span style="color:var(--dim);font-size:9px">#${t.trade_id}</span> | |
| </div>`; | |
| }).join(''); | |
| } | |
| // ββ Backtest ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function runBacktest() { | |
| const btn = document.getElementById('bt-btn'); | |
| btn.textContent = 'β³ RUNNING...'; btn.disabled = true; | |
| try { | |
| const r = await api('POST', '/api/backtest', { | |
| strategy: document.getElementById('bt-strategy').value, | |
| symbol: document.getElementById('bt-symbol').value, | |
| ticks: parseInt(document.getElementById('bt-ticks').value), | |
| start_price: parseFloat(document.getElementById('bt-price').value) | |
| }); | |
| document.getElementById('bt-result').style.display = 'block'; | |
| setVal('bt-r-strategy', r.strategy); | |
| setVal('bt-r-ticks', fmtNum(r.ticks_processed)); | |
| setVal('bt-r-orders', fmtNum(r.orders_submitted)); | |
| setVal('bt-r-fills', fmtNum(r.orders_filled)); | |
| setVal('bt-r-rej', fmtNum(r.orders_rejected)); | |
| setVal('bt-r-trades', fmtNum(r.trades_count)); | |
| const pnlEl = document.getElementById('bt-r-pnl'); | |
| pnlEl.textContent = (r.total_pnl >= 0 ? '+' : '') + '$' + r.total_pnl.toFixed(2); | |
| pnlEl.className = 'stat-val ' + (r.total_pnl >= 0 ? 'green' : 'red'); | |
| document.getElementById('bt-r-dd').textContent = '-$' + r.max_drawdown.toFixed(2); | |
| document.getElementById('bt-r-sharpe').textContent = r.sharpe.toFixed(3); | |
| document.getElementById('bt-r-p50').textContent = r.order_p50_us.toFixed(2) + 'Β΅s'; | |
| document.getElementById('bt-r-p99').textContent = r.order_p99_us.toFixed(2) + 'Β΅s'; | |
| document.getElementById('bt-r-thru').textContent = fmtNum(Math.round(r.throughput_eps)) + ' ev/s'; | |
| drawEquityCurve(r.equity_curve || []); | |
| } finally { | |
| btn.textContent = 'βΆ RUN BACKTEST'; btn.disabled = false; | |
| } | |
| } | |
| function drawEquityCurve(curve) { | |
| const canvas = document.getElementById('equity-canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const W = canvas.parentElement.offsetWidth - 2; | |
| const H = 100; | |
| canvas.width = W; canvas.height = H; | |
| ctx.fillStyle = '#0d1219'; | |
| ctx.fillRect(0,0,W,H); | |
| if (curve.length < 2) { return; } | |
| const minV = Math.min(...curve); | |
| const maxV = Math.max(...curve); | |
| const range = maxV - minV || 1; | |
| const pad = 15; | |
| const xOf = i => pad + (i / (curve.length-1)) * (W - 2*pad); | |
| const yOf = v => pad + (1 - (v - minV)/range) * (H - 2*pad); | |
| // Zero line | |
| const zy = yOf(0); | |
| if (zy > pad && zy < H-pad) { | |
| ctx.strokeStyle = '#1e2d3d'; | |
| ctx.setLineDash([3,3]); | |
| ctx.beginPath(); ctx.moveTo(pad, zy); ctx.lineTo(W-pad, zy); ctx.stroke(); | |
| ctx.setLineDash([]); | |
| } | |
| ctx.beginPath(); | |
| ctx.moveTo(xOf(0), yOf(curve[0])); | |
| for (let i = 1; i < curve.length; i++) ctx.lineTo(xOf(i), yOf(curve[i])); | |
| ctx.strokeStyle = curve[curve.length-1] >= 0 ? '#00e676' : '#ff1744'; | |
| ctx.lineWidth = 1.5; | |
| ctx.stroke(); | |
| ctx.fillStyle = '#5a7a90'; ctx.font = '8px JetBrains Mono'; | |
| ctx.textAlign = 'right'; | |
| ctx.fillText('$'+maxV.toFixed(1), W-2, pad+8); | |
| ctx.fillText('$'+minV.toFixed(1), W-2, H-pad); | |
| } | |
| // ββ Risk Limits βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function setRiskLimits() { | |
| const r = await api('POST', '/api/risk/limits', { | |
| max_order_qty: parseInt(document.getElementById('r-max-qty').value), | |
| max_position: parseInt(document.getElementById('r-max-pos').value), | |
| max_notional_usd: parseFloat(document.getElementById('r-max-notional').value), | |
| max_orders_per_sec: parseInt(document.getElementById('r-max-rate').value), | |
| }); | |
| showToast(r.ok ? 'β Risk limits updated' : 'β Failed', r.ok ? 'green' : 'red'); | |
| } | |
| // ββ Reset βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function resetSim() { | |
| await api('POST', '/api/reset'); | |
| priceHistory = {}; | |
| document.getElementById('trade-log').innerHTML = ''; | |
| showToast('βΊ Simulator reset', 'cyan'); | |
| } | |
| // ββ Right mode ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function setRightMode(mode) { | |
| rightMode = mode; | |
| ['positions','backtest','risk'].forEach(m => { | |
| document.getElementById('view-'+m).style.display = (m === mode) ? 'flex' : 'none'; | |
| }); | |
| document.querySelectorAll('.mode-tab').forEach((t,i) => { | |
| const modes = ['positions','backtest','risk']; | |
| t.className = 'mode-tab' + (modes[i]===mode ? ' active' : ''); | |
| }); | |
| } | |
| // ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function fmtNum(n) { | |
| if (n >= 1e6) return (n/1e6).toFixed(1)+'M'; | |
| if (n >= 1e3) return (n/1e3).toFixed(1)+'K'; | |
| return n.toString(); | |
| } | |
| function showToast(msg, color='cyan') { | |
| const t = document.createElement('div'); | |
| t.className = 'toast'; | |
| t.style.borderColor = `var(--${color})`; | |
| t.style.color = `var(--${color})`; | |
| t.textContent = msg; | |
| document.body.appendChild(t); | |
| setTimeout(() => t.remove(), 3000); | |
| } | |
| // Resize handler for chart | |
| window.addEventListener('resize', () => { | |
| if (priceHistory[selectedSymbol]) drawPriceChart(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |