Spaces:
Sleeping
Sleeping
Delete index.html
Browse files- index.html +0 -1278
index.html
DELETED
|
@@ -1,1278 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>HFT Exchange Simulator</title>
|
| 7 |
-
<style>
|
| 8 |
-
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Space+Mono:wght@400;700&display=swap');
|
| 9 |
-
|
| 10 |
-
:root {
|
| 11 |
-
--bg: #080c10;
|
| 12 |
-
--bg1: #0d1219;
|
| 13 |
-
--bg2: #121820;
|
| 14 |
-
--bg3: #1a2230;
|
| 15 |
-
--border: #1e2d3d;
|
| 16 |
-
--border2: #253545;
|
| 17 |
-
--text: #c8d8e8;
|
| 18 |
-
--dim: #5a7a90;
|
| 19 |
-
--green: #00e676;
|
| 20 |
-
--green2: #00c853;
|
| 21 |
-
--red: #ff1744;
|
| 22 |
-
--red2: #d50000;
|
| 23 |
-
--yellow: #ffea00;
|
| 24 |
-
--blue: #40c4ff;
|
| 25 |
-
--purple: #ea80fc;
|
| 26 |
-
--orange: #ff9100;
|
| 27 |
-
--cyan: #18ffff;
|
| 28 |
-
--accent: #00e5ff;
|
| 29 |
-
--glow: 0 0 12px rgba(0,229,255,0.25);
|
| 30 |
-
--glow-g: 0 0 12px rgba(0,230,118,0.3);
|
| 31 |
-
--glow-r: 0 0 12px rgba(255,23,68,0.3);
|
| 32 |
-
--mono: 'JetBrains Mono', 'Space Mono', monospace;
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 36 |
-
|
| 37 |
-
body {
|
| 38 |
-
font-family: var(--mono);
|
| 39 |
-
background: var(--bg);
|
| 40 |
-
color: var(--text);
|
| 41 |
-
min-height: 100vh;
|
| 42 |
-
overflow-x: hidden;
|
| 43 |
-
font-size: 12px;
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
/* Scanline overlay */
|
| 47 |
-
body::before {
|
| 48 |
-
content: '';
|
| 49 |
-
position: fixed; inset: 0;
|
| 50 |
-
background: repeating-linear-gradient(0deg,
|
| 51 |
-
transparent, transparent 2px,
|
| 52 |
-
rgba(0,0,0,0.03) 2px, rgba(0,0,0,0.03) 4px);
|
| 53 |
-
pointer-events: none; z-index: 9999;
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
/* Header */
|
| 57 |
-
.header {
|
| 58 |
-
display: flex; align-items: center; justify-content: space-between;
|
| 59 |
-
padding: 10px 20px;
|
| 60 |
-
background: linear-gradient(90deg, #050a10 0%, #0d1825 50%, #050a10 100%);
|
| 61 |
-
border-bottom: 1px solid var(--accent);
|
| 62 |
-
box-shadow: 0 0 30px rgba(0,229,255,0.1);
|
| 63 |
-
position: sticky; top: 0; z-index: 100;
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
.logo {
|
| 67 |
-
font-size: 15px; font-weight: 700; letter-spacing: 3px;
|
| 68 |
-
color: var(--accent);
|
| 69 |
-
text-shadow: 0 0 20px rgba(0,229,255,0.6);
|
| 70 |
-
}
|
| 71 |
-
.logo span { color: var(--green); }
|
| 72 |
-
|
| 73 |
-
.header-stats {
|
| 74 |
-
display: flex; gap: 24px;
|
| 75 |
-
}
|
| 76 |
-
.hstat { text-align: center; }
|
| 77 |
-
.hstat-label { font-size: 9px; color: var(--dim); text-transform: uppercase; letter-spacing: 1px; }
|
| 78 |
-
.hstat-val { font-size: 13px; font-weight: 600; }
|
| 79 |
-
|
| 80 |
-
.conn-dot {
|
| 81 |
-
width: 8px; height: 8px; border-radius: 50%;
|
| 82 |
-
background: var(--green);
|
| 83 |
-
box-shadow: 0 0 8px var(--green);
|
| 84 |
-
animation: pulse 2s ease-in-out infinite;
|
| 85 |
-
}
|
| 86 |
-
@keyframes pulse {
|
| 87 |
-
0%,100% { opacity: 1; } 50% { opacity: 0.4; }
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
/* Main grid */
|
| 91 |
-
.grid {
|
| 92 |
-
display: grid;
|
| 93 |
-
grid-template-columns: 320px 1fr 300px;
|
| 94 |
-
grid-template-rows: auto auto auto;
|
| 95 |
-
gap: 1px;
|
| 96 |
-
background: var(--border);
|
| 97 |
-
height: calc(100vh - 48px);
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
.panel {
|
| 101 |
-
background: var(--bg1);
|
| 102 |
-
overflow: hidden;
|
| 103 |
-
display: flex; flex-direction: column;
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
.panel-title {
|
| 107 |
-
font-size: 9px; font-weight: 700;
|
| 108 |
-
text-transform: uppercase; letter-spacing: 2px;
|
| 109 |
-
color: var(--dim);
|
| 110 |
-
padding: 8px 14px;
|
| 111 |
-
border-bottom: 1px solid var(--border);
|
| 112 |
-
display: flex; align-items: center; justify-content: space-between;
|
| 113 |
-
background: var(--bg2);
|
| 114 |
-
flex-shrink: 0;
|
| 115 |
-
}
|
| 116 |
-
.panel-title .tag {
|
| 117 |
-
font-size: 8px; padding: 2px 6px;
|
| 118 |
-
background: var(--bg3); border-radius: 2px;
|
| 119 |
-
color: var(--accent); letter-spacing: 1px;
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
.panel-body { flex: 1; overflow-y: auto; padding: 12px; }
|
| 123 |
-
.panel-body::-webkit-scrollbar { width: 3px; }
|
| 124 |
-
.panel-body::-webkit-scrollbar-track { background: var(--bg1); }
|
| 125 |
-
.panel-body::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; }
|
| 126 |
-
|
| 127 |
-
/* ORDER BOOK */
|
| 128 |
-
.book-header {
|
| 129 |
-
display: grid; grid-template-columns: 1fr 80px 80px;
|
| 130 |
-
font-size: 9px; color: var(--dim); letter-spacing: 1px;
|
| 131 |
-
padding: 4px 12px; border-bottom: 1px solid var(--border);
|
| 132 |
-
text-transform: uppercase;
|
| 133 |
-
}
|
| 134 |
-
|
| 135 |
-
.book-row {
|
| 136 |
-
display: grid; grid-template-columns: 1fr 80px 80px;
|
| 137 |
-
padding: 3px 12px;
|
| 138 |
-
position: relative; cursor: default;
|
| 139 |
-
transition: background 0.1s;
|
| 140 |
-
}
|
| 141 |
-
.book-row:hover { background: rgba(255,255,255,0.03); }
|
| 142 |
-
|
| 143 |
-
.book-bar {
|
| 144 |
-
position: absolute; top: 0; bottom: 0;
|
| 145 |
-
opacity: 0.12; pointer-events: none;
|
| 146 |
-
transition: width 0.3s ease;
|
| 147 |
-
}
|
| 148 |
-
|
| 149 |
-
.ask-row .book-bar { background: var(--red); right: 0; }
|
| 150 |
-
.bid-row .book-bar { background: var(--green); left: 0; }
|
| 151 |
-
|
| 152 |
-
.ask-row .price { color: var(--red); font-weight: 600; }
|
| 153 |
-
.bid-row .price { color: var(--green); font-weight: 600; }
|
| 154 |
-
.qty-val { text-align: right; color: var(--text); }
|
| 155 |
-
.orders-val { text-align: right; color: var(--dim); }
|
| 156 |
-
|
| 157 |
-
.spread-row {
|
| 158 |
-
display: flex; align-items: center; justify-content: center;
|
| 159 |
-
padding: 4px; font-size: 9px; color: var(--yellow);
|
| 160 |
-
border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
|
| 161 |
-
background: var(--bg2);
|
| 162 |
-
letter-spacing: 2px;
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
/* ORDER ENTRY */
|
| 166 |
-
.tabs {
|
| 167 |
-
display: flex; border-bottom: 1px solid var(--border);
|
| 168 |
-
flex-shrink: 0;
|
| 169 |
-
}
|
| 170 |
-
.tab {
|
| 171 |
-
flex: 1; padding: 8px; font-family: var(--mono); font-size: 9px;
|
| 172 |
-
font-weight: 700; letter-spacing: 1px; text-transform: uppercase;
|
| 173 |
-
border: none; cursor: pointer;
|
| 174 |
-
transition: all 0.2s;
|
| 175 |
-
background: var(--bg2); color: var(--dim);
|
| 176 |
-
}
|
| 177 |
-
.tab.active-buy { background: rgba(0,230,118,0.1); color: var(--green); border-bottom: 2px solid var(--green); }
|
| 178 |
-
.tab.active-sell { background: rgba(255,23,68,0.1); color: var(--red); border-bottom: 2px solid var(--red); }
|
| 179 |
-
.tab:hover { background: var(--bg3); }
|
| 180 |
-
|
| 181 |
-
.form-group { margin-bottom: 10px; }
|
| 182 |
-
label { font-size: 9px; color: var(--dim); text-transform: uppercase; letter-spacing: 1px; display: block; margin-bottom: 4px; }
|
| 183 |
-
|
| 184 |
-
input, select {
|
| 185 |
-
width: 100%; padding: 8px 10px;
|
| 186 |
-
background: var(--bg3); border: 1px solid var(--border2);
|
| 187 |
-
color: var(--text); font-family: var(--mono); font-size: 12px;
|
| 188 |
-
border-radius: 2px; outline: none;
|
| 189 |
-
transition: border-color 0.2s, box-shadow 0.2s;
|
| 190 |
-
}
|
| 191 |
-
input:focus, select:focus {
|
| 192 |
-
border-color: var(--accent);
|
| 193 |
-
box-shadow: var(--glow);
|
| 194 |
-
}
|
| 195 |
-
select option { background: var(--bg2); }
|
| 196 |
-
|
| 197 |
-
.btn {
|
| 198 |
-
width: 100%; padding: 10px;
|
| 199 |
-
font-family: var(--mono); font-size: 11px; font-weight: 700;
|
| 200 |
-
letter-spacing: 2px; text-transform: uppercase;
|
| 201 |
-
border: none; border-radius: 2px; cursor: pointer;
|
| 202 |
-
transition: all 0.2s; margin-top: 4px;
|
| 203 |
-
}
|
| 204 |
-
.btn-buy {
|
| 205 |
-
background: linear-gradient(135deg, #00c853, #00e676);
|
| 206 |
-
color: #001a0a;
|
| 207 |
-
box-shadow: 0 0 15px rgba(0,230,118,0.3);
|
| 208 |
-
}
|
| 209 |
-
.btn-buy:hover { box-shadow: 0 0 25px rgba(0,230,118,0.5); transform: translateY(-1px); }
|
| 210 |
-
.btn-sell {
|
| 211 |
-
background: linear-gradient(135deg, #d50000, #ff1744);
|
| 212 |
-
color: #1a0000;
|
| 213 |
-
box-shadow: 0 0 15px rgba(255,23,68,0.3);
|
| 214 |
-
}
|
| 215 |
-
.btn-sell:hover { box-shadow: 0 0 25px rgba(255,23,68,0.5); transform: translateY(-1px); }
|
| 216 |
-
.btn-neutral {
|
| 217 |
-
background: var(--bg3); color: var(--text);
|
| 218 |
-
border: 1px solid var(--border2);
|
| 219 |
-
}
|
| 220 |
-
.btn-neutral:hover { border-color: var(--accent); color: var(--accent); }
|
| 221 |
-
.btn-accent {
|
| 222 |
-
background: linear-gradient(135deg, #006080, #00acc1);
|
| 223 |
-
color: #001a20;
|
| 224 |
-
box-shadow: 0 0 15px rgba(0,229,255,0.2);
|
| 225 |
-
}
|
| 226 |
-
.btn-accent:hover { box-shadow: 0 0 25px rgba(0,229,255,0.4); transform: translateY(-1px); }
|
| 227 |
-
|
| 228 |
-
/* PRICE CHART */
|
| 229 |
-
canvas { display: block; }
|
| 230 |
-
#price-canvas { width: 100%; }
|
| 231 |
-
|
| 232 |
-
/* LATENCY / STATS */
|
| 233 |
-
.stat-row {
|
| 234 |
-
display: flex; justify-content: space-between;
|
| 235 |
-
padding: 5px 0; border-bottom: 1px solid rgba(30,45,61,0.5);
|
| 236 |
-
}
|
| 237 |
-
.stat-key { color: var(--dim); font-size: 10px; }
|
| 238 |
-
.stat-val { font-weight: 600; font-size: 11px; }
|
| 239 |
-
.green { color: var(--green); }
|
| 240 |
-
.red { color: var(--red); }
|
| 241 |
-
.yellow { color: var(--yellow); }
|
| 242 |
-
.blue { color: var(--blue); }
|
| 243 |
-
.cyan { color: var(--cyan); }
|
| 244 |
-
|
| 245 |
-
/* TRADE LOG */
|
| 246 |
-
.trade-entry {
|
| 247 |
-
display: flex; gap: 8px; padding: 5px 0;
|
| 248 |
-
border-bottom: 1px solid rgba(30,45,61,0.4);
|
| 249 |
-
font-size: 10px; align-items: center;
|
| 250 |
-
animation: fadeIn 0.3s ease;
|
| 251 |
-
}
|
| 252 |
-
@keyframes fadeIn { from { opacity: 0; transform: translateX(-5px); } to { opacity: 1; transform: translateX(0); } }
|
| 253 |
-
|
| 254 |
-
.trade-side {
|
| 255 |
-
padding: 2px 5px; font-size: 8px; font-weight: 700;
|
| 256 |
-
letter-spacing: 1px; border-radius: 2px;
|
| 257 |
-
min-width: 30px; text-align: center;
|
| 258 |
-
}
|
| 259 |
-
.buy-badge { background: rgba(0,230,118,0.15); color: var(--green); }
|
| 260 |
-
.sell-badge { background: rgba(255,23,68,0.15); color: var(--red); }
|
| 261 |
-
|
| 262 |
-
/* POSITION TABLE */
|
| 263 |
-
.pos-table { width: 100%; border-collapse: collapse; font-size: 10px; }
|
| 264 |
-
.pos-table th {
|
| 265 |
-
text-align: left; padding: 4px 6px;
|
| 266 |
-
color: var(--dim); font-size: 8px; text-transform: uppercase; letter-spacing: 1px;
|
| 267 |
-
border-bottom: 1px solid var(--border);
|
| 268 |
-
}
|
| 269 |
-
.pos-table td { padding: 5px 6px; border-bottom: 1px solid rgba(30,45,61,0.3); }
|
| 270 |
-
|
| 271 |
-
/* BACKTEST PANEL */
|
| 272 |
-
.bt-result {
|
| 273 |
-
background: var(--bg2); border: 1px solid var(--border2);
|
| 274 |
-
border-radius: 3px; padding: 10px; margin-top: 10px;
|
| 275 |
-
}
|
| 276 |
-
.bt-metric {
|
| 277 |
-
display: flex; justify-content: space-between;
|
| 278 |
-
padding: 3px 0; font-size: 10px;
|
| 279 |
-
}
|
| 280 |
-
|
| 281 |
-
/* EQUITY CURVE */
|
| 282 |
-
#equity-canvas { width: 100%; border-top: 1px solid var(--border); }
|
| 283 |
-
|
| 284 |
-
/* LATENCY BARS */
|
| 285 |
-
.lat-bar-wrap { margin: 4px 0; }
|
| 286 |
-
.lat-bar-label { display: flex; justify-content: space-between; font-size: 9px; margin-bottom: 2px; }
|
| 287 |
-
.lat-bar { height: 6px; background: var(--bg3); border-radius: 3px; overflow: hidden; }
|
| 288 |
-
.lat-bar-fill { height: 100%; border-radius: 3px; transition: width 0.5s ease; }
|
| 289 |
-
|
| 290 |
-
/* RISK PANEL */
|
| 291 |
-
.risk-meter {
|
| 292 |
-
height: 8px; background: var(--bg3); border-radius: 4px; overflow: hidden;
|
| 293 |
-
margin: 4px 0 10px;
|
| 294 |
-
}
|
| 295 |
-
.risk-fill {
|
| 296 |
-
height: 100%; border-radius: 4px; transition: width 0.5s ease;
|
| 297 |
-
}
|
| 298 |
-
|
| 299 |
-
/* FEED CONTROL */
|
| 300 |
-
.feed-control {
|
| 301 |
-
display: flex; gap: 6px; padding: 8px 12px;
|
| 302 |
-
border-top: 1px solid var(--border);
|
| 303 |
-
flex-shrink: 0;
|
| 304 |
-
}
|
| 305 |
-
.feed-control .btn { margin: 0; flex: 1; padding: 7px; font-size: 9px; }
|
| 306 |
-
|
| 307 |
-
/* Symbol selector strip */
|
| 308 |
-
.sym-strip {
|
| 309 |
-
display: flex; gap: 4px; padding: 6px 12px;
|
| 310 |
-
border-bottom: 1px solid var(--border);
|
| 311 |
-
overflow-x: auto; flex-shrink: 0;
|
| 312 |
-
}
|
| 313 |
-
.sym-btn {
|
| 314 |
-
padding: 3px 10px; font-family: var(--mono); font-size: 9px;
|
| 315 |
-
font-weight: 700; letter-spacing: 1px; text-transform: uppercase;
|
| 316 |
-
border: 1px solid var(--border2); border-radius: 2px;
|
| 317 |
-
cursor: pointer; background: var(--bg3); color: var(--dim);
|
| 318 |
-
transition: all 0.15s; white-space: nowrap;
|
| 319 |
-
}
|
| 320 |
-
.sym-btn.active, .sym-btn:hover {
|
| 321 |
-
border-color: var(--accent); color: var(--accent);
|
| 322 |
-
background: rgba(0,229,255,0.08);
|
| 323 |
-
}
|
| 324 |
-
|
| 325 |
-
/* Notification toast */
|
| 326 |
-
.toast {
|
| 327 |
-
position: fixed; top: 60px; right: 20px;
|
| 328 |
-
background: var(--bg2); border: 1px solid var(--accent);
|
| 329 |
-
padding: 10px 16px; border-radius: 3px;
|
| 330 |
-
font-size: 11px; z-index: 1000;
|
| 331 |
-
animation: toastIn 0.3s ease, toastOut 0.3s ease 2.7s forwards;
|
| 332 |
-
box-shadow: var(--glow);
|
| 333 |
-
}
|
| 334 |
-
@keyframes toastIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } }
|
| 335 |
-
@keyframes toastOut { to { opacity: 0; transform: translateX(20px); } }
|
| 336 |
-
|
| 337 |
-
/* Scrollable sections */
|
| 338 |
-
.scroll-area { flex: 1; overflow-y: auto; }
|
| 339 |
-
.scroll-area::-webkit-scrollbar { width: 3px; }
|
| 340 |
-
.scroll-area::-webkit-scrollbar-thumb { background: var(--border2); }
|
| 341 |
-
|
| 342 |
-
/* Responsive col spans */
|
| 343 |
-
.span2 { grid-column: span 2; }
|
| 344 |
-
.col-left { grid-column: 1; grid-row: 1 / span 3; }
|
| 345 |
-
.col-mid { grid-column: 2; }
|
| 346 |
-
.col-right { grid-column: 3; grid-row: 1 / span 3; }
|
| 347 |
-
|
| 348 |
-
.grid-inner {
|
| 349 |
-
display: grid;
|
| 350 |
-
grid-template-columns: 320px 1fr 300px;
|
| 351 |
-
grid-template-rows: 360px 260px 1fr;
|
| 352 |
-
gap: 1px;
|
| 353 |
-
background: var(--border);
|
| 354 |
-
height: calc(100vh - 48px);
|
| 355 |
-
}
|
| 356 |
-
|
| 357 |
-
/* Alert for rejected orders */
|
| 358 |
-
.rejected { border-left: 3px solid var(--red) !important; animation: shake 0.3s; }
|
| 359 |
-
@keyframes shake {
|
| 360 |
-
0%,100% { transform: translateX(0); }
|
| 361 |
-
25% { transform: translateX(-4px); }
|
| 362 |
-
75% { transform: translateX(4px); }
|
| 363 |
-
}
|
| 364 |
-
|
| 365 |
-
/* Blinking cursor */
|
| 366 |
-
.cursor::after { content: '█'; animation: blink 1s step-end infinite; color: var(--accent); }
|
| 367 |
-
@keyframes blink { 50% { opacity: 0; } }
|
| 368 |
-
|
| 369 |
-
/* Number animations */
|
| 370 |
-
.num-up { animation: numUp 0.4s ease; }
|
| 371 |
-
.num-down { animation: numDown 0.4s ease; }
|
| 372 |
-
@keyframes numUp { 0% { color: var(--green); } 100% { color: inherit; } }
|
| 373 |
-
@keyframes numDown { 0% { color: var(--red); } 100% { color: inherit; } }
|
| 374 |
-
|
| 375 |
-
/* Progress ring for latency */
|
| 376 |
-
.ring-wrap { position: relative; display: inline-flex; align-items: center; justify-content: center; }
|
| 377 |
-
|
| 378 |
-
/* Hover row highlight for order book */
|
| 379 |
-
.book-asks .book-row:hover .price { text-shadow: 0 0 8px var(--red); }
|
| 380 |
-
.book-bids .book-row:hover .price { text-shadow: 0 0 8px var(--green); }
|
| 381 |
-
|
| 382 |
-
/* Section divider */
|
| 383 |
-
.divider { height: 1px; background: var(--border); margin: 10px 0; }
|
| 384 |
-
|
| 385 |
-
/* Blinking red for reject */
|
| 386 |
-
.flash-red { animation: flashRed 0.5s; }
|
| 387 |
-
@keyframes flashRed { 0%,100% { background: var(--bg1); } 50% { background: rgba(255,23,68,0.15); } }
|
| 388 |
-
|
| 389 |
-
/* Toggle switch */
|
| 390 |
-
.toggle { position: relative; display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
|
| 391 |
-
.toggle input { display: none; }
|
| 392 |
-
.toggle-track {
|
| 393 |
-
width: 32px; height: 16px; background: var(--bg3);
|
| 394 |
-
border: 1px solid var(--border2); border-radius: 8px;
|
| 395 |
-
transition: background 0.2s;
|
| 396 |
-
position: relative;
|
| 397 |
-
}
|
| 398 |
-
.toggle input:checked + .toggle-track { background: var(--green2); border-color: var(--green); }
|
| 399 |
-
.toggle-thumb {
|
| 400 |
-
position: absolute; top: 1px; left: 1px;
|
| 401 |
-
width: 12px; height: 12px; border-radius: 6px;
|
| 402 |
-
background: var(--dim); transition: transform 0.2s, background 0.2s;
|
| 403 |
-
}
|
| 404 |
-
.toggle input:checked ~ .toggle-thumb {
|
| 405 |
-
transform: translateX(16px); background: white;
|
| 406 |
-
}
|
| 407 |
-
|
| 408 |
-
.mode-tabs { display: flex; gap: 1px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
| 409 |
-
.mode-tab {
|
| 410 |
-
flex: 1; padding: 6px; font-family: var(--mono); font-size: 9px; font-weight: 700;
|
| 411 |
-
letter-spacing: 1px; text-transform: uppercase; border: none; cursor: pointer;
|
| 412 |
-
background: var(--bg2); color: var(--dim); transition: all 0.15s;
|
| 413 |
-
}
|
| 414 |
-
.mode-tab.active { background: var(--bg3); color: var(--accent); border-bottom: 1px solid var(--accent); }
|
| 415 |
-
</style>
|
| 416 |
-
</head>
|
| 417 |
-
<body>
|
| 418 |
-
|
| 419 |
-
<!-- HEADER -->
|
| 420 |
-
<div class="header">
|
| 421 |
-
<div style="display:flex;align-items:center;gap:16px">
|
| 422 |
-
<div class="conn-dot" id="conn-dot"></div>
|
| 423 |
-
<div class="logo">HFT<span>·</span>EXCHANGE<span>·</span>SIM</div>
|
| 424 |
-
<div style="font-size:9px;color:var(--dim);letter-spacing:1px">C++ MATCHING ENGINE</div>
|
| 425 |
-
</div>
|
| 426 |
-
<div class="header-stats">
|
| 427 |
-
<div class="hstat"><div class="hstat-label">Orders</div><div class="hstat-val cyan" id="h-orders">0</div></div>
|
| 428 |
-
<div class="hstat"><div class="hstat-label">Trades</div><div class="hstat-val green" id="h-trades">0</div></div>
|
| 429 |
-
<div class="hstat"><div class="hstat-label">Rejects</div><div class="hstat-val red" id="h-rejects">0</div></div>
|
| 430 |
-
<div class="hstat"><div class="hstat-label">P&L</div><div class="hstat-val" id="h-pnl">$0.00</div></div>
|
| 431 |
-
<div class="hstat"><div class="hstat-label">p99 Latency</div><div class="hstat-val yellow" id="h-p99">—</div></div>
|
| 432 |
-
<div class="hstat"><div class="hstat-label">Throughput</div><div class="hstat-val blue" id="h-thru">—</div></div>
|
| 433 |
-
</div>
|
| 434 |
-
</div>
|
| 435 |
-
|
| 436 |
-
<!-- MAIN GRID -->
|
| 437 |
-
<div class="grid-inner">
|
| 438 |
-
|
| 439 |
-
<!-- COL 1: Order Book + Order Entry -->
|
| 440 |
-
<div class="panel" style="grid-column:1;grid-row:1/span 3;display:flex;flex-direction:column">
|
| 441 |
-
|
| 442 |
-
<!-- Symbol strip -->
|
| 443 |
-
<div class="sym-strip" id="sym-strip"></div>
|
| 444 |
-
|
| 445 |
-
<!-- Order Book -->
|
| 446 |
-
<div class="panel-title">
|
| 447 |
-
Order Book — L2 <span id="book-symbol-tag" class="tag">AAPL</span>
|
| 448 |
-
</div>
|
| 449 |
-
<div class="book-header">
|
| 450 |
-
<span>PRICE</span><span style="text-align:right">QTY</span><span style="text-align:right">ODS</span>
|
| 451 |
-
</div>
|
| 452 |
-
|
| 453 |
-
<div style="flex:1;overflow-y:auto;display:flex;flex-direction:column">
|
| 454 |
-
<div class="book-asks scroll-area" id="book-asks" style="flex:1;display:flex;flex-direction:column;justify-content:flex-end"></div>
|
| 455 |
-
<div class="spread-row" id="spread-row">SPREAD: —</div>
|
| 456 |
-
<div class="book-bids scroll-area" id="book-bids" style="flex:1"></div>
|
| 457 |
-
</div>
|
| 458 |
-
|
| 459 |
-
<!-- Order Entry -->
|
| 460 |
-
<div class="divider" style="margin:0"></div>
|
| 461 |
-
<div class="panel-title">Order Entry</div>
|
| 462 |
-
|
| 463 |
-
<div class="tabs">
|
| 464 |
-
<button class="tab active-buy" id="tab-buy" onclick="setOrderSide('BUY')">▲ BUY</button>
|
| 465 |
-
<button class="tab" id="tab-sell" onclick="setOrderSide('SELL')">▼ SELL</button>
|
| 466 |
-
</div>
|
| 467 |
-
|
| 468 |
-
<div style="padding:12px;flex-shrink:0">
|
| 469 |
-
<div class="form-group">
|
| 470 |
-
<label>Type</label>
|
| 471 |
-
<select id="oe-type" onchange="updateOrderForm()">
|
| 472 |
-
<option value="LIMIT">LIMIT</option>
|
| 473 |
-
<option value="MARKET">MARKET</option>
|
| 474 |
-
<option value="IOC">IOC (Immediate-Or-Cancel)</option>
|
| 475 |
-
<option value="FOK">FOK (Fill-Or-Kill)</option>
|
| 476 |
-
</select>
|
| 477 |
-
</div>
|
| 478 |
-
<div class="form-group">
|
| 479 |
-
<label>Symbol</label>
|
| 480 |
-
<select id="oe-symbol">
|
| 481 |
-
<option>AAPL</option><option>GOOG</option><option>MSFT</option>
|
| 482 |
-
<option>TSLA</option><option>NVDA</option>
|
| 483 |
-
</select>
|
| 484 |
-
</div>
|
| 485 |
-
<div class="form-group" id="oe-price-group">
|
| 486 |
-
<label>Price</label>
|
| 487 |
-
<input type="number" id="oe-price" step="0.01" placeholder="0.00">
|
| 488 |
-
</div>
|
| 489 |
-
<div class="form-group">
|
| 490 |
-
<label>Quantity</label>
|
| 491 |
-
<input type="number" id="oe-qty" value="100" min="1">
|
| 492 |
-
</div>
|
| 493 |
-
<div class="form-group" style="display:none">
|
| 494 |
-
<label>Client ID (optional)</label>
|
| 495 |
-
<input type="text" id="oe-cid" placeholder="STRAT_01">
|
| 496 |
-
</div>
|
| 497 |
-
<button class="btn btn-buy" id="submit-btn" onclick="submitOrder()">▲ PLACE BUY ORDER</button>
|
| 498 |
-
</div>
|
| 499 |
-
|
| 500 |
-
<div class="feed-control">
|
| 501 |
-
<button class="btn btn-neutral" onclick="feedStep(10)">▶ +10 TICKS</button>
|
| 502 |
-
<button class="btn btn-neutral" onclick="feedStep(100)">▶▶ +100</button>
|
| 503 |
-
<button class="btn btn-accent" onclick="autoFeed()" id="auto-btn">AUTO</button>
|
| 504 |
-
</div>
|
| 505 |
-
</div>
|
| 506 |
-
|
| 507 |
-
<!-- COL 2, ROW 1: Price Chart -->
|
| 508 |
-
<div class="panel" style="grid-column:2;grid-row:1">
|
| 509 |
-
<div class="panel-title">
|
| 510 |
-
Price Feed — Real-time Tick
|
| 511 |
-
<div style="display:flex;gap:10px;align-items:center">
|
| 512 |
-
<span id="chart-symbol" class="tag">AAPL</span>
|
| 513 |
-
<span id="chart-price" style="font-size:13px;font-weight:700;color:var(--accent)">—</span>
|
| 514 |
-
</div>
|
| 515 |
-
</div>
|
| 516 |
-
<canvas id="price-canvas" height="290"></canvas>
|
| 517 |
-
</div>
|
| 518 |
-
|
| 519 |
-
<!-- COL 2, ROW 2: Latency / Throughput -->
|
| 520 |
-
<div class="panel" style="grid-column:2;grid-row:2">
|
| 521 |
-
<div class="panel-title">Latency Profiler <span class="tag">C++ ENGINE</span></div>
|
| 522 |
-
<div class="panel-body" style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
| 523 |
-
<div>
|
| 524 |
-
<div style="font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Order Latency</div>
|
| 525 |
-
<div class="lat-bar-wrap">
|
| 526 |
-
<div class="lat-bar-label"><span style="color:var(--dim)">p50</span><span id="lat-o-p50" style="color:var(--green)">—</span></div>
|
| 527 |
-
<div class="lat-bar"><div class="lat-bar-fill" id="lat-o-p50-bar" style="background:var(--green);width:0%"></div></div>
|
| 528 |
-
</div>
|
| 529 |
-
<div class="lat-bar-wrap">
|
| 530 |
-
<div class="lat-bar-label"><span style="color:var(--dim)">p99</span><span id="lat-o-p99" style="color:var(--yellow)">—</span></div>
|
| 531 |
-
<div class="lat-bar"><div class="lat-bar-fill" id="lat-o-p99-bar" style="background:var(--yellow);width:0%"></div></div>
|
| 532 |
-
</div>
|
| 533 |
-
<div class="lat-bar-wrap">
|
| 534 |
-
<div class="lat-bar-label"><span style="color:var(--dim)">avg</span><span id="lat-o-avg" style="color:var(--blue)">—</span></div>
|
| 535 |
-
<div class="lat-bar"><div class="lat-bar-fill" id="lat-o-avg-bar" style="background:var(--blue);width:0%"></div></div>
|
| 536 |
-
</div>
|
| 537 |
-
<div class="divider"></div>
|
| 538 |
-
<div class="stat-row"><span class="stat-key">Checked</span><span id="risk-checked" class="stat-val cyan">0</span></div>
|
| 539 |
-
<div class="stat-row"><span class="stat-key">Rejected</span><span id="risk-rejected" class="stat-val red">0</span></div>
|
| 540 |
-
<div class="stat-row"><span class="stat-key">Fill Rate</span><span id="fill-rate" class="stat-val green">0%</span></div>
|
| 541 |
-
</div>
|
| 542 |
-
<div>
|
| 543 |
-
<div style="font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Tick Latency</div>
|
| 544 |
-
<div class="lat-bar-wrap">
|
| 545 |
-
<div class="lat-bar-label"><span style="color:var(--dim)">p50</span><span id="lat-t-p50" style="color:var(--green)">—</span></div>
|
| 546 |
-
<div class="lat-bar"><div class="lat-bar-fill" id="lat-t-p50-bar" style="background:var(--green);width:0%"></div></div>
|
| 547 |
-
</div>
|
| 548 |
-
<div class="lat-bar-wrap">
|
| 549 |
-
<div class="lat-bar-label"><span style="color:var(--dim)">p99</span><span id="lat-t-p99" style="color:var(--yellow)">—</span></div>
|
| 550 |
-
<div class="lat-bar"><div class="lat-bar-fill" id="lat-t-p99-bar" style="background:var(--yellow);width:0%"></div></div>
|
| 551 |
-
</div>
|
| 552 |
-
<div class="divider"></div>
|
| 553 |
-
<div class="stat-row"><span class="stat-key">Feed Size</span><span id="feed-size" class="stat-val">—</span></div>
|
| 554 |
-
<div class="stat-row"><span class="stat-key">Feed Index</span><span id="feed-idx" class="stat-val">—</span></div>
|
| 555 |
-
<div class="stat-row"><span class="stat-key">Total P&L</span><span id="stat-pnl" class="stat-val">—</span></div>
|
| 556 |
-
</div>
|
| 557 |
-
</div>
|
| 558 |
-
</div>
|
| 559 |
-
|
| 560 |
-
<!-- COL 2, ROW 3: Trade Log -->
|
| 561 |
-
<div class="panel" style="grid-column:2;grid-row:3">
|
| 562 |
-
<div class="panel-title">
|
| 563 |
-
Trade Log
|
| 564 |
-
<div style="display:flex;gap:8px">
|
| 565 |
-
<span id="trade-count" class="tag">0 trades</span>
|
| 566 |
-
<button class="btn btn-neutral" style="padding:2px 8px;font-size:8px;margin:0;width:auto" onclick="resetSim()">RESET</button>
|
| 567 |
-
</div>
|
| 568 |
-
</div>
|
| 569 |
-
<div class="scroll-area" style="flex:1" id="trade-log"></div>
|
| 570 |
-
</div>
|
| 571 |
-
|
| 572 |
-
<!-- COL 3: Positions + Backtest + Risk -->
|
| 573 |
-
<div class="panel" style="grid-column:3;grid-row:1/span 3;display:flex;flex-direction:column">
|
| 574 |
-
|
| 575 |
-
<div class="mode-tabs">
|
| 576 |
-
<button class="mode-tab active" onclick="setRightMode('positions')">POSITIONS</button>
|
| 577 |
-
<button class="mode-tab" onclick="setRightMode('backtest')">BACKTEST</button>
|
| 578 |
-
<button class="mode-tab" onclick="setRightMode('risk')">RISK</button>
|
| 579 |
-
</div>
|
| 580 |
-
|
| 581 |
-
<!-- POSITIONS VIEW -->
|
| 582 |
-
<div id="view-positions" style="flex:1;overflow:hidden;display:flex;flex-direction:column">
|
| 583 |
-
<div class="panel-title">Live Positions</div>
|
| 584 |
-
<div class="panel-body" style="overflow:auto">
|
| 585 |
-
<table class="pos-table" id="pos-table">
|
| 586 |
-
<thead><tr>
|
| 587 |
-
<th>SYM</th><th>POS</th><th>AVG PX</th><th>RPNL</th><th>UPNL</th>
|
| 588 |
-
</tr></thead>
|
| 589 |
-
<tbody id="pos-body"></tbody>
|
| 590 |
-
</table>
|
| 591 |
-
<div class="divider"></div>
|
| 592 |
-
<div style="font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px">Recent Orders</div>
|
| 593 |
-
<div id="order-log" style="max-height:200px;overflow-y:auto"></div>
|
| 594 |
-
</div>
|
| 595 |
-
</div>
|
| 596 |
-
|
| 597 |
-
<!-- BACKTEST VIEW -->
|
| 598 |
-
<div id="view-backtest" style="flex:1;overflow:hidden;display:flex;flex-direction:column;display:none">
|
| 599 |
-
<div class="panel-title">Backtest Engine <span class="tag">DETERMINISTIC</span></div>
|
| 600 |
-
<div class="panel-body" style="overflow:auto">
|
| 601 |
-
<div class="form-group">
|
| 602 |
-
<label>Strategy</label>
|
| 603 |
-
<select id="bt-strategy">
|
| 604 |
-
<option value="MarketMaking">Market Making (MM)</option>
|
| 605 |
-
<option value="Momentum">Momentum (EMA Cross)</option>
|
| 606 |
-
<option value="MeanReversion">Mean Reversion (Z-Score)</option>
|
| 607 |
-
</select>
|
| 608 |
-
</div>
|
| 609 |
-
<div class="form-group">
|
| 610 |
-
<label>Symbol</label>
|
| 611 |
-
<select id="bt-symbol">
|
| 612 |
-
<option>AAPL</option><option>GOOG</option><option>MSFT</option>
|
| 613 |
-
<option>TSLA</option><option>NVDA</option>
|
| 614 |
-
</select>
|
| 615 |
-
</div>
|
| 616 |
-
<div class="form-group">
|
| 617 |
-
<label>Ticks to Replay</label>
|
| 618 |
-
<input type="number" id="bt-ticks" value="5000" min="200" max="50000">
|
| 619 |
-
</div>
|
| 620 |
-
<div class="form-group">
|
| 621 |
-
<label>Start Price</label>
|
| 622 |
-
<input type="number" id="bt-price" value="185.00" step="0.01">
|
| 623 |
-
</div>
|
| 624 |
-
<button class="btn btn-accent" onclick="runBacktest()" id="bt-btn">▶ RUN BACKTEST</button>
|
| 625 |
-
|
| 626 |
-
<div id="bt-result" style="display:none" class="bt-result">
|
| 627 |
-
<div style="font-size:9px;color:var(--dim);letter-spacing:1px;margin-bottom:8px">BACKTEST RESULTS</div>
|
| 628 |
-
<div class="bt-metric"><span class="stat-key">Strategy</span><span id="bt-r-strategy" class="stat-val cyan"></span></div>
|
| 629 |
-
<div class="bt-metric"><span class="stat-key">Ticks</span><span id="bt-r-ticks" class="stat-val"></span></div>
|
| 630 |
-
<div class="bt-metric"><span class="stat-key">Orders</span><span id="bt-r-orders" class="stat-val"></span></div>
|
| 631 |
-
<div class="bt-metric"><span class="stat-key">Fills</span><span id="bt-r-fills" class="stat-val green"></span></div>
|
| 632 |
-
<div class="bt-metric"><span class="stat-key">Rejected</span><span id="bt-r-rej" class="stat-val red"></span></div>
|
| 633 |
-
<div class="bt-metric"><span class="stat-key">Trades</span><span id="bt-r-trades" class="stat-val"></span></div>
|
| 634 |
-
<div class="bt-metric"><span class="stat-key">Total P&L</span><span id="bt-r-pnl" class="stat-val"></span></div>
|
| 635 |
-
<div class="bt-metric"><span class="stat-key">Max Drawdown</span><span id="bt-r-dd" class="stat-val red"></span></div>
|
| 636 |
-
<div class="bt-metric"><span class="stat-key">Sharpe</span><span id="bt-r-sharpe" class="stat-val yellow"></span></div>
|
| 637 |
-
<div class="bt-metric"><span class="stat-key">Order p50</span><span id="bt-r-p50" class="stat-val green"></span></div>
|
| 638 |
-
<div class="bt-metric"><span class="stat-key">Order p99</span><span id="bt-r-p99" class="stat-val yellow"></span></div>
|
| 639 |
-
<div class="bt-metric"><span class="stat-key">Throughput</span><span id="bt-r-thru" class="stat-val blue"></span></div>
|
| 640 |
-
<canvas id="equity-canvas" height="100" style="margin-top:10px;width:100%;border:1px solid var(--border)"></canvas>
|
| 641 |
-
</div>
|
| 642 |
-
</div>
|
| 643 |
-
</div>
|
| 644 |
-
|
| 645 |
-
<!-- RISK VIEW -->
|
| 646 |
-
<div id="view-risk" style="flex:1;overflow:hidden;display:flex;flex-direction:column;display:none">
|
| 647 |
-
<div class="panel-title">Pre-Trade Risk <span class="tag">LIVE</span></div>
|
| 648 |
-
<div class="panel-body" style="overflow:auto">
|
| 649 |
-
<div class="form-group">
|
| 650 |
-
<label>Max Order Qty</label>
|
| 651 |
-
<input type="number" id="r-max-qty" value="10000">
|
| 652 |
-
</div>
|
| 653 |
-
<div class="form-group">
|
| 654 |
-
<label>Max Position</label>
|
| 655 |
-
<input type="number" id="r-max-pos" value="500000">
|
| 656 |
-
</div>
|
| 657 |
-
<div class="form-group">
|
| 658 |
-
<label>Max Notional ($)</label>
|
| 659 |
-
<input type="number" id="r-max-notional" value="50000000">
|
| 660 |
-
</div>
|
| 661 |
-
<div class="form-group">
|
| 662 |
-
<label>Max Orders/sec</label>
|
| 663 |
-
<input type="number" id="r-max-rate" value="2000">
|
| 664 |
-
</div>
|
| 665 |
-
<button class="btn btn-accent" onclick="setRiskLimits()">UPDATE LIMITS</button>
|
| 666 |
-
|
| 667 |
-
<div class="divider"></div>
|
| 668 |
-
<div style="font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Risk Utilization</div>
|
| 669 |
-
<div class="lat-bar-wrap">
|
| 670 |
-
<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>
|
| 671 |
-
<div class="lat-bar"><div class="lat-bar-fill" id="r-rate-bar" style="width:0%;background:var(--yellow)"></div></div>
|
| 672 |
-
</div>
|
| 673 |
-
<div class="lat-bar-wrap">
|
| 674 |
-
<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>
|
| 675 |
-
<div class="lat-bar"><div class="lat-bar-fill" id="r-rej-bar" style="width:0%;background:var(--red)"></div></div>
|
| 676 |
-
</div>
|
| 677 |
-
<div class="divider"></div>
|
| 678 |
-
<div class="stat-row"><span class="stat-key">Checked</span><span id="r-checked" class="stat-val">0</span></div>
|
| 679 |
-
<div class="stat-row"><span class="stat-key">Approved</span><span id="r-approved" class="stat-val green">0</span></div>
|
| 680 |
-
<div class="stat-row"><span class="stat-key">Rejected</span><span id="r-rejected2" class="stat-val red">0</span></div>
|
| 681 |
-
|
| 682 |
-
<div class="divider"></div>
|
| 683 |
-
<div style="font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Risk Check Types</div>
|
| 684 |
-
<div style="font-size:10px;color:var(--text);line-height:1.8">
|
| 685 |
-
✓ Max Order Size<br>
|
| 686 |
-
✓ Max Position Limit<br>
|
| 687 |
-
✓ Max Notional Exposure<br>
|
| 688 |
-
✓ Max Order Rate (1s window)<br>
|
| 689 |
-
✓ Invalid Price/Qty<br>
|
| 690 |
-
✓ Symbol Validation
|
| 691 |
-
</div>
|
| 692 |
-
</div>
|
| 693 |
-
</div>
|
| 694 |
-
|
| 695 |
-
</div>
|
| 696 |
-
</div>
|
| 697 |
-
|
| 698 |
-
<script>
|
| 699 |
-
// ── State ─────────────────────────────────────────────────────────────────────
|
| 700 |
-
const API = '';
|
| 701 |
-
let selectedSymbol = 'AAPL';
|
| 702 |
-
let orderSide = 'BUY';
|
| 703 |
-
let autoFeedInterval = null;
|
| 704 |
-
let priceHistory = {}; // symbol -> [{ts, price}]
|
| 705 |
-
let rightMode = 'positions';
|
| 706 |
-
|
| 707 |
-
// Price chart
|
| 708 |
-
const priceCtx = document.getElementById('price-canvas');
|
| 709 |
-
let priceChart = null;
|
| 710 |
-
|
| 711 |
-
// ── Init ──────────────────────────────────────────────────────────────────────
|
| 712 |
-
window.addEventListener('DOMContentLoaded', () => {
|
| 713 |
-
buildSymbolStrip();
|
| 714 |
-
selectSymbol('AAPL');
|
| 715 |
-
startPolling();
|
| 716 |
-
feedStep(50); // warm up
|
| 717 |
-
});
|
| 718 |
-
|
| 719 |
-
function buildSymbolStrip() {
|
| 720 |
-
const syms = ['AAPL','GOOG','MSFT','TSLA','NVDA'];
|
| 721 |
-
const strip = document.getElementById('sym-strip');
|
| 722 |
-
strip.innerHTML = syms.map(s =>
|
| 723 |
-
`<button class="sym-btn ${s===selectedSymbol?'active':''}" onclick="selectSymbol('${s}')">${s}</button>`
|
| 724 |
-
).join('');
|
| 725 |
-
}
|
| 726 |
-
|
| 727 |
-
function selectSymbol(sym) {
|
| 728 |
-
selectedSymbol = sym;
|
| 729 |
-
document.getElementById('oe-symbol').value = sym;
|
| 730 |
-
document.getElementById('book-symbol-tag').textContent = sym;
|
| 731 |
-
document.getElementById('chart-symbol').textContent = sym;
|
| 732 |
-
buildSymbolStrip();
|
| 733 |
-
refreshBook();
|
| 734 |
-
refreshChart();
|
| 735 |
-
}
|
| 736 |
-
|
| 737 |
-
// ── API calls ─────────────────────────────────────────────────────────────────
|
| 738 |
-
async function api(method, path, body=null) {
|
| 739 |
-
const opts = { method, headers: {'Content-Type':'application/json'} };
|
| 740 |
-
if (body) opts.body = JSON.stringify(body);
|
| 741 |
-
const r = await fetch(API + path, opts);
|
| 742 |
-
return r.json();
|
| 743 |
-
}
|
| 744 |
-
|
| 745 |
-
// ── Polling ───────────────────────────────────────────────────────────────────
|
| 746 |
-
function startPolling() {
|
| 747 |
-
refreshStatus();
|
| 748 |
-
refreshBook();
|
| 749 |
-
refreshPositions();
|
| 750 |
-
refreshTrades();
|
| 751 |
-
setInterval(refreshStatus, 1500);
|
| 752 |
-
setInterval(refreshBook, 500);
|
| 753 |
-
setInterval(refreshPositions, 2000);
|
| 754 |
-
setInterval(refreshTrades, 1000);
|
| 755 |
-
}
|
| 756 |
-
|
| 757 |
-
async function refreshStatus() {
|
| 758 |
-
const d = await api('GET', '/api/status');
|
| 759 |
-
if (d.error) return;
|
| 760 |
-
|
| 761 |
-
setVal('h-orders', d.total_orders);
|
| 762 |
-
setVal('h-trades', d.total_trades);
|
| 763 |
-
setVal('h-rejects', d.total_rejects);
|
| 764 |
-
|
| 765 |
-
const pnl = d.total_pnl || 0;
|
| 766 |
-
const pnlEl = document.getElementById('h-pnl');
|
| 767 |
-
pnlEl.textContent = '$' + pnl.toFixed(2);
|
| 768 |
-
pnlEl.className = 'hstat-val ' + (pnl >= 0 ? 'green' : 'red');
|
| 769 |
-
|
| 770 |
-
const p99 = d.order_p99_us || 0;
|
| 771 |
-
document.getElementById('h-p99').textContent = p99 > 0 ? p99.toFixed(1)+'µs' : '—';
|
| 772 |
-
|
| 773 |
-
// Latency bars (relative to 1000µs max)
|
| 774 |
-
setLatBar('lat-o-p50', 'lat-o-p50-bar', d.order_p50_us, 500);
|
| 775 |
-
setLatBar('lat-o-p99', 'lat-o-p99-bar', d.order_p99_us, 500);
|
| 776 |
-
setLatBar('lat-t-p50', 'lat-t-p50-bar', d.tick_p50_us, 100);
|
| 777 |
-
setLatBar('lat-t-p99', 'lat-t-p99-bar', d.tick_p99_us, 100);
|
| 778 |
-
|
| 779 |
-
setVal('risk-checked', d.risk_checked);
|
| 780 |
-
setVal('risk-rejected', d.risk_rejected);
|
| 781 |
-
setVal('r-checked', d.risk_checked);
|
| 782 |
-
setVal('r-rejected2', d.risk_rejected);
|
| 783 |
-
setVal('r-approved', Math.max(0, d.risk_checked - d.risk_rejected));
|
| 784 |
-
|
| 785 |
-
const fillRate = d.total_orders > 0
|
| 786 |
-
? ((d.total_trades / d.total_orders) * 100).toFixed(1)+'%'
|
| 787 |
-
: '0%';
|
| 788 |
-
document.getElementById('fill-rate').textContent = fillRate;
|
| 789 |
-
|
| 790 |
-
setVal('feed-size', d.feed_size);
|
| 791 |
-
setVal('feed-idx', d.feed_idx);
|
| 792 |
-
|
| 793 |
-
const pnlEl2 = document.getElementById('stat-pnl');
|
| 794 |
-
pnlEl2.textContent = '$' + pnl.toFixed(2);
|
| 795 |
-
pnlEl2.className = 'stat-val ' + (pnl >= 0 ? 'green' : 'red');
|
| 796 |
-
|
| 797 |
-
// Reject rate bar
|
| 798 |
-
if (d.risk_checked > 0) {
|
| 799 |
-
const rejPct = (d.risk_rejected / d.risk_checked * 100);
|
| 800 |
-
setBarWidth('r-rej-bar', rejPct);
|
| 801 |
-
document.getElementById('r-rej-pct').textContent = rejPct.toFixed(1)+'%';
|
| 802 |
-
}
|
| 803 |
-
|
| 804 |
-
// Connection indicator
|
| 805 |
-
document.getElementById('conn-dot').style.background = 'var(--green)';
|
| 806 |
-
}
|
| 807 |
-
|
| 808 |
-
function setLatBar(valId, barId, val, maxVal) {
|
| 809 |
-
const el = document.getElementById(valId);
|
| 810 |
-
const bar = document.getElementById(barId);
|
| 811 |
-
if (!el || !bar) return;
|
| 812 |
-
if (val === undefined || val === null) return;
|
| 813 |
-
el.textContent = val > 0 ? val.toFixed(2)+'µs' : '< 1µs';
|
| 814 |
-
const pct = Math.min(100, (val / maxVal) * 100);
|
| 815 |
-
bar.style.width = pct + '%';
|
| 816 |
-
}
|
| 817 |
-
|
| 818 |
-
function setBarWidth(id, pct) {
|
| 819 |
-
const el = document.getElementById(id);
|
| 820 |
-
if (el) el.style.width = Math.min(100, pct) + '%';
|
| 821 |
-
}
|
| 822 |
-
|
| 823 |
-
function setVal(id, val) {
|
| 824 |
-
const el = document.getElementById(id);
|
| 825 |
-
if (el) el.textContent = val;
|
| 826 |
-
}
|
| 827 |
-
|
| 828 |
-
// ── Order Book ────────────────────────────────────────────────────────────────
|
| 829 |
-
async function refreshBook() {
|
| 830 |
-
const d = await api('GET', `/api/book/${selectedSymbol}`);
|
| 831 |
-
if (d.error || !d.asks) return;
|
| 832 |
-
renderBook(d);
|
| 833 |
-
}
|
| 834 |
-
|
| 835 |
-
function renderBook(d) {
|
| 836 |
-
const maxQty = Math.max(
|
| 837 |
-
...(d.bids||[]).map(b=>b.qty),
|
| 838 |
-
...(d.asks||[]).map(a=>a.qty), 1
|
| 839 |
-
);
|
| 840 |
-
|
| 841 |
-
// Asks: show in ascending order but display reversed (best ask at bottom)
|
| 842 |
-
const asks = (d.asks||[]).slice(0,10);
|
| 843 |
-
const asksEl = document.getElementById('book-asks');
|
| 844 |
-
asksEl.innerHTML = [...asks].reverse().map(a => {
|
| 845 |
-
const pct = (a.qty / maxQty * 100).toFixed(1);
|
| 846 |
-
return `<div class="book-row ask-row">
|
| 847 |
-
<div class="book-bar" style="width:${pct}%"></div>
|
| 848 |
-
<span class="price">${a.price.toFixed(4)}</span>
|
| 849 |
-
<span class="qty-val">${fmtNum(a.qty)}</span>
|
| 850 |
-
<span class="orders-val">${a.orders}</span>
|
| 851 |
-
</div>`;
|
| 852 |
-
}).join('');
|
| 853 |
-
|
| 854 |
-
const spreadEl = document.getElementById('spread-row');
|
| 855 |
-
if (d.spread > 0) {
|
| 856 |
-
spreadEl.textContent = `BID ${d.best_bid.toFixed(4)} ─── SPREAD ${d.spread.toFixed(4)} ─── ASK ${d.best_ask.toFixed(4)}`;
|
| 857 |
-
}
|
| 858 |
-
|
| 859 |
-
// Update chart price
|
| 860 |
-
const mid = d.mid || d.best_bid;
|
| 861 |
-
if (mid > 0) {
|
| 862 |
-
document.getElementById('chart-price').textContent = '$' + mid.toFixed(2);
|
| 863 |
-
}
|
| 864 |
-
|
| 865 |
-
const bids = (d.bids||[]).slice(0,10);
|
| 866 |
-
const bidsEl = document.getElementById('book-bids');
|
| 867 |
-
bidsEl.innerHTML = bids.map(b => {
|
| 868 |
-
const pct = (b.qty / maxQty * 100).toFixed(1);
|
| 869 |
-
return `<div class="book-row bid-row">
|
| 870 |
-
<div class="book-bar" style="width:${pct}%"></div>
|
| 871 |
-
<span class="price">${b.price.toFixed(4)}</span>
|
| 872 |
-
<span class="qty-val">${fmtNum(b.qty)}</span>
|
| 873 |
-
<span class="orders-val">${b.orders}</span>
|
| 874 |
-
</div>`;
|
| 875 |
-
}).join('');
|
| 876 |
-
}
|
| 877 |
-
|
| 878 |
-
// ── Price Chart ───────────────────────────────────────────────────────────────
|
| 879 |
-
const MAX_HISTORY = 300;
|
| 880 |
-
|
| 881 |
-
async function refreshChart() {
|
| 882 |
-
// Just use feed data from status — we'll seed from order book mid
|
| 883 |
-
}
|
| 884 |
-
|
| 885 |
-
function addPricePoint(sym, price, ts) {
|
| 886 |
-
if (!priceHistory[sym]) priceHistory[sym] = [];
|
| 887 |
-
priceHistory[sym].push({price, ts});
|
| 888 |
-
if (priceHistory[sym].length > MAX_HISTORY)
|
| 889 |
-
priceHistory[sym].shift();
|
| 890 |
-
if (sym === selectedSymbol) drawPriceChart();
|
| 891 |
-
}
|
| 892 |
-
|
| 893 |
-
function drawPriceChart() {
|
| 894 |
-
const canvas = document.getElementById('price-canvas');
|
| 895 |
-
const ctx = canvas.getContext('2d');
|
| 896 |
-
const W = canvas.parentElement.offsetWidth;
|
| 897 |
-
const H = 290;
|
| 898 |
-
canvas.width = W; canvas.height = H;
|
| 899 |
-
|
| 900 |
-
ctx.fillStyle = '#0d1219';
|
| 901 |
-
ctx.fillRect(0, 0, W, H);
|
| 902 |
-
|
| 903 |
-
const data = priceHistory[selectedSymbol] || [];
|
| 904 |
-
if (data.length < 2) {
|
| 905 |
-
ctx.fillStyle = '#1e2d3d';
|
| 906 |
-
ctx.font = '11px JetBrains Mono';
|
| 907 |
-
ctx.textAlign = 'center';
|
| 908 |
-
ctx.fillText('Feed ticks to populate chart', W/2, H/2);
|
| 909 |
-
return;
|
| 910 |
-
}
|
| 911 |
-
|
| 912 |
-
const prices = data.map(d => d.price);
|
| 913 |
-
const minP = Math.min(...prices);
|
| 914 |
-
const maxP = Math.max(...prices);
|
| 915 |
-
const range = maxP - minP || 1;
|
| 916 |
-
const pad = { t:20, r:60, b:30, l:10 };
|
| 917 |
-
const cW = W - pad.l - pad.r;
|
| 918 |
-
const cH = H - pad.t - pad.b;
|
| 919 |
-
|
| 920 |
-
const xOf = i => pad.l + (i / (data.length-1)) * cW;
|
| 921 |
-
const yOf = p => pad.t + (1 - (p - minP) / range) * cH;
|
| 922 |
-
|
| 923 |
-
// Grid lines
|
| 924 |
-
ctx.strokeStyle = '#1a2230';
|
| 925 |
-
ctx.lineWidth = 1;
|
| 926 |
-
for (let i = 0; i <= 4; i++) {
|
| 927 |
-
const y = pad.t + (i / 4) * cH;
|
| 928 |
-
ctx.beginPath(); ctx.moveTo(pad.l, y); ctx.lineTo(W - pad.r, y); ctx.stroke();
|
| 929 |
-
const price = maxP - (i / 4) * range;
|
| 930 |
-
ctx.fillStyle = '#5a7a90'; ctx.font = '9px JetBrains Mono';
|
| 931 |
-
ctx.textAlign = 'right';
|
| 932 |
-
ctx.fillText('$' + price.toFixed(2), W - 5, y + 3);
|
| 933 |
-
}
|
| 934 |
-
|
| 935 |
-
// Gradient fill
|
| 936 |
-
const grad = ctx.createLinearGradient(0, pad.t, 0, H - pad.b);
|
| 937 |
-
const last = data[data.length - 1];
|
| 938 |
-
const first = data[0];
|
| 939 |
-
const isUp = last.price >= first.price;
|
| 940 |
-
if (isUp) {
|
| 941 |
-
grad.addColorStop(0, 'rgba(0,230,118,0.2)');
|
| 942 |
-
grad.addColorStop(1, 'rgba(0,230,118,0.01)');
|
| 943 |
-
} else {
|
| 944 |
-
grad.addColorStop(0, 'rgba(255,23,68,0.2)');
|
| 945 |
-
grad.addColorStop(1, 'rgba(255,23,68,0.01)');
|
| 946 |
-
}
|
| 947 |
-
|
| 948 |
-
ctx.beginPath();
|
| 949 |
-
ctx.moveTo(xOf(0), yOf(data[0].price));
|
| 950 |
-
for (let i = 1; i < data.length; i++) {
|
| 951 |
-
const x1 = xOf(i-1), x2 = xOf(i);
|
| 952 |
-
const y1 = yOf(data[i-1].price), y2 = yOf(data[i].price);
|
| 953 |
-
const cpX = (x1 + x2) / 2;
|
| 954 |
-
ctx.bezierCurveTo(cpX, y1, cpX, y2, x2, y2);
|
| 955 |
-
}
|
| 956 |
-
ctx.lineTo(xOf(data.length-1), H - pad.b);
|
| 957 |
-
ctx.lineTo(xOf(0), H - pad.b);
|
| 958 |
-
ctx.closePath();
|
| 959 |
-
ctx.fillStyle = grad;
|
| 960 |
-
ctx.fill();
|
| 961 |
-
|
| 962 |
-
// Line
|
| 963 |
-
ctx.beginPath();
|
| 964 |
-
ctx.moveTo(xOf(0), yOf(data[0].price));
|
| 965 |
-
for (let i = 1; i < data.length; i++) {
|
| 966 |
-
const x1 = xOf(i-1), x2 = xOf(i);
|
| 967 |
-
const y1 = yOf(data[i-1].price), y2 = yOf(data[i].price);
|
| 968 |
-
const cpX = (x1 + x2) / 2;
|
| 969 |
-
ctx.bezierCurveTo(cpX, y1, cpX, y2, x2, y2);
|
| 970 |
-
}
|
| 971 |
-
ctx.strokeStyle = isUp ? '#00e676' : '#ff1744';
|
| 972 |
-
ctx.lineWidth = 2;
|
| 973 |
-
ctx.shadowColor = isUp ? '#00e676' : '#ff1744';
|
| 974 |
-
ctx.shadowBlur = 6;
|
| 975 |
-
ctx.stroke();
|
| 976 |
-
ctx.shadowBlur = 0;
|
| 977 |
-
|
| 978 |
-
// Last price dot
|
| 979 |
-
const lx = xOf(data.length - 1);
|
| 980 |
-
const ly = yOf(last.price);
|
| 981 |
-
ctx.beginPath(); ctx.arc(lx, ly, 4, 0, Math.PI*2);
|
| 982 |
-
ctx.fillStyle = isUp ? '#00e676' : '#ff1744';
|
| 983 |
-
ctx.fill();
|
| 984 |
-
|
| 985 |
-
// OHLC mini labels
|
| 986 |
-
ctx.fillStyle = '#5a7a90'; ctx.font = '9px JetBrains Mono';
|
| 987 |
-
ctx.textAlign = 'left';
|
| 988 |
-
ctx.fillText(`↑ ${maxP.toFixed(2)}`, pad.l + 5, pad.t + 12);
|
| 989 |
-
ctx.fillText(`↓ ${minP.toFixed(2)}`, pad.l + 5, H - pad.b - 5);
|
| 990 |
-
}
|
| 991 |
-
|
| 992 |
-
// ── Feed ──────────────────────────────────────────────────────────────────────
|
| 993 |
-
async function feedStep(n) {
|
| 994 |
-
const ticks = await api('POST', '/api/feed/step', {n});
|
| 995 |
-
if (!Array.isArray(ticks)) return;
|
| 996 |
-
ticks.forEach(t => {
|
| 997 |
-
addPricePoint(t.symbol, t.last, t.ts);
|
| 998 |
-
if (t.symbol === selectedSymbol) {
|
| 999 |
-
document.getElementById('chart-price').textContent = '$' + t.last.toFixed(2);
|
| 1000 |
-
// Pre-fill order price
|
| 1001 |
-
const priceInput = document.getElementById('oe-price');
|
| 1002 |
-
if (!priceInput.value || priceInput.value == '0') {
|
| 1003 |
-
priceInput.value = t.bid.toFixed(2);
|
| 1004 |
-
}
|
| 1005 |
-
}
|
| 1006 |
-
});
|
| 1007 |
-
refreshBook();
|
| 1008 |
-
}
|
| 1009 |
-
|
| 1010 |
-
let autoRunning = false;
|
| 1011 |
-
function autoFeed() {
|
| 1012 |
-
const btn = document.getElementById('auto-btn');
|
| 1013 |
-
if (autoRunning) {
|
| 1014 |
-
clearInterval(autoFeedInterval);
|
| 1015 |
-
autoRunning = false;
|
| 1016 |
-
btn.textContent = 'AUTO';
|
| 1017 |
-
btn.classList.remove('btn-sell');
|
| 1018 |
-
btn.classList.add('btn-accent');
|
| 1019 |
-
} else {
|
| 1020 |
-
autoRunning = true;
|
| 1021 |
-
btn.textContent = '■ STOP';
|
| 1022 |
-
btn.classList.remove('btn-accent');
|
| 1023 |
-
btn.classList.add('btn-sell');
|
| 1024 |
-
autoFeedInterval = setInterval(() => feedStep(5), 200);
|
| 1025 |
-
}
|
| 1026 |
-
}
|
| 1027 |
-
|
| 1028 |
-
// ── Order Entry ───────────────────────────────────────────────────────────────
|
| 1029 |
-
function setOrderSide(side) {
|
| 1030 |
-
orderSide = side;
|
| 1031 |
-
const buyTab = document.getElementById('tab-buy');
|
| 1032 |
-
const sellTab = document.getElementById('tab-sell');
|
| 1033 |
-
const btn = document.getElementById('submit-btn');
|
| 1034 |
-
if (side === 'BUY') {
|
| 1035 |
-
buyTab.className = 'tab active-buy';
|
| 1036 |
-
sellTab.className = 'tab';
|
| 1037 |
-
btn.className = 'btn btn-buy';
|
| 1038 |
-
btn.textContent = '▲ PLACE BUY ORDER';
|
| 1039 |
-
} else {
|
| 1040 |
-
buyTab.className = 'tab';
|
| 1041 |
-
sellTab.className = 'tab active-sell';
|
| 1042 |
-
btn.className = 'btn btn-sell';
|
| 1043 |
-
btn.textContent = '▼ PLACE SELL ORDER';
|
| 1044 |
-
}
|
| 1045 |
-
}
|
| 1046 |
-
|
| 1047 |
-
function updateOrderForm() {
|
| 1048 |
-
const type = document.getElementById('oe-type').value;
|
| 1049 |
-
const priceGroup = document.getElementById('oe-price-group');
|
| 1050 |
-
priceGroup.style.display = (type === 'MARKET') ? 'none' : 'block';
|
| 1051 |
-
}
|
| 1052 |
-
|
| 1053 |
-
async function submitOrder() {
|
| 1054 |
-
const sym = document.getElementById('oe-symbol').value;
|
| 1055 |
-
const type = document.getElementById('oe-type').value;
|
| 1056 |
-
const price = parseFloat(document.getElementById('oe-price').value) || 0;
|
| 1057 |
-
const qty = parseInt(document.getElementById('oe-qty').value) || 100;
|
| 1058 |
-
const cid = document.getElementById('oe-cid').value;
|
| 1059 |
-
|
| 1060 |
-
const body = {
|
| 1061 |
-
symbol: sym, side: orderSide, type,
|
| 1062 |
-
price, qty, client_id: cid,
|
| 1063 |
-
market_price: price
|
| 1064 |
-
};
|
| 1065 |
-
|
| 1066 |
-
const r = await api('POST', '/api/order', body);
|
| 1067 |
-
if (!r) return;
|
| 1068 |
-
|
| 1069 |
-
if (r.approved) {
|
| 1070 |
-
showToast(`✓ ${orderSide} ${qty} ${sym} @ ${type === 'MARKET' ? 'MKT' : '$'+price} — ${r.order.status}`, 'green');
|
| 1071 |
-
if (r.trades && r.trades.length > 0) {
|
| 1072 |
-
showToast(`⚡ ${r.trades.length} trade(s) executed!`, 'green');
|
| 1073 |
-
}
|
| 1074 |
-
} else {
|
| 1075 |
-
showToast(`✗ REJECTED: ${r.reject_reason}`, 'red');
|
| 1076 |
-
document.body.classList.add('flash-red');
|
| 1077 |
-
setTimeout(() => document.body.classList.remove('flash-red'), 500);
|
| 1078 |
-
}
|
| 1079 |
-
|
| 1080 |
-
refreshBook();
|
| 1081 |
-
refreshPositions();
|
| 1082 |
-
refreshTrades();
|
| 1083 |
-
}
|
| 1084 |
-
|
| 1085 |
-
// ── Positions ─────────────────────────────────────────────────────────────────
|
| 1086 |
-
async function refreshPositions() {
|
| 1087 |
-
const pos = await api('GET', '/api/positions');
|
| 1088 |
-
if (!Array.isArray(pos)) return;
|
| 1089 |
-
|
| 1090 |
-
const tbody = document.getElementById('pos-body');
|
| 1091 |
-
tbody.innerHTML = pos.filter(p => p.net_qty !== 0).map(p => {
|
| 1092 |
-
const rPnl = p.realized_pnl || 0;
|
| 1093 |
-
const uPnl = p.unrealized_pnl || 0;
|
| 1094 |
-
const posDir = p.net_qty > 0 ? '+' : '';
|
| 1095 |
-
return `<tr>
|
| 1096 |
-
<td style="color:var(--cyan);font-weight:700">${p.symbol}</td>
|
| 1097 |
-
<td style="color:${p.net_qty>0?'var(--green)':'var(--red)'}">${posDir}${fmtNum(p.net_qty)}</td>
|
| 1098 |
-
<td>$${p.avg_price.toFixed(2)}</td>
|
| 1099 |
-
<td style="color:${rPnl>=0?'var(--green)':'var(--red)'}">${rPnl>=0?'+':''}$${rPnl.toFixed(2)}</td>
|
| 1100 |
-
<td style="color:${uPnl>=0?'var(--green)':'var(--red)'}">${uPnl>=0?'+':''}$${uPnl.toFixed(2)}</td>
|
| 1101 |
-
</tr>`;
|
| 1102 |
-
}).join('') || '<tr><td colspan="5" style="text-align:center;color:var(--dim);padding:10px">No open positions</td></tr>';
|
| 1103 |
-
|
| 1104 |
-
// Order log
|
| 1105 |
-
const orders = await api('GET', '/api/orders');
|
| 1106 |
-
if (!Array.isArray(orders)) return;
|
| 1107 |
-
const logEl = document.getElementById('order-log');
|
| 1108 |
-
logEl.innerHTML = orders.slice(-20).reverse().map(o => {
|
| 1109 |
-
const isRej = o.status === 'REJECTED';
|
| 1110 |
-
return `<div class="trade-entry" style="${isRej?'border-left:2px solid var(--red)':''}">
|
| 1111 |
-
<span class="trade-side ${o.side==='BUY'?'buy-badge':'sell-badge'}">${o.side}</span>
|
| 1112 |
-
<span style="color:var(--cyan)">${o.symbol}</span>
|
| 1113 |
-
<span style="color:var(--dim)">${o.type}</span>
|
| 1114 |
-
<span>$${(o.price||0).toFixed(2)}</span>
|
| 1115 |
-
<span style="color:var(--dim)">×${fmtNum(o.qty)}</span>
|
| 1116 |
-
<span style="color:${isRej?'var(--red)':o.status==='FILLED'?'var(--green)':'var(--dim)'}">
|
| 1117 |
-
${o.status}
|
| 1118 |
-
</span>
|
| 1119 |
-
</div>`;
|
| 1120 |
-
}).join('');
|
| 1121 |
-
}
|
| 1122 |
-
|
| 1123 |
-
// ── Trades ────────────────────────────────────────────────────────────────────
|
| 1124 |
-
async function refreshTrades() {
|
| 1125 |
-
const trades = await api('GET', '/api/trades');
|
| 1126 |
-
if (!Array.isArray(trades)) return;
|
| 1127 |
-
|
| 1128 |
-
document.getElementById('trade-count').textContent = `${trades.length} trades`;
|
| 1129 |
-
document.getElementById('h-trades').textContent = trades.length;
|
| 1130 |
-
|
| 1131 |
-
const logEl = document.getElementById('trade-log');
|
| 1132 |
-
logEl.innerHTML = trades.slice().reverse().slice(0, 100).map(t => {
|
| 1133 |
-
const time = new Date(t.timestamp / 1e6).toISOString().substr(11,12);
|
| 1134 |
-
return `<div class="trade-entry" style="padding:5px 12px">
|
| 1135 |
-
<span style="color:var(--dim);font-size:9px">${time}</span>
|
| 1136 |
-
<span style="color:var(--cyan);font-weight:700">${t.symbol}</span>
|
| 1137 |
-
<span style="color:var(--text)">$${t.price.toFixed(4)}</span>
|
| 1138 |
-
<span style="color:var(--dim)">×</span>
|
| 1139 |
-
<span style="font-weight:600">${fmtNum(t.qty)}</span>
|
| 1140 |
-
<span style="color:var(--dim);font-size:9px">#${t.trade_id}</span>
|
| 1141 |
-
</div>`;
|
| 1142 |
-
}).join('');
|
| 1143 |
-
}
|
| 1144 |
-
|
| 1145 |
-
// ── Backtest ──────────────────────────────────────────────────────────────────
|
| 1146 |
-
async function runBacktest() {
|
| 1147 |
-
const btn = document.getElementById('bt-btn');
|
| 1148 |
-
btn.textContent = '⟳ RUNNING...'; btn.disabled = true;
|
| 1149 |
-
|
| 1150 |
-
try {
|
| 1151 |
-
const r = await api('POST', '/api/backtest', {
|
| 1152 |
-
strategy: document.getElementById('bt-strategy').value,
|
| 1153 |
-
symbol: document.getElementById('bt-symbol').value,
|
| 1154 |
-
ticks: parseInt(document.getElementById('bt-ticks').value),
|
| 1155 |
-
start_price: parseFloat(document.getElementById('bt-price').value)
|
| 1156 |
-
});
|
| 1157 |
-
|
| 1158 |
-
document.getElementById('bt-result').style.display = 'block';
|
| 1159 |
-
setVal('bt-r-strategy', r.strategy);
|
| 1160 |
-
setVal('bt-r-ticks', fmtNum(r.ticks_processed));
|
| 1161 |
-
setVal('bt-r-orders', fmtNum(r.orders_submitted));
|
| 1162 |
-
setVal('bt-r-fills', fmtNum(r.orders_filled));
|
| 1163 |
-
setVal('bt-r-rej', fmtNum(r.orders_rejected));
|
| 1164 |
-
setVal('bt-r-trades', fmtNum(r.trades_count));
|
| 1165 |
-
|
| 1166 |
-
const pnlEl = document.getElementById('bt-r-pnl');
|
| 1167 |
-
pnlEl.textContent = (r.total_pnl >= 0 ? '+' : '') + '$' + r.total_pnl.toFixed(2);
|
| 1168 |
-
pnlEl.className = 'stat-val ' + (r.total_pnl >= 0 ? 'green' : 'red');
|
| 1169 |
-
|
| 1170 |
-
document.getElementById('bt-r-dd').textContent = '-$' + r.max_drawdown.toFixed(2);
|
| 1171 |
-
document.getElementById('bt-r-sharpe').textContent = r.sharpe.toFixed(3);
|
| 1172 |
-
document.getElementById('bt-r-p50').textContent = r.order_p50_us.toFixed(2) + 'µs';
|
| 1173 |
-
document.getElementById('bt-r-p99').textContent = r.order_p99_us.toFixed(2) + 'µs';
|
| 1174 |
-
document.getElementById('bt-r-thru').textContent = fmtNum(Math.round(r.throughput_eps)) + ' ev/s';
|
| 1175 |
-
|
| 1176 |
-
drawEquityCurve(r.equity_curve || []);
|
| 1177 |
-
} finally {
|
| 1178 |
-
btn.textContent = '▶ RUN BACKTEST'; btn.disabled = false;
|
| 1179 |
-
}
|
| 1180 |
-
}
|
| 1181 |
-
|
| 1182 |
-
function drawEquityCurve(curve) {
|
| 1183 |
-
const canvas = document.getElementById('equity-canvas');
|
| 1184 |
-
const ctx = canvas.getContext('2d');
|
| 1185 |
-
const W = canvas.parentElement.offsetWidth - 2;
|
| 1186 |
-
const H = 100;
|
| 1187 |
-
canvas.width = W; canvas.height = H;
|
| 1188 |
-
|
| 1189 |
-
ctx.fillStyle = '#0d1219';
|
| 1190 |
-
ctx.fillRect(0,0,W,H);
|
| 1191 |
-
|
| 1192 |
-
if (curve.length < 2) { return; }
|
| 1193 |
-
|
| 1194 |
-
const minV = Math.min(...curve);
|
| 1195 |
-
const maxV = Math.max(...curve);
|
| 1196 |
-
const range = maxV - minV || 1;
|
| 1197 |
-
const pad = 15;
|
| 1198 |
-
|
| 1199 |
-
const xOf = i => pad + (i / (curve.length-1)) * (W - 2*pad);
|
| 1200 |
-
const yOf = v => pad + (1 - (v - minV)/range) * (H - 2*pad);
|
| 1201 |
-
|
| 1202 |
-
// Zero line
|
| 1203 |
-
const zy = yOf(0);
|
| 1204 |
-
if (zy > pad && zy < H-pad) {
|
| 1205 |
-
ctx.strokeStyle = '#1e2d3d';
|
| 1206 |
-
ctx.setLineDash([3,3]);
|
| 1207 |
-
ctx.beginPath(); ctx.moveTo(pad, zy); ctx.lineTo(W-pad, zy); ctx.stroke();
|
| 1208 |
-
ctx.setLineDash([]);
|
| 1209 |
-
}
|
| 1210 |
-
|
| 1211 |
-
ctx.beginPath();
|
| 1212 |
-
ctx.moveTo(xOf(0), yOf(curve[0]));
|
| 1213 |
-
for (let i = 1; i < curve.length; i++) ctx.lineTo(xOf(i), yOf(curve[i]));
|
| 1214 |
-
ctx.strokeStyle = curve[curve.length-1] >= 0 ? '#00e676' : '#ff1744';
|
| 1215 |
-
ctx.lineWidth = 1.5;
|
| 1216 |
-
ctx.stroke();
|
| 1217 |
-
|
| 1218 |
-
ctx.fillStyle = '#5a7a90'; ctx.font = '8px JetBrains Mono';
|
| 1219 |
-
ctx.textAlign = 'right';
|
| 1220 |
-
ctx.fillText('$'+maxV.toFixed(1), W-2, pad+8);
|
| 1221 |
-
ctx.fillText('$'+minV.toFixed(1), W-2, H-pad);
|
| 1222 |
-
}
|
| 1223 |
-
|
| 1224 |
-
// ── Risk Limits ───────────────────────────────────────────────────────────────
|
| 1225 |
-
async function setRiskLimits() {
|
| 1226 |
-
const r = await api('POST', '/api/risk/limits', {
|
| 1227 |
-
max_order_qty: parseInt(document.getElementById('r-max-qty').value),
|
| 1228 |
-
max_position: parseInt(document.getElementById('r-max-pos').value),
|
| 1229 |
-
max_notional_usd: parseFloat(document.getElementById('r-max-notional').value),
|
| 1230 |
-
max_orders_per_sec: parseInt(document.getElementById('r-max-rate').value),
|
| 1231 |
-
});
|
| 1232 |
-
showToast(r.ok ? '✓ Risk limits updated' : '✗ Failed', r.ok ? 'green' : 'red');
|
| 1233 |
-
}
|
| 1234 |
-
|
| 1235 |
-
// ── Reset ─────────────────────────────────────────────────────────────────────
|
| 1236 |
-
async function resetSim() {
|
| 1237 |
-
await api('POST', '/api/reset');
|
| 1238 |
-
priceHistory = {};
|
| 1239 |
-
document.getElementById('trade-log').innerHTML = '';
|
| 1240 |
-
showToast('↺ Simulator reset', 'cyan');
|
| 1241 |
-
}
|
| 1242 |
-
|
| 1243 |
-
// ── Right mode ────────────────────────────────────────────────────────────────
|
| 1244 |
-
function setRightMode(mode) {
|
| 1245 |
-
rightMode = mode;
|
| 1246 |
-
['positions','backtest','risk'].forEach(m => {
|
| 1247 |
-
document.getElementById('view-'+m).style.display = (m === mode) ? 'flex' : 'none';
|
| 1248 |
-
});
|
| 1249 |
-
document.querySelectorAll('.mode-tab').forEach((t,i) => {
|
| 1250 |
-
const modes = ['positions','backtest','risk'];
|
| 1251 |
-
t.className = 'mode-tab' + (modes[i]===mode ? ' active' : '');
|
| 1252 |
-
});
|
| 1253 |
-
}
|
| 1254 |
-
|
| 1255 |
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
| 1256 |
-
function fmtNum(n) {
|
| 1257 |
-
if (n >= 1e6) return (n/1e6).toFixed(1)+'M';
|
| 1258 |
-
if (n >= 1e3) return (n/1e3).toFixed(1)+'K';
|
| 1259 |
-
return n.toString();
|
| 1260 |
-
}
|
| 1261 |
-
|
| 1262 |
-
function showToast(msg, color='cyan') {
|
| 1263 |
-
const t = document.createElement('div');
|
| 1264 |
-
t.className = 'toast';
|
| 1265 |
-
t.style.borderColor = `var(--${color})`;
|
| 1266 |
-
t.style.color = `var(--${color})`;
|
| 1267 |
-
t.textContent = msg;
|
| 1268 |
-
document.body.appendChild(t);
|
| 1269 |
-
setTimeout(() => t.remove(), 3000);
|
| 1270 |
-
}
|
| 1271 |
-
|
| 1272 |
-
// Resize handler for chart
|
| 1273 |
-
window.addEventListener('resize', () => {
|
| 1274 |
-
if (priceHistory[selectedSymbol]) drawPriceChart();
|
| 1275 |
-
});
|
| 1276 |
-
</script>
|
| 1277 |
-
</body>
|
| 1278 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|