Alvin3y1 commited on
Commit
c428f6c
·
verified ·
1 Parent(s): c91e0bd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +507 -747
app.py CHANGED
@@ -2,818 +2,578 @@ import asyncio
2
  import json
3
  import logging
4
  import time
5
- import bisect
6
  import math
7
  import statistics
8
  import aiohttp
9
- from datetime import datetime
10
  from aiohttp import web
11
- import websockets
12
-
13
- SYMBOL_KRAKEN = "BTC/USD"
 
 
 
 
 
 
14
  PORT = 7860
15
- HISTORY_LENGTH = 300
16
- BROADCAST_RATE = 0.1
17
-
18
- DECAY_LAMBDA = 50.0
19
- IMPACT_SENSITIVITY = 2.0
20
- WALL_DAMPENING = 0.8
21
- Z_SCORE_THRESHOLD = 3.0
22
- WALL_LOOKBACK = 200
23
-
24
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
25
-
26
- market_state = {
27
- "bids": {},
28
- "asks": {},
29
- "history": [],
30
- "pred_history": [],
31
- "trade_vol_history": [],
32
- "ohlc_history": [],
33
- "current_vol_window": {"buy": 0.0, "sell": 0.0, "start": time.time()},
34
- "current_mid": 0.0,
35
- "ready": False
36
- }
37
-
38
- connected_clients = set()
39
-
40
- def detect_anomalies(orders, scan_depth):
41
- if len(orders) < 10: return []
42
- relevant_orders = orders[:scan_depth]
43
- volumes = [q for p, q in relevant_orders]
44
- if not volumes: return []
45
-
46
- try:
47
- avg_vol = statistics.mean(volumes)
48
- stdev_vol = statistics.stdev(volumes)
49
- except statistics.StatisticsError:
50
- return []
51
-
52
- if stdev_vol == 0: return []
53
-
54
- walls = []
55
- for price, qty in relevant_orders:
56
- z_score = (qty - avg_vol) / stdev_vol
57
- if z_score > Z_SCORE_THRESHOLD:
58
- walls.append({"price": price, "vol": qty, "z_score": z_score})
59
-
60
- walls.sort(key=lambda x: x['z_score'], reverse=True)
61
- return walls[:3]
62
-
63
- def calculate_micro_price_structure(diff_x, diff_y_net, current_mid, best_bid, best_ask, walls):
64
- if not diff_x or len(diff_x) < 5: return None
65
-
66
- weighted_imbalance = 0.0
67
- total_weight = 0.0
68
-
69
- for i in range(len(diff_x)):
70
- dist = diff_x[i]
71
- net_vol = diff_y_net[i]
72
- weight = math.exp(-dist / DECAY_LAMBDA)
73
- weighted_imbalance += net_vol * weight
74
- total_weight += weight
75
-
76
- rho = weighted_imbalance / total_weight if total_weight > 0 else 0
77
-
78
- spread = best_ask - best_bid
79
- theoretical_delta = (spread / 2) * rho * IMPACT_SENSITIVITY
80
- projected_price = current_mid + theoretical_delta
81
-
82
- final_delta = theoretical_delta
83
- if final_delta > 0 and walls['asks']:
84
- nearest_wall = walls['asks'][0]
85
- if projected_price >= nearest_wall['price']:
86
- damp_factor = 1.0 / (1.0 + (nearest_wall['z_score'] * 0.2))
87
- final_delta *= damp_factor
88
- elif final_delta < 0 and walls['bids']:
89
- nearest_wall = walls['bids'][0]
90
- if projected_price <= nearest_wall['price']:
91
- damp_factor = 1.0 / (1.0 + (nearest_wall['z_score'] * 0.2))
92
- final_delta *= damp_factor
93
-
94
- return {
95
- "projected": current_mid + final_delta,
96
- "rho": rho
97
- }
98
-
99
- def calculate_polr(bids, asks, mid):
100
- if not bids or not asks: return []
101
-
102
- sorted_bids = sorted(bids.items(), key=lambda x: -x[0])
103
- sorted_asks = sorted(asks.items(), key=lambda x: x[0])
104
 
105
- path_points = []
106
- volume_steps = [i * 0.5 for i in range(1, 61)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
- current_time = time.time()
 
 
 
 
109
 
110
- for i, target_vol in enumerate(volume_steps):
111
- ask_cost_dist = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  cum_vol = 0
113
- target_ask_price = mid
114
- for p, q in sorted_asks:
115
  cum_vol += q
116
- if cum_vol >= target_vol:
117
- target_ask_price = p
118
- break
119
- ask_cost_dist = target_ask_price - mid
120
-
121
- bid_cost_dist = 0
122
  cum_vol = 0
123
- target_bid_price = mid
124
- for p, q in sorted_bids:
125
  cum_vol += q
126
- if cum_vol >= target_vol:
127
- target_bid_price = p
128
- break
129
- bid_cost_dist = mid - target_bid_price
 
 
 
 
130
 
131
- if bid_cost_dist <= 0: bid_cost_dist = 0.01
132
- if ask_cost_dist <= 0: ask_cost_dist = 0.01
133
 
134
- projected_p = mid
135
- if ask_cost_dist > bid_cost_dist:
136
- projected_p = target_ask_price
137
- else:
138
- projected_p = target_bid_price
139
-
140
- path_points.append({
141
- 'index': i,
142
- 'p': projected_p
143
- })
144
 
145
- return path_points
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
- def process_market_data():
148
- if not market_state['ready']: return {"error": "Initializing..."}
 
 
149
 
150
- mid = market_state['current_mid']
151
-
152
- now = time.time()
153
- if now - market_state['current_vol_window']['start'] >= 1.0:
154
- market_state['trade_vol_history'].append({
155
- 't': now,
156
- 'buy': market_state['current_vol_window']['buy'],
157
- 'sell': market_state['current_vol_window']['sell']
158
- })
159
- if len(market_state['trade_vol_history']) > 60:
160
- market_state['trade_vol_history'].pop(0)
161
- market_state['current_vol_window'] = {"buy": 0.0, "sell": 0.0, "start": now}
162
-
163
- sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
164
- sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
165
-
166
- if not sorted_bids or not sorted_asks: return {"error": "Empty Book"}
167
-
168
- best_bid = sorted_bids[0][0]
169
- best_ask = sorted_asks[0][0]
170
-
171
- bid_walls = detect_anomalies(sorted_bids, WALL_LOOKBACK)
172
- ask_walls = detect_anomalies(sorted_asks, WALL_LOOKBACK)
173
-
174
- d_b_x, d_b_y, cum = [], [], 0
175
- for p, q in sorted_bids[:300]:
176
- d = mid - p
177
- if d >= 0:
178
- cum += q
179
- d_b_x.append(d); d_b_y.append(cum)
180
-
181
- d_a_x, d_a_y, cum = [], [], 0
182
- for p, q in sorted_asks[:300]:
183
- d = p - mid
184
- if d >= 0:
185
- cum += q
186
- d_a_x.append(d); d_a_y.append(cum)
187
-
188
- diff_x, diff_y_net = [], []
189
- chart_bids, chart_asks = [], []
190
-
191
- if d_b_x and d_a_x:
192
- max_dist = min(d_b_x[-1], d_a_x[-1])
193
- step_size = max_dist / 100
194
- steps = [i * step_size for i in range(1, 101)]
195
 
196
- for s in steps:
197
- idx_b = bisect.bisect_right(d_b_x, s)
198
- vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
199
- idx_a = bisect.bisect_right(d_a_x, s)
200
- vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
201
-
202
- diff_x.append(s)
203
- diff_y_net.append(vol_b - vol_a)
204
- chart_bids.append(vol_b)
205
- chart_asks.append(vol_a)
206
-
207
- analysis = calculate_micro_price_structure(
208
- diff_x, diff_y_net, mid, best_bid, best_ask,
209
- {"bids": bid_walls, "asks": ask_walls}
210
- )
211
-
212
- polr_path = calculate_polr(market_state['bids'], market_state['asks'], mid)
213
-
214
- if analysis:
215
- if not market_state['pred_history'] or (now - market_state['pred_history'][-1]['t'] > 0.5):
216
- market_state['pred_history'].append({'t': now, 'p': analysis['projected']})
217
- if len(market_state['pred_history']) > HISTORY_LENGTH:
218
- market_state['pred_history'].pop(0)
219
-
220
- return {
221
- "mid": mid,
222
- "history": market_state['history'],
223
- "pred_history": market_state['pred_history'],
224
- "polr": polr_path,
225
- "trade_history": market_state['trade_vol_history'],
226
- "ohlc": market_state['ohlc_history'],
227
- "depth_x": diff_x,
228
- "depth_net": diff_y_net,
229
- "depth_bids": chart_bids,
230
- "depth_asks": chart_asks,
231
- "analysis": analysis,
232
- "walls": {"bids": bid_walls, "asks": ask_walls}
233
- }
234
 
235
- HTML_PAGE = f"""
236
  <!DOCTYPE html>
237
  <html lang="en">
238
  <head>
239
  <meta charset="UTF-8">
240
- <title>{SYMBOL_KRAKEN}</title>
241
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
242
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
243
  <style>
244
- :root {{
245
- --bg-base: #000000;
246
- --bg-panel: #0a0a0a;
247
- --border: #252525;
248
- --text-main: #FFFFFF;
249
- --text-dim: #999999;
250
- --green: #00ff9d;
251
- --red: #ff3b3b;
252
- --blue: #2979ff;
253
- --yellow: #ffeb3b;
254
- --purple: #d500f9;
255
- }}
256
- body {{
257
- margin: 0; padding: 0;
258
- background-color: var(--bg-base);
259
- color: var(--text-main);
260
- font-family: 'Inter', sans-serif;
261
- overflow: hidden;
262
- height: 100vh; width: 100vw;
263
- }}
264
- .layout {{
265
- display: grid;
266
- grid-template-rows: 34px 1fr 1fr;
267
- grid-template-columns: 3fr 1fr;
268
  gap: 1px;
269
- background-color: var(--border);
270
- height: 100vh;
271
- box-sizing: border-box;
272
- }}
273
- .panel {{ background: var(--bg-panel); display: flex; flex-direction: column; overflow: hidden; }}
274
 
275
- .status-bar {{
276
- grid-column: 1 / 3;
277
- grid-row: 1 / 2;
278
- background: var(--bg-panel);
279
- display: flex;
280
- align-items: center;
281
- justify-content: space-between;
282
- padding: 0 12px;
283
- font-family: 'JetBrains Mono', monospace;
284
- font-size: 12px;
285
- text-transform: uppercase;
286
- border-bottom: 1px solid var(--border);
287
- z-index: 50;
288
- }}
289
- .status-left {{ display: flex; gap: 20px; align-items: center; }}
290
- .live-dot {{ width: 8px; height: 8px; background-color: var(--green); border-radius: 50%; display: inline-block; box-shadow: 0 0 8px var(--green); }}
291
- .ticker-val {{ font-weight: 700; color: #fff; font-size: 13px; }}
292
-
293
- #p-chart {{ grid-column: 1 / 2; grid-row: 2 / 3; }}
294
 
295
- #p-bottom {{
296
- grid-column: 1 / 2; grid-row: 3 / 4;
297
- display: grid;
298
- grid-template-columns: 1fr 1fr;
299
- gap: 1px;
300
- background: var(--border);
301
- }}
302
- .bottom-sub {{ background: var(--bg-panel); display: flex; flex-direction: column; position: relative; }}
303
-
304
- #p-sidebar {{
305
- grid-column: 2 / 3;
306
- grid-row: 2 / 4;
307
- padding: 15px;
308
- display: flex;
309
- flex-direction: column;
310
- gap: 15px;
311
- border-left: 1px solid var(--border);
312
- overflow: hidden;
313
- }}
314
-
315
- .chart-header {{
316
- height: 24px;
317
- min-height: 24px;
318
- display: flex;
319
- align-items: center;
320
- padding-left: 12px;
321
- font-size: 10px;
322
- font-weight: 700;
323
- color: var(--text-dim);
324
- background: #050505;
325
- border-bottom: 1px solid #151515;
326
- letter-spacing: 0.5px;
327
- }}
328
-
329
- .data-group {{ display: flex; flex-direction: column; gap: 4px; }}
330
- .label {{ font-size: 10px; color: var(--text-dim); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }}
331
- .value {{ font-family: 'JetBrains Mono', monospace; font-size: 20px; font-weight: 700; color: #fff; }}
332
- .value-lg {{ font-size: 26px; }}
333
- .value-sub {{ font-family: 'JetBrains Mono', monospace; font-size: 11px; margin-top: 2px; color: #666; }}
334
-
335
- .divider {{ height: 1px; background: var(--border); width: 100%; }}
336
- .c-green {{ color: var(--green); }}
337
- .c-red {{ color: var(--red); }}
338
- .c-dim {{ color: var(--text-dim); }}
339
- .c-purp {{ color: var(--purple); }}
340
-
341
- .list-container {{ display: flex; flex-direction: column; gap: 8px; overflow-y: auto; height: 100px; }}
342
- .list-item {{
343
- display: flex; justify-content: space-between;
344
- font-family: 'JetBrains Mono', monospace;
345
- font-size: 11px;
346
- border-bottom: 1px solid #151515;
347
- padding-bottom: 4px;
348
- }}
349
- .list-item span:first-child {{ color: #e0e0e0; }}
350
- .list-item:last-child {{ border: none; }}
351
 
352
- .sidebar-chart-box {{
353
- flex: 1;
354
- display: flex;
355
- flex-direction: column;
356
- min-height: 0;
357
- }}
358
- .mini-chart {{
359
- flex: 1;
360
- background: rgba(255,255,255,0.02);
361
- border: 1px solid var(--border);
362
- border-radius: 4px;
363
- }}
364
  </style>
365
  </head>
366
  <body>
367
- <div class="layout">
368
- <div class="status-bar">
369
- <div class="status-left">
370
- <span class="live-dot"></span>
371
- <span style="font-weight:700; color:#fff;">{SYMBOL_KRAKEN}</span>
372
- <span id="price-ticker" class="ticker-val">---</span>
373
- </div>
374
- <div class="status-right" id="clock">00:00:00 UTC</div>
375
- </div>
376
 
377
- <div id="p-chart" class="panel">
378
- <div class="chart-header">
379
- PRICE (BLUE) // <span class="c-purp">POLR RIVER (HIGH RES)</span> // <span style="color:var(--yellow)">PRED (YELLOW)</span>
380
- </div>
381
- <div id="tv-price" style="flex: 1; width: 100%;"></div>
382
  </div>
383
 
384
- <div id="p-bottom">
385
- <div class="bottom-sub">
386
- <div class="chart-header">1M KLINE (KRAKEN OHLC)</div>
387
- <div id="tv-candles" style="flex: 1; width: 100%;"></div>
388
- </div>
389
- <div class="bottom-sub">
390
- <div class="chart-header">ORDER FLOW IMBALANCE</div>
391
- <div id="tv-net" style="flex: 1; width: 100%;"></div>
392
  </div>
393
  </div>
 
 
 
 
394
 
395
- <div id="p-sidebar" class="panel">
396
-
397
- <div class="data-group">
398
- <span class="label">Micro-Price Delta</span>
399
- <div style="display:flex; align-items: baseline; gap: 10px;">
400
- <span id="proj-pct" class="value value-lg">--%</span>
401
- <span id="proj-val" class="value-sub">---</span>
402
- </div>
403
- </div>
404
-
405
- <div class="divider"></div>
406
-
407
- <div class="data-group">
408
- <span class="label">OFI Imbalance Ratio</span>
409
- <span id="score-val" class="value">0.00</span>
410
  </div>
411
-
412
- <div class="divider"></div>
413
-
414
- <div class="data-group">
415
- <span class="label">Detected Walls (Z > 3.0)</span>
416
- <div id="wall-list" class="list-container">
417
- <span class="c-dim" style="font-size: 11px;">Scanning...</span>
418
- </div>
419
  </div>
420
-
421
- <div class="sidebar-chart-box">
422
- <span class="label" style="margin-bottom:4px;">Real-time Volume Ticks</span>
423
- <div id="sidebar-vol" class="mini-chart"></div>
424
  </div>
425
-
426
- <div class="sidebar-chart-box">
427
- <span class="label" style="margin-bottom:4px;">Liquidity Density</span>
428
- <div id="sidebar-density" class="mini-chart"></div>
429
  </div>
430
  </div>
431
  </div>
432
 
433
  <script>
434
- setInterval(() => {{
435
- const now = new Date();
436
- document.getElementById('clock').innerText = now.toISOString().split('T')[1].split('.')[0] + ' UTC';
437
- }}, 1000);
438
-
439
- document.addEventListener('DOMContentLoaded', () => {{
440
- const dom = {{
441
- ticker: document.getElementById('price-ticker'),
442
- score: document.getElementById('score-val'),
443
- projVal: document.getElementById('proj-val'),
444
- projPct: document.getElementById('proj-pct'),
445
- wallList: document.getElementById('wall-list')
446
- }};
447
-
448
- const chartOpts = {{
449
- layout: {{ background: {{ type: 'solid', color: '#0a0a0a' }}, textColor: '#888', fontFamily: 'JetBrains Mono' }},
450
- grid: {{ vertLines: {{ color: '#151515' }}, horzLines: {{ color: '#151515' }} }},
451
- rightPriceScale: {{ borderColor: '#222', scaleMargins: {{ top: 0.1, bottom: 0.1 }} }},
452
- timeScale: {{ borderColor: '#222', timeVisible: true, secondsVisible: true }},
453
- crosshair: {{ mode: 1, vertLine: {{ color: '#444', labelBackgroundColor: '#444' }}, horzLine: {{ color: '#444', labelBackgroundColor: '#444' }} }}
454
- }};
455
-
456
- const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts);
457
-
458
- const polrLines = [];
459
- const polrCount = 60;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
 
461
- for(let i=0; i<polrCount; i++) {{
462
- const opacity = 1.0 - (i / (polrCount + 5));
463
- const color = `rgba(213, 0, 249, ${{opacity.toFixed(2)}})`;
464
-
465
- polrLines.push(
466
- priceChart.addLineSeries({{
467
- color: color,
468
- lineWidth: 1,
469
- crosshairMarkerVisible: false,
470
- lastValueVisible: false,
471
- priceLineVisible: false,
472
- title: ''
473
- }})
474
- );
475
- }}
476
-
477
- const priceSeries = priceChart.addLineSeries({{ color: '#2979ff', lineWidth: 2, title: 'Price' }});
478
- const predSeries = priceChart.addLineSeries({{ color: '#ffeb3b', lineWidth: 2, lineStyle: 2, title: 'Math Forecast' }});
479
 
480
- const candleChart = LightweightCharts.createChart(document.getElementById('tv-candles'), {{
481
- ...chartOpts,
482
- timeScale: {{ timeVisible: true, secondsVisible: false }}
483
- }});
484
- const candleSeries = candleChart.addCandlestickSeries({{
485
- upColor: '#00ff9d', downColor: '#ff3b3b', borderVisible: false, wickUpColor: '#00ff9d', wickDownColor: '#ff3b3b'
486
- }});
487
-
488
- const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{
489
- ...chartOpts, localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
490
- }});
491
- const netSeries = netChart.addHistogramSeries({{ color: '#2979ff' }});
492
-
493
- const volChart = LightweightCharts.createChart(document.getElementById('sidebar-vol'), {{
494
- ...chartOpts,
495
- grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }},
496
- rightPriceScale: {{ visible: false }},
497
- timeScale: {{ visible: false }},
498
- handleScroll: false, handleScale: false
499
- }});
500
- const volBuySeries = volChart.addHistogramSeries({{ color: '#00ff9d' }});
501
- const volSellSeries = volChart.addHistogramSeries({{ color: '#ff3b3b' }});
502
-
503
- const denChart = LightweightCharts.createChart(document.getElementById('sidebar-density'), {{
504
- ...chartOpts,
505
- grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }},
506
- rightPriceScale: {{ visible: false }},
507
- timeScale: {{ visible: false }},
508
- handleScroll: false, handleScale: false
509
- }});
510
- const bidSeries = denChart.addAreaSeries({{ lineColor: '#00ff9d', topColor: 'rgba(0, 255, 157, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
511
- const askSeries = denChart.addAreaSeries({{ lineColor: '#ff3b3b', topColor: 'rgba(255, 59, 59, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
512
-
513
- let activeLines = [];
514
- let activeCandleLines = [];
515
-
516
- new ResizeObserver(entries => {{
517
- for(let entry of entries) {{
518
- const {{width, height}} = entry.contentRect;
519
- if(entry.target.id === 'tv-price') priceChart.applyOptions({{width, height}});
520
- if(entry.target.id === 'tv-candles') candleChart.applyOptions({{width, height}});
521
- if(entry.target.id === 'tv-net') netChart.applyOptions({{width, height}});
522
- if(entry.target.id === 'sidebar-vol') volChart.applyOptions({{width, height}});
523
- if(entry.target.id === 'sidebar-density') denChart.applyOptions({{width, height}});
524
- }}
525
- }}).observe(document.body);
526
-
527
- function connect() {{
528
- const ws = new WebSocket((location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/ws');
529
 
530
- ws.onmessage = (e) => {{
531
- const data = JSON.parse(e.data);
532
- if (data.error) return;
533
-
534
- if (data.history.length) {{
535
- const hist = data.history.map(d => ({{ time: Math.floor(d.t), value: d.p }}));
536
- const cleanHist = [...new Map(hist.map(i => [i.time, i])).values()];
537
- priceSeries.setData(cleanHist);
538
-
539
- const lastP = cleanHist[cleanHist.length-1].value;
540
- const lastTime = cleanHist[cleanHist.length-1].time;
541
- dom.ticker.innerText = lastP.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
542
-
543
- if (data.analysis) {{
544
- const proj = data.analysis.projected;
545
- const rho = data.analysis.rho;
546
- predSeries.setData([
547
- cleanHist[cleanHist.length-1],
548
- {{ time: lastTime + 60, value: proj }}
549
- ]);
550
- const pct = ((proj - lastP) / lastP) * 100;
551
- const sign = pct >= 0 ? "+" : "";
552
- dom.projPct.innerText = `${{sign}}${{pct.toFixed(4)}}%`;
553
- dom.projPct.style.color = pct >= 0 ? "var(--green)" : "var(--red)";
554
- dom.projVal.innerText = proj.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
555
- dom.score.innerText = rho.toFixed(3);
556
- dom.score.style.color = rho > 0 ? "var(--green)" : (rho < 0 ? "var(--red)" : "var(--text-main)");
557
- }}
558
-
559
- if (data.polr && data.polr.length) {{
560
- data.polr.forEach((point, index) => {{
561
- if (index < polrLines.length) {{
562
- polrLines[index].update({{
563
- time: lastTime,
564
- value: point.p
565
- }});
566
- }}
567
- }});
568
- }}
569
- }}
570
-
571
- if (data.ohlc && data.ohlc.length) {{
572
- const candles = data.ohlc.map(c => ({{
573
- time: c.time,
574
- open: c.open,
575
- high: c.high,
576
- low: c.low,
577
- close: c.close
578
- }}));
579
- const uniqueCandles = [...new Map(candles.map(i => [i.time, i])).values()];
580
- candleSeries.setData(uniqueCandles);
581
- }}
582
-
583
- if (data.walls) {{
584
- activeLines.forEach(l => priceSeries.removePriceLine(l));
585
- activeLines = [];
586
- activeCandleLines.forEach(l => candleSeries.removePriceLine(l));
587
- activeCandleLines = [];
588
-
589
- let html = "";
590
- const addWall = (w, type) => {{
591
- const color = type === 'BID' ? '#00ff9d' : '#ff3b3b';
592
- const lineOpts = {{ price: w.price, color: color, lineWidth: 1, lineStyle: 2, axisLabelVisible: false }};
593
-
594
- activeLines.push(priceSeries.createPriceLine(lineOpts));
595
- activeCandleLines.push(candleSeries.createPriceLine(lineOpts));
596
-
597
- html += `<div class="list-item"><span style="color:${{color}}">${{type}} ${{w.price}}</span><span class="c-dim">Z:${{w.z_score.toFixed(1)}}</span></div>`;
598
- }};
599
- data.walls.asks.forEach(w => addWall(w, 'ASK'));
600
- data.walls.bids.forEach(w => addWall(w, 'BID'));
601
- dom.wallList.innerHTML = html || '<span class="c-dim" style="font-size:11px">Scanning...</span>';
602
- }}
603
-
604
- if (data.trade_history && data.trade_history.length) {{
605
- const buyData = [], sellData = [];
606
- data.trade_history.forEach(t => {{
607
- const time = Math.floor(t.t);
608
- buyData.push({{ time: time, value: t.buy }});
609
- sellData.push({{ time: time, value: t.sell }});
610
- }});
611
- volBuySeries.setData([...new Map(buyData.map(i => [i.time, i])).values()]);
612
- volSellSeries.setData([...new Map(sellData.map(i => [i.time, i])).values()]);
613
- }}
614
-
615
- if (data.depth_x.length) {{
616
- const bids = [], asks = [], nets = [];
617
- for(let i=0; i<data.depth_x.length; i++) {{
618
- const t = data.depth_x[i];
619
- bids.push({{ time: t, value: data.depth_bids[i] }});
620
- asks.push({{ time: t, value: data.depth_asks[i] }});
621
- nets.push({{ time: t, value: data.depth_net[i], color: data.depth_net[i] > 0 ? '#00ff9d' : '#ff3b3b' }});
622
- }}
623
- bidSeries.setData(bids);
624
- askSeries.setData(asks);
625
- netSeries.setData(nets);
626
- }}
627
- }};
628
- ws.onclose = () => setTimeout(connect, 2000);
629
- }}
630
- connect();
631
- }});
632
  </script>
633
  </body>
634
  </html>
635
  """
636
 
637
- async def kraken_worker():
638
- global market_state
639
- try:
640
- async with aiohttp.ClientSession() as session:
641
- url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
642
- async with session.get(url) as response:
643
- if response.status == 200:
644
- data = await response.json()
645
- if 'result' in data:
646
- for key in data['result']:
647
- if key != 'last':
648
- raw_candles = data['result'][key]
649
- market_state['ohlc_history'] = [
650
- {
651
- 'time': int(c[0]),
652
- 'open': float(c[1]),
653
- 'high': float(c[2]),
654
- 'low': float(c[3]),
655
- 'close': float(c[4])
656
- }
657
- for c in raw_candles[-120:]
658
- ]
659
- break
660
- except Exception as e:
661
- logging.error(f"History fetch failed: {e}")
662
-
663
- while True:
664
- try:
665
- async with websockets.connect("wss://ws.kraken.com/v2") as ws:
666
- logging.info(f"🔌 Connected to Kraken ({SYMBOL_KRAKEN})")
667
-
668
- await ws.send(json.dumps({
669
- "method": "subscribe",
670
- "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}
671
- }))
672
- await ws.send(json.dumps({
673
- "method": "subscribe",
674
- "params": {"channel": "trade", "symbol": [SYMBOL_KRAKEN]}
675
- }))
676
- await ws.send(json.dumps({
677
- "method": "subscribe",
678
- "params": {"channel": "ohlc", "symbol": [SYMBOL_KRAKEN], "interval": 1}
679
- }))
680
-
681
- async for message in ws:
682
- payload = json.loads(message)
683
- channel = payload.get("channel")
684
- data = payload.get("data", [])
685
-
686
- if channel == "book":
687
- for item in data:
688
- for bid in item.get('bids', []):
689
- q, p = float(bid['qty']), float(bid['price'])
690
- if q == 0: market_state['bids'].pop(p, None)
691
- else: market_state['bids'][p] = q
692
- for ask in item.get('asks', []):
693
- q, p = float(ask['qty']), float(ask['price'])
694
- if q == 0: market_state['asks'].pop(p, None)
695
- else: market_state['asks'][p] = q
696
-
697
- if market_state['bids'] and market_state['asks']:
698
- best_bid = max(market_state['bids'].keys())
699
- best_ask = min(market_state['asks'].keys())
700
- mid = (best_bid + best_ask) / 2
701
- market_state['prev_mid'] = market_state['current_mid']
702
- market_state['current_mid'] = mid
703
- market_state['ready'] = True
704
-
705
- now = time.time()
706
- if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
707
- market_state['history'].append({'t': now, 'p': mid})
708
- if len(market_state['history']) > HISTORY_LENGTH:
709
- market_state['history'].pop(0)
710
-
711
- elif channel == "trade":
712
- for trade in data:
713
- try:
714
- qty = float(trade['qty'])
715
- price = float(trade['price'])
716
- side = trade['side']
717
-
718
- if side == 'buy': market_state['current_vol_window']['buy'] += qty
719
- else: market_state['current_vol_window']['sell'] += qty
720
-
721
- current_minute_start = int(time.time()) // 60 * 60
722
-
723
- if market_state['ohlc_history']:
724
- last_candle = market_state['ohlc_history'][-1]
725
-
726
- if last_candle['time'] == current_minute_start:
727
- last_candle['close'] = price
728
- if price > last_candle['high']: last_candle['high'] = price
729
- if price < last_candle['low']: last_candle['low'] = price
730
-
731
- elif current_minute_start > last_candle['time']:
732
- new_candle = {
733
- 'time': current_minute_start,
734
- 'open': price,
735
- 'high': price,
736
- 'low': price,
737
- 'close': price
738
- }
739
- market_state['ohlc_history'].append(new_candle)
740
- if len(market_state['ohlc_history']) > 200:
741
- market_state['ohlc_history'].pop(0)
742
- except: pass
743
-
744
- elif channel == "ohlc":
745
- for candle in data:
746
- try:
747
- start_time = int(float(candle['endtime'])) - 60
748
- c_data = {
749
- 'time': start_time,
750
- 'open': float(candle['open']),
751
- 'high': float(candle['high']),
752
- 'low': float(candle['low']),
753
- 'close': float(candle['close'])
754
- }
755
-
756
- if market_state['ohlc_history']:
757
- if market_state['ohlc_history'][-1]['time'] == start_time:
758
- market_state['ohlc_history'][-1] = c_data
759
- elif market_state['ohlc_history'][-1]['time'] < start_time:
760
- market_state['ohlc_history'].append(c_data)
761
- if len(market_state['ohlc_history']) > 200:
762
- market_state['ohlc_history'].pop(0)
763
- except Exception as e:
764
- pass
765
 
766
- except Exception as e:
767
- logging.warning(f"⚠️ Reconnecting: {e}")
768
- await asyncio.sleep(3)
769
-
770
- async def broadcast_worker():
771
- while True:
772
- if connected_clients and market_state['ready']:
773
- payload = process_market_data()
774
- msg = json.dumps(payload)
775
- for ws in list(connected_clients):
776
- try: await ws.send_str(msg)
777
- except: pass
778
- await asyncio.sleep(BROADCAST_RATE)
779
-
780
- async def websocket_handler(request):
781
  ws = web.WebSocketResponse()
782
  await ws.prepare(request)
783
- connected_clients.add(ws)
 
784
  try:
785
- async for msg in ws:
786
- pass
787
  finally:
788
- connected_clients.remove(ws)
789
  return ws
790
 
791
- async def handle_index(request):
792
- return web.Response(text=HTML_PAGE, content_type='text/html')
793
 
794
- async def start_background(app):
795
- app['kraken_task'] = asyncio.create_task(kraken_worker())
796
- app['broadcast_task'] = asyncio.create_task(broadcast_worker())
 
797
 
798
- async def cleanup_background(app):
799
- app['kraken_task'].cancel()
800
  app['broadcast_task'].cancel()
801
- try: await app['kraken_task']; await app['broadcast_task']
802
- except: pass
803
 
804
- async def main():
805
  app = web.Application()
806
- app.router.add_get('/', handle_index)
807
- app.router.add_get('/ws', websocket_handler)
808
- app.on_startup.append(start_background)
809
- app.on_cleanup.append(cleanup_background)
810
- runner = web.AppRunner(app)
811
- await runner.setup()
812
- site = web.TCPSite(runner, '0.0.0.0', PORT)
813
- await site.start()
814
- print(f"🚀 Quant Dashboard: http://localhost:{PORT}")
815
- await asyncio.Event().wait()
816
 
817
  if __name__ == "__main__":
818
- try: asyncio.run(main())
819
- except KeyboardInterrupt: pass
 
 
 
 
 
 
 
2
  import json
3
  import logging
4
  import time
 
5
  import math
6
  import statistics
7
  import aiohttp
 
8
  from aiohttp import web
9
+ from collections import deque
10
+ from dataclasses import dataclass, field
11
+ from typing import Dict, List, Optional
12
+
13
+ # ==========================================
14
+ # CONFIGURATION & HYPERPARAMETERS
15
+ # ==========================================
16
+ SYMBOL_DISPLAY = "BTC/USD"
17
+ SYMBOL_KRAKEN = "BTC/USD" # Kraken WS V2 format
18
  PORT = 7860
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ # Quantitative Parameters
21
+ OFI_LOOKBACK = 50 # Number of updates to smooth OFI
22
+ LIQUIDITY_BANDS_USD = [10000, 50000, 150000, 500000] # Dynamic slippage tiers
23
+ DEPTH_BUCKETS = 100 # Resolution of depth chart
24
+ BROADCAST_INTERVAL = 0.1 # 100ms UI updates (10Hz)
25
+
26
+ # Logging Setup
27
+ logging.basicConfig(
28
+ level=logging.INFO,
29
+ format='%(asctime)s | %(levelname)s | %(message)s',
30
+ datefmt='%H:%M:%S'
31
+ )
32
+ logger = logging.getLogger("QuantEngine")
33
+
34
+ # ==========================================
35
+ # DATA STRUCTURES
36
+ # ==========================================
37
+
38
+ @dataclass
39
+ class MarketMicrostructure:
40
+ """Thread-safe container for market state."""
41
+ bids: Dict[float, float] = field(default_factory=dict)
42
+ asks: Dict[float, float] = field(default_factory=dict)
43
 
44
+ # State snapshots for OFI calculation
45
+ prev_best_bid: float = 0.0
46
+ prev_best_ask: float = 0.0
47
+ prev_bid_qty: float = 0.0
48
+ prev_ask_qty: float = 0.0
49
 
50
+ # Computed Metrics
51
+ mid_price: float = 0.0
52
+ spread: float = 0.0
53
+ ofi_rolling: float = 0.0
54
+ ofi_history: deque = field(default_factory=lambda: deque(maxlen=OFI_LOOKBACK))
55
+
56
+ # Trade Data
57
+ last_trade_price: float = 0.0
58
+ trade_pressure: float = 0.0 # VWP equivalent
59
+
60
+ ready: bool = False
61
+
62
+ def get_sorted_book(self):
63
+ """Returns sorted lists of (price, qty) tuples."""
64
+ # Sorting is expensive, do it only when necessary or optimize with b-trees in C++
65
+ # For Python/Websockets, standard timsort is sufficient for <1000 items
66
+ b_sorted = sorted(self.bids.items(), key=lambda x: -x[0])
67
+ a_sorted = sorted(self.asks.items(), key=lambda x: x[0])
68
+ return b_sorted, a_sorted
69
+
70
+ state = MarketMicrostructure()
71
+
72
+ # ==========================================
73
+ # QUANTITATIVE ALGORITHMS
74
+ # ==========================================
75
+
76
+ class QuantEngine:
77
+ @staticmethod
78
+ def calculate_ofi(best_bid, bid_qty, best_ask, ask_qty):
79
+ """
80
+ Order Flow Imbalance (OFI) Calculation.
81
+ Formula based on Cont, Kukanov, Stoikov (2014).
82
+ Measures net supply/demand changes at the best quotes.
83
+ """
84
+ if state.prev_best_bid == 0: return 0.0
85
+
86
+ # Bid Contribution
87
+ e_b = 0.0
88
+ if best_bid > state.prev_best_bid:
89
+ e_b = bid_qty
90
+ elif best_bid < state.prev_best_bid:
91
+ e_b = -state.prev_bid_qty
92
+ else:
93
+ e_b = bid_qty - state.prev_bid_qty
94
+
95
+ # Ask Contribution
96
+ e_a = 0.0
97
+ if best_ask > state.prev_best_ask:
98
+ e_a = state.prev_ask_qty
99
+ elif best_ask < state.prev_best_ask:
100
+ e_a = -ask_qty
101
+ else:
102
+ e_a = state.prev_ask_qty - ask_qty
103
+
104
+ return e_b - e_a
105
+
106
+ @staticmethod
107
+ def calculate_liquidity_bands(bids, asks, mid_price):
108
+ """
109
+ Path of Least Resistance (POLR) v2.
110
+ Calculates the price levels required to sweep specific USD amounts (Liquidity Bands).
111
+ Returns a set of price points representing dynamic support/resistance.
112
+ """
113
+ bands = {'bids': [], 'asks': []}
114
+
115
+ # Calculate Ask Bands (Resistance)
116
+ current_cost = 0.0
117
+ current_vol = 0.0
118
+ ask_ptr = 0
119
+
120
+ for target_usd in LIQUIDITY_BANDS_USD:
121
+ while ask_ptr < len(asks):
122
+ p, q = asks[ask_ptr]
123
+ cost = p * q
124
+ if current_cost + cost >= target_usd:
125
+ # Interpolate exact price for remaining amount
126
+ remaining = target_usd - current_cost
127
+ bands['asks'].append(p) # Approx
128
+ current_cost += remaining # Cap it here
129
+ break
130
+ current_cost += cost
131
+ ask_ptr += 1
132
+ if ask_ptr >= len(asks):
133
+ bands['asks'].append(asks[-1][0])
134
+
135
+ # Calculate Bid Bands (Support)
136
+ current_cost = 0.0
137
+ bid_ptr = 0
138
+ for target_usd in LIQUIDITY_BANDS_USD:
139
+ while bid_ptr < len(bids):
140
+ p, q = bids[bid_ptr]
141
+ cost = p * q
142
+ if current_cost + cost >= target_usd:
143
+ bands['bids'].append(p)
144
+ current_cost += remaining
145
+ break
146
+ current_cost += cost
147
+ bid_ptr += 1
148
+ if bid_ptr >= len(bids):
149
+ bands['bids'].append(bids[-1][0])
150
+
151
+ return bands
152
+
153
+ @staticmethod
154
+ def aggregate_depth(bids, asks, mid):
155
+ """Buckets order book depth for efficient frontend rendering."""
156
+ # Simple decimation for visualization
157
+ if not bids or not asks: return [], [], []
158
+
159
+ range_pct = 0.02 # 2% depth
160
+ min_p = mid * (1 - range_pct)
161
+ max_p = mid * (1 + range_pct)
162
+
163
+ chart_bids = []
164
  cum_vol = 0
165
+ for p, q in bids:
166
+ if p < min_p: break
167
  cum_vol += q
168
+ chart_bids.append({'p': p, 'v': cum_vol})
169
+
170
+ chart_asks = []
 
 
 
171
  cum_vol = 0
172
+ for p, q in asks:
173
+ if p > max_p: break
174
  cum_vol += q
175
+ chart_asks.append({'p': p, 'v': cum_vol})
176
+
177
+ # Downsample to N points
178
+ def downsample(data, n):
179
+ if not data: return []
180
+ if len(data) <= n: return data
181
+ step = len(data) / n
182
+ return [data[int(i * step)] for i in range(n)]
183
 
184
+ return downsample(chart_bids, DEPTH_BUCKETS), downsample(chart_asks, DEPTH_BUCKETS)
 
185
 
186
+ # ==========================================
187
+ # ASYNC WORKERS
188
+ # ==========================================
 
 
 
 
 
 
 
189
 
190
+ async def ingestion_worker():
191
+ """Handles WebSocket connection to Kraken and updates Market State."""
192
+ while True:
193
+ try:
194
+ async with aiohttp.ClientSession() as session:
195
+ async with session.ws_connect("wss://ws.kraken.com/v2") as ws:
196
+ logger.info(f"🔌 Connected to Kraken V2: {SYMBOL_KRAKEN}")
197
+
198
+ # Subscribe to Book and Trade
199
+ await ws.send_json({
200
+ "method": "subscribe",
201
+ "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}
202
+ })
203
+ await ws.send_json({
204
+ "method": "subscribe",
205
+ "params": {"channel": "trade", "symbol": [SYMBOL_KRAKEN]}
206
+ })
207
+
208
+ async for msg in ws:
209
+ payload = json.loads(msg.data)
210
+ channel = payload.get("channel")
211
+
212
+ if channel == "book":
213
+ data = payload.get("data", [])[0]
214
+ # Is this a snapshot or update? V2 usually sends snapshot first
215
+ # For simplified handling here, we assume standard dict updates
216
+
217
+ # Update Bids
218
+ for bid in data.get('bids', []):
219
+ price, qty = float(bid['price']), float(bid['qty'])
220
+ if qty == 0: state.bids.pop(price, None)
221
+ else: state.bids[price] = qty
222
+
223
+ # Update Asks
224
+ for ask in data.get('asks', []):
225
+ price, qty = float(ask['price']), float(ask['qty'])
226
+ if qty == 0: state.asks.pop(price, None)
227
+ else: state.asks[price] = qty
228
+
229
+ # --- QUANTITATIVE UPDATE TRIGGER ---
230
+ if state.bids and state.asks:
231
+ # 1. Get Sorted Top of Book
232
+ b_sorted, a_sorted = state.get_sorted_book()
233
+ best_bid, best_bid_qty = b_sorted[0]
234
+ best_ask, best_ask_qty = a_sorted[0]
235
+
236
+ # 2. Calculate Mid & Spread
237
+ state.mid_price = (best_bid + best_ask) / 2
238
+ state.spread = best_ask - best_bid
239
+
240
+ # 3. Calculate OFI (Microstructure Alpha)
241
+ ofi_val = QuantEngine.calculate_ofi(
242
+ best_bid, best_bid_qty,
243
+ best_ask, best_ask_qty
244
+ )
245
+ state.ofi_history.append(ofi_val)
246
+ state.ofi_rolling = statistics.mean(state.ofi_history) if state.ofi_history else 0
247
+
248
+ # 4. Update State for next tick
249
+ state.prev_best_bid = best_bid
250
+ state.prev_best_ask = best_ask
251
+ state.prev_bid_qty = best_bid_qty
252
+ state.prev_ask_qty = best_ask_qty
253
+
254
+ state.ready = True
255
+
256
+ elif channel == "trade":
257
+ trades = payload.get("data", [])
258
+ for t in trades:
259
+ state.last_trade_price = float(t['price'])
260
+ qty = float(t['qty'])
261
+ side = t['side'] # 'buy' or 'sell'
262
+
263
+ # Simple Buying/Selling Pressure Oscillator
264
+ direction = 1 if side == 'buy' else -1
265
+ state.trade_pressure = (state.trade_pressure * 0.95) + (direction * qty * 0.05)
266
 
267
+ except Exception as e:
268
+ logger.error(f"Ingestion Error: {e}")
269
+ state.ready = False
270
+ await asyncio.sleep(5)
271
 
272
+ async def broadcast_worker(app):
273
+ """Pushes analyzed metrics to frontend at fixed intervals."""
274
+ while True:
275
+ start_t = time.time()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
 
277
+ if state.ready and app['websockets']:
278
+ try:
279
+ # 1. Snapshot Data
280
+ bids, asks = state.get_sorted_book()
281
+ mid = state.mid_price
282
+
283
+ # 2. Run Heavy Analysis
284
+ liq_bands = QuantEngine.calculate_liquidity_bands(bids, asks, mid)
285
+ depth_b, depth_a = QuantEngine.aggregate_depth(bids, asks, mid)
286
+
287
+ # 3. Construct Payload
288
+ payload = {
289
+ "t": time.time(),
290
+ "mid": mid,
291
+ "spread": state.spread,
292
+ "trade_p": state.last_trade_price,
293
+ "ofi": state.ofi_rolling,
294
+ "pressure": state.trade_pressure,
295
+ "bands": liq_bands,
296
+ "depth": {"bids": depth_b, "asks": depth_a}
297
+ }
298
+
299
+ # 4. Broadcast
300
+ msg = json.dumps(payload)
301
+ for ws in set(app['websockets']):
302
+ await ws.send_str(msg)
303
+
304
+ except Exception as e:
305
+ logger.error(f"Broadcast Logic Error: {e}")
306
+
307
+ # Sleep remaining time to maintain Hz
308
+ elapsed = time.time() - start_t
309
+ wait = max(0, BROADCAST_INTERVAL - elapsed)
310
+ await asyncio.sleep(wait)
311
+
312
+ # ==========================================
313
+ # FRONTEND (EMBEDDED HTML/JS)
314
+ # ==========================================
315
 
316
+ HTML_TEMPLATE = """
317
  <!DOCTYPE html>
318
  <html lang="en">
319
  <head>
320
  <meta charset="UTF-8">
321
+ <title>QUANT // COMMAND</title>
322
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
 
323
  <style>
324
+ :root {
325
+ --bg-dark: #050505;
326
+ --bg-panel: #0E0E0E;
327
+ --border: #1F1F1F;
328
+ --text-main: #EAEAEA;
329
+ --text-dim: #666666;
330
+ --accent-green: #00F0FF; /* Cyber Cyan */
331
+ --accent-red: #FF2A6D; /* Neon Red */
332
+ --accent-yellow: #FAFF00;
333
+ --accent-band: rgba(255, 255, 255, 0.08);
334
+ }
335
+ * { box-sizing: border-box; }
336
+ body { margin: 0; background: var(--bg-dark); color: var(--text-main); font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; overflow: hidden; height: 100vh; width: 100vw; }
337
+
338
+ /* GRID LAYOUT */
339
+ .grid-container {
340
+ display: grid;
341
+ grid-template-columns: 3fr 1fr;
342
+ grid-template-rows: 40px 2fr 1fr;
343
+ height: 100vh;
 
 
 
 
344
  gap: 1px;
345
+ background-color: var(--border);
346
+ }
 
 
 
347
 
348
+ .header { grid-column: 1 / 3; background: var(--bg-panel); display: flex; align-items: center; padding: 0 15px; border-bottom: 1px solid var(--border); justify-content: space-between; }
349
+ .main-chart { grid-column: 1 / 2; grid-row: 2 / 3; background: var(--bg-panel); position: relative; }
350
+ .sub-chart { grid-column: 1 / 2; grid-row: 3 / 4; background: var(--bg-panel); position: relative; }
351
+ .sidebar { grid-column: 2 / 3; grid-row: 2 / 4; background: var(--bg-panel); padding: 10px; display: flex; flex-direction: column; gap: 10px; border-left: 1px solid var(--border); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
 
353
+ /* TYPOGRAPHY */
354
+ .title { font-weight: 800; letter-spacing: 1px; font-size: 14px; color: var(--text-main); }
355
+ .live-tag { background: var(--accent-red); color: #000; font-weight: bold; font-size: 10px; padding: 2px 6px; border-radius: 2px; margin-right: 10px; }
356
+ .metric-box { background: rgba(255,255,255,0.03); padding: 10px; border-radius: 4px; border: 1px solid var(--border); }
357
+ .metric-label { font-size: 10px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; display: block; margin-bottom: 4px; }
358
+ .metric-val { font-family: 'Courier New', monospace; font-size: 18px; font-weight: 700; color: #FFF; }
359
+ .small-chart-container { height: 150px; width: 100%; margin-top: auto; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
 
361
+ /* UTILS */
362
+ .c-up { color: var(--accent-green); }
363
+ .c-down { color: var(--accent-red); }
364
+ .chart-overlay-info { position: absolute; top: 10px; left: 10px; z-index: 10; font-family: monospace; font-size: 11px; background: rgba(0,0,0,0.5); padding: 5px; pointer-events: none; }
 
 
 
 
 
 
 
 
365
  </style>
366
  </head>
367
  <body>
 
 
 
 
 
 
 
 
 
368
 
369
+ <div class="grid-container">
370
+ <div class="header">
371
+ <div><span class="live-tag">LIVE</span> <span class="title">KRAKEN // QUANTITATIVE MONITOR // BTC-USD</span></div>
372
+ <div style="font-family: monospace; font-size: 12px; color: var(--text-dim);" id="connection-status">DISCONNECTED</div>
 
373
  </div>
374
 
375
+ <div class="main-chart" id="chart-price">
376
+ <div class="chart-overlay-info">
377
+ MID: <span id="val-mid">---</span><br>
378
+ SPREAD: <span id="val-spread">---</span>
 
 
 
 
379
  </div>
380
  </div>
381
+
382
+ <div class="sub-chart" id="chart-ofi">
383
+ <div class="chart-overlay-info">ORDER FLOW IMBALANCE (Rolling)</div>
384
+ </div>
385
 
386
+ <div class="sidebar">
387
+ <div class="metric-box">
388
+ <span class="metric-label">Liquidity Cost (Ask)</span>
389
+ <span class="metric-val" id="val-ask-liq">---</span>
 
 
 
 
 
 
 
 
 
 
 
390
  </div>
391
+ <div class="metric-box">
392
+ <span class="metric-label">Liquidity Cost (Bid)</span>
393
+ <span class="metric-val" id="val-bid-liq">---</span>
 
 
 
 
 
394
  </div>
395
+ <div class="metric-box">
396
+ <span class="metric-label">Net Pressure</span>
397
+ <span class="metric-val" id="val-pressure">0.00</span>
 
398
  </div>
399
+
400
+ <div style="flex:1; display:flex; flex-direction:column;">
401
+ <span class="metric-label">REAL-TIME DEPTH</span>
402
+ <div id="chart-depth" style="flex:1; width:100%;"></div>
403
  </div>
404
  </div>
405
  </div>
406
 
407
  <script>
408
+ const CONFIG = {
409
+ colors: {
410
+ bg: '#0E0E0E',
411
+ grid: '#1F1F1F',
412
+ text: '#666',
413
+ green: '#00F0FF',
414
+ red: '#FF2A6D',
415
+ band: 'rgba(255, 255, 255, 0.05)'
416
+ }
417
+ };
418
+
419
+ // --- CHART INITIALIZATION ---
420
+ const chartOpts = {
421
+ layout: { background: { type: 'solid', color: CONFIG.colors.bg }, textColor: '#888' },
422
+ grid: { vertLines: { color: CONFIG.colors.grid }, horzLines: { color: CONFIG.colors.grid } },
423
+ crosshair: { mode: 1 },
424
+ timeScale: { timeVisible: true, secondsVisible: true }
425
+ };
426
+
427
+ // 1. Price Chart
428
+ const priceChart = LightweightCharts.createChart(document.getElementById('chart-price'), chartOpts);
429
+ const midSeries = priceChart.addLineSeries({ color: '#FFF', lineWidth: 1, title: 'Mid' });
430
+ const tradeSeries = priceChart.addAreaSeries({
431
+ topColor: 'rgba(0, 240, 255, 0.1)', bottomColor: 'rgba(0,0,0,0)',
432
+ lineColor: CONFIG.colors.green, lineWidth: 2, title: 'Last Trade'
433
+ });
434
+
435
+ // POLR Bands (4 levels)
436
+ const bandSeries = [];
437
+ for(let i=0; i<4; i++) {
438
+ bandSeries.push({
439
+ bid: priceChart.addLineSeries({ color: '#2b2b2b', lineWidth: 1, lineStyle: 2, lastValueVisible: false }),
440
+ ask: priceChart.addLineSeries({ color: '#2b2b2b', lineWidth: 1, lineStyle: 2, lastValueVisible: false })
441
+ });
442
+ }
443
+
444
+ // 2. OFI Chart
445
+ const ofiChart = LightweightCharts.createChart(document.getElementById('chart-ofi'), {
446
+ ...chartOpts,
447
+ rightPriceScale: { scaleMargins: { top: 0.1, bottom: 0.1 } }
448
+ });
449
+ const ofiSeries = ofiChart.addHistogramSeries({ color: '#26a69a' });
450
+
451
+ // 3. Depth Chart (Sidebar)
452
+ const depthChart = LightweightCharts.createChart(document.getElementById('chart-depth'), {
453
+ layout: { background: { type: 'solid', color: 'transparent' }, textColor: '#444' },
454
+ grid: { visible: false, vertLines: { visible: false }, horzLines: { visible: false } },
455
+ rightPriceScale: { visible: false },
456
+ timeScale: { visible: false },
457
+ crosshair: { visible: false },
458
+ handleScroll: false, handleScale: false
459
+ });
460
+ const depthBidSeries = depthChart.addAreaSeries({ lineColor: CONFIG.colors.green, topColor: 'rgba(0, 240, 255, 0.2)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 });
461
+ const depthAskSeries = depthChart.addAreaSeries({ lineColor: CONFIG.colors.red, topColor: 'rgba(255, 42, 109, 0.2)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 });
462
+
463
+ // --- RESIZING ---
464
+ new ResizeObserver(entries => {
465
+ for (let entry of entries) {
466
+ const { width, height } = entry.contentRect;
467
+ if (entry.target.id === 'chart-price') priceChart.applyOptions({ width, height });
468
+ if (entry.target.id === 'chart-ofi') ofiChart.applyOptions({ width, height });
469
+ if (entry.target.id === 'chart-depth') depthChart.applyOptions({ width, height });
470
+ }
471
+ }).observe(document.body);
472
+
473
+ // --- SYNC ---
474
+ // Simple time-sync logic could be added here, but tricky with different scale types (Price vs Histogram)
475
+
476
+ // --- WEBSOCKET ---
477
+ const connect = () => {
478
+ const ws = new WebSocket(`ws://${location.host}/ws`);
479
+ const status = document.getElementById('connection-status');
480
 
481
+ ws.onopen = () => { status.innerText = 'CONNECTED'; status.style.color = CONFIG.colors.green; };
482
+ ws.onclose = () => { status.innerText = 'RECONNECTING...'; status.style.color = CONFIG.colors.red; setTimeout(connect, 2000); };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
 
484
+ ws.onmessage = (event) => {
485
+ const data = JSON.parse(event.data);
486
+ const t = data.t; // timestamp
487
+
488
+ // 1. Update Price
489
+ midSeries.update({ time: t, value: data.mid });
490
+ tradeSeries.update({ time: t, value: data.trade_p });
491
+
492
+ // 2. Update Bands
493
+ if (data.bands) {
494
+ data.bands.bids.forEach((p, i) => { if(bandSeries[i]) bandSeries[i].bid.update({ time: t, value: p }); });
495
+ data.bands.asks.forEach((p, i) => { if(bandSeries[i]) bandSeries[i].ask.update({ time: t, value: p }); });
496
+ }
497
+
498
+ // 3. Update OFI
499
+ const color = data.ofi >= 0 ? CONFIG.colors.green : CONFIG.colors.red;
500
+ ofiSeries.update({ time: t, value: data.ofi, color: color });
501
+
502
+ // 4. Update Depth (Snapshot mode, re-mapping x-axis to indices for smooth look)
503
+ if (data.depth) {
504
+ // Depth chart doesn't use time, just simple index 0..100
505
+ const bidData = data.depth.bids.reverse().map((d, i) => ({ time: i, value: d.v }));
506
+ const askData = data.depth.asks.map((d, i) => ({ time: i + 100, value: d.v }));
507
+ depthBidSeries.setData(bidData);
508
+ depthAskSeries.setData(askData);
509
+ depthChart.timeScale().fitContent();
510
+ }
511
+
512
+ // 5. DOM Updates
513
+ document.getElementById('val-mid').innerText = data.mid.toFixed(1);
514
+ document.getElementById('val-spread').innerText = data.spread.toFixed(1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
 
516
+ const pressEl = document.getElementById('val-pressure');
517
+ pressEl.innerText = data.pressure.toFixed(4);
518
+ pressEl.style.color = data.pressure > 0 ? CONFIG.colors.green : CONFIG.colors.red;
519
+
520
+ // Simple distance to first band calc
521
+ if(data.bands.asks.length) document.getElementById('val-ask-liq').innerText = (data.bands.asks[0] - data.mid).toFixed(2);
522
+ if(data.bands.bids.length) document.getElementById('val-bid-liq').innerText = (data.mid - data.bands.bids[0]).toFixed(2);
523
+ };
524
+ };
525
+
526
+ connect();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  </script>
528
  </body>
529
  </html>
530
  """
531
 
532
+ # ==========================================
533
+ # WEBSERVER SETUP
534
+ # ==========================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
535
 
536
+ async def ws_handler(request):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
  ws = web.WebSocketResponse()
538
  await ws.prepare(request)
539
+
540
+ request.app['websockets'].add(ws)
541
  try:
542
+ async for msg in ws: pass
 
543
  finally:
544
+ request.app['websockets'].discard(ws)
545
  return ws
546
 
547
+ async def index_handler(request):
548
+ return web.Response(text=HTML_TEMPLATE, content_type='text/html')
549
 
550
+ async def on_startup(app):
551
+ app['websockets'] = set()
552
+ app['ingestion_task'] = asyncio.create_task(ingestion_worker())
553
+ app['broadcast_task'] = asyncio.create_task(broadcast_worker(app))
554
 
555
+ async def on_cleanup(app):
556
+ app['ingestion_task'].cancel()
557
  app['broadcast_task'].cancel()
558
+ for ws in app['websockets']:
559
+ await ws.close()
560
 
561
+ def main():
562
  app = web.Application()
563
+ app.router.add_get('/', index_handler)
564
+ app.router.add_get('/ws', ws_handler)
565
+ app.on_startup.append(on_startup)
566
+ app.on_cleanup.append(on_cleanup)
567
+
568
+ print(f"🚀 QUANT SUITE ACTIVE: http://localhost:{PORT}")
569
+ web.run_app(app, port=PORT, print=None)
 
 
 
570
 
571
  if __name__ == "__main__":
572
+ try:
573
+ # Windows selector event loop policy fix
574
+ import sys
575
+ if sys.platform == 'win32':
576
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
577
+ main()
578
+ except KeyboardInterrupt:
579
+ pass