NOT-OMEGA's picture
Upload 22 files
ba6114e verified
Raw
History Blame Contribute Delete
51.7 kB
<!DOCTYPE html>
<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) !important; 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>