Alvin3y1 commited on
Commit
1533a4b
·
verified ·
1 Parent(s): 6bf2975

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +499 -160
app.py CHANGED
@@ -2,6 +2,9 @@ import asyncio
2
  import json
3
  import logging
4
  import time
 
 
 
5
  import aiohttp
6
  from aiohttp import web
7
  import websockets
@@ -9,107 +12,215 @@ import websockets
9
  SYMBOL_KRAKEN = "BTC/USD"
10
  PORT = 7860
11
  HISTORY_LENGTH = 300
12
- BROADCAST_RATE = 0.1 # 100ms Updates (High Frequency)
 
 
 
 
 
 
 
 
 
13
 
14
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  market_state = {
17
  "bids": {},
18
  "asks": {},
19
  "history": [],
20
- "ofi_history": [], # Stores the Cumulative OFI
 
21
  "trade_vol_history": [],
22
  "ohlc_history": [],
23
  "current_vol_window": {"buy": 0.0, "sell": 0.0, "start": time.time()},
24
  "current_mid": 0.0,
25
- "prev_book": None, # To track t-1 state for CKS model
26
- "cumulative_ofi": 0.0, # The running total of Order Flow Imbalance
27
  "ready": False
28
  }
29
 
30
  connected_clients = set()
31
 
32
- def calculate_ofi(best_bid_p, best_bid_q, best_ask_p, best_ask_q):
33
- """
34
- Cont-Kukanov-Stoikov (CKS) Order Flow Imbalance Model.
35
- e_n = I(b_n >= b_{n-1})*q_n^b - I(b_n <= b_{n-1})*q_{n-1}^b ...
36
- """
37
- prev = market_state['prev_book']
38
- if not prev:
39
- return 0.0
40
-
41
- ofi_delta = 0.0
42
-
43
- # --- BID SIDE IMPACT (Buying Pressure) ---
44
- if best_bid_p > prev['bp']:
45
- # Price Improvement: Aggressive Buy (Add full current qty)
46
- ofi_delta += best_bid_q
47
- elif best_bid_p < prev['bp']:
48
- # Price Drop: Support Pulled (Subtract previous qty)
49
- ofi_delta -= prev['bq']
50
- else:
51
- # Price Same: Net change in liquidity
52
- # If Qty increases, Support added (+). If Qty decreases, Support pulled (-).
53
- ofi_delta += (best_bid_q - prev['bq'])
54
-
55
- # --- ASK SIDE IMPACT (Selling Pressure) ---
56
- # Note: Ask logic is inverted. Higher Ask = Less Pressure (Bullish), Lower Ask = More Pressure (Bearish)
57
- # We subtract Ask Impact from Total OFI.
58
-
59
- ask_impact = 0.0
60
- if best_ask_p < prev['ap']:
61
- # Price Drop: Aggressive Sell (Add full current qty to sell pressure)
62
- ask_impact += best_ask_q
63
- elif best_ask_p > prev['ap']:
64
- # Price Rise: Resistance Removed (Subtract previous qty from sell pressure)
65
- ask_impact -= prev['aq']
66
- else:
67
- # Price Same: Net change in liquidity
68
- # If Qty increases, Resistance added (+). If Qty decreases, Resistance removed (-).
69
- ask_impact += (best_ask_q - prev['aq'])
70
-
71
- # Net OFI = Bid Impact - Ask Impact
72
- return ofi_delta - ask_impact
73
-
74
- def process_market_data():
75
- if not market_state['ready']: return {"error": "Initializing..."}
76
 
77
- # 1. Sort Book to get Best Bid/Ask
78
- sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
79
- sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
- if not sorted_bids or not sorted_asks: return {"error": "Empty Book"}
 
 
 
82
 
83
- best_bid_p, best_bid_q = sorted_bids[0]
84
- best_ask_p, best_ask_q = sorted_asks[0]
85
- mid = (best_bid_p + best_ask_p) / 2
86
- market_state['current_mid'] = mid
87
 
88
- now = time.time()
 
89
 
90
- # 2. CKS OFI Calculation
91
- ofi_step = calculate_ofi(best_bid_p, best_bid_q, best_ask_p, best_ask_q)
92
- market_state['cumulative_ofi'] += ofi_step
93
 
94
- # Store state for next tick (t-1)
95
- market_state['prev_book'] = {
96
- 'bp': best_bid_p, 'bq': best_bid_q,
97
- 'ap': best_ask_p, 'aq': best_ask_q
98
- }
99
-
100
- # 3. History Management
101
- if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
102
- # Price History
103
- market_state['history'].append({'t': now, 'p': mid})
104
- if len(market_state['history']) > HISTORY_LENGTH:
105
- market_state['history'].pop(0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
- # OFI History (Synced with price updates)
108
- market_state['ofi_history'].append({'t': now, 'v': market_state['cumulative_ofi']})
109
- if len(market_state['ofi_history']) > HISTORY_LENGTH:
110
- market_state['ofi_history'].pop(0)
 
 
111
 
112
- # 4. Volume Window Logic
 
 
113
  if now - market_state['current_vol_window']['start'] >= 1.0:
114
  market_state['trade_vol_history'].append({
115
  't': now,
@@ -120,17 +231,100 @@ def process_market_data():
120
  market_state['trade_vol_history'].pop(0)
121
  market_state['current_vol_window'] = {"buy": 0.0, "sell": 0.0, "start": now}
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  return {
124
  "mid": mid,
125
  "history": market_state['history'],
126
- "ofi": market_state['ofi_history'],
 
 
127
  "trade_history": market_state['trade_vol_history'],
128
  "ohlc": market_state['ohlc_history'],
129
- "stats": {
130
- "ofi_val": market_state['cumulative_ofi'],
131
- "bid_depth": sum(q for p, q in sorted_bids[:10]),
132
- "ask_depth": sum(q for p, q in sorted_asks[:10])
133
- }
 
134
  }
135
 
136
  HTML_PAGE = f"""
@@ -138,7 +332,7 @@ HTML_PAGE = f"""
138
  <html lang="en">
139
  <head>
140
  <meta charset="UTF-8">
141
- <title>{SYMBOL_KRAKEN} | Institutional OFI</title>
142
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
143
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
144
  <style>
@@ -152,6 +346,8 @@ HTML_PAGE = f"""
152
  --red: #ff3b3b;
153
  --blue: #2979ff;
154
  --yellow: #ffeb3b;
 
 
155
  }}
156
  body {{
157
  margin: 0; padding: 0;
@@ -194,11 +390,13 @@ HTML_PAGE = f"""
194
 
195
  #p-bottom {{
196
  grid-column: 1 / 2; grid-row: 3 / 4;
197
- display: flex;
198
- flex-direction: column;
199
- background: var(--bg-panel);
 
200
  }}
201
-
 
202
  #p-sidebar {{
203
  grid-column: 2 / 3;
204
  grid-row: 2 / 4;
@@ -228,11 +426,25 @@ HTML_PAGE = f"""
228
  .label {{ font-size: 10px; color: var(--text-dim); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }}
229
  .value {{ font-family: 'JetBrains Mono', monospace; font-size: 20px; font-weight: 700; color: #fff; }}
230
  .value-lg {{ font-size: 26px; }}
231
-
 
232
  .divider {{ height: 1px; background: var(--border); width: 100%; }}
233
  .c-green {{ color: var(--green); }}
234
  .c-red {{ color: var(--red); }}
235
- .c-yellow {{ color: var(--yellow); }}
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
  .sidebar-chart-box {{
238
  flex: 1;
@@ -261,42 +473,62 @@ HTML_PAGE = f"""
261
 
262
  <div id="p-chart" class="panel">
263
  <div class="chart-header">
264
- PRICE (BLUE - RHS) vs <span class="c-yellow">CUMULATIVE OFI (YELLOW - LHS)</span>
265
  </div>
266
  <div id="tv-price" style="flex: 1; width: 100%;"></div>
267
  </div>
268
 
269
- <div id="p-bottom" class="panel">
270
- <div class="chart-header">1M KLINE (KRAKEN OHLC)</div>
271
- <div id="tv-candles" style="flex: 1; width: 100%;"></div>
 
 
 
 
 
 
272
  </div>
273
 
274
  <div id="p-sidebar" class="panel">
275
 
276
  <div class="data-group">
277
- <span class="label">Net OFI (Buying Pressure)</span>
278
- <span id="ofi-val" class="value value-lg c-yellow">---</span>
 
 
 
 
 
 
 
 
279
  </div>
280
 
281
  <div class="divider"></div>
282
 
283
  <div class="data-group">
284
- <span class="label">Interpretation</span>
285
- <div style="font-size: 11px; color: #888; line-height: 1.4;">
286
- <span class="c-yellow">Yellow Line</span> tracks Order Flow Imbalance.
287
- <br><br>
288
- <b>Divergence:</b><br>
289
- Yellow UP + Price Flat = <span class="c-green">BULLISH COIL</span><br>
290
- Yellow DOWN + Price Flat = <span class="c-red">BEARISH DIST</span>
291
- </div>
292
  </div>
293
 
294
  <div class="divider"></div>
295
 
 
 
 
 
 
 
 
296
  <div class="sidebar-chart-box">
297
  <span class="label" style="margin-bottom:4px;">Real-time Volume Ticks</span>
298
  <div id="sidebar-vol" class="mini-chart"></div>
299
  </div>
 
 
 
 
 
300
  </div>
301
  </div>
302
 
@@ -309,45 +541,62 @@ HTML_PAGE = f"""
309
  document.addEventListener('DOMContentLoaded', () => {{
310
  const dom = {{
311
  ticker: document.getElementById('price-ticker'),
312
- ofiVal: document.getElementById('ofi-val')
 
 
 
 
313
  }};
314
 
315
- // --- MASTER CHART (Dual Axis) ---
316
- const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), {{
317
  layout: {{ background: {{ type: 'solid', color: '#0a0a0a' }}, textColor: '#888', fontFamily: 'JetBrains Mono' }},
318
  grid: {{ vertLines: {{ color: '#151515' }}, horzLines: {{ color: '#151515' }} }},
319
- rightPriceScale: {{ visible: true, borderColor: '#2979ff' }}, // Price on Right
320
- leftPriceScale: {{ visible: true, borderColor: '#ffeb3b' }}, // OFI on Left
321
  timeScale: {{ borderColor: '#222', timeVisible: true, secondsVisible: true }},
322
- }});
323
-
324
- // Price Series (Blue, Right Axis)
325
- const priceSeries = priceChart.addLineSeries({{
326
- color: '#2979ff', lineWidth: 2, title: 'Price',
327
- priceScaleId: 'right'
328
- }});
329
 
330
- // OFI Series (Yellow, Left Axis)
331
- const ofiSeries = priceChart.addLineSeries({{
332
- color: '#ffeb3b', lineWidth: 2, title: 'Cumulative OFI',
333
- priceScaleId: 'left',
334
- lastValueVisible: true
335
- }});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
- // --- CANDLE CHART ---
 
 
 
338
  const candleChart = LightweightCharts.createChart(document.getElementById('tv-candles'), {{
339
- layout: {{ background: {{ type: 'solid', color: '#0a0a0a' }}, textColor: '#888', fontFamily: 'JetBrains Mono' }},
340
- grid: {{ vertLines: {{ color: '#151515' }}, horzLines: {{ color: '#151515' }} }},
341
- timeScale: {{ timeVisible: true, secondsVisible: false }},
342
  }});
343
  const candleSeries = candleChart.addCandlestickSeries({{
344
  upColor: '#00ff9d', downColor: '#ff3b3b', borderVisible: false, wickUpColor: '#00ff9d', wickDownColor: '#ff3b3b'
345
  }});
346
 
347
- // --- VOLUME CHART ---
 
 
 
 
348
  const volChart = LightweightCharts.createChart(document.getElementById('sidebar-vol'), {{
349
- layout: {{ background: {{ type: 'solid', color: '#0a0a0a' }}, textColor: '#888' }},
350
- grid: {{ visible: false }},
351
  rightPriceScale: {{ visible: false }},
352
  timeScale: {{ visible: false }},
353
  handleScroll: false, handleScale: false
@@ -355,13 +604,27 @@ HTML_PAGE = f"""
355
  const volBuySeries = volChart.addHistogramSeries({{ color: '#00ff9d' }});
356
  const volSellSeries = volChart.addHistogramSeries({{ color: '#ff3b3b' }});
357
 
358
- // Resize Logic
 
 
 
 
 
 
 
 
 
 
 
 
359
  new ResizeObserver(entries => {{
360
  for(let entry of entries) {{
361
  const {{width, height}} = entry.contentRect;
362
  if(entry.target.id === 'tv-price') priceChart.applyOptions({{width, height}});
363
  if(entry.target.id === 'tv-candles') candleChart.applyOptions({{width, height}});
 
364
  if(entry.target.id === 'sidebar-vol') volChart.applyOptions({{width, height}});
 
365
  }}
366
  }}).observe(document.body);
367
 
@@ -372,38 +635,86 @@ HTML_PAGE = f"""
372
  const data = JSON.parse(e.data);
373
  if (data.error) return;
374
 
375
- // 1. Update Price
376
  if (data.history.length) {{
377
  const hist = data.history.map(d => ({{ time: Math.floor(d.t), value: d.p }}));
378
- const uniqueHist = [...new Map(hist.map(i => [i.time, i])).values()];
379
- priceSeries.setData(uniqueHist);
380
 
381
- const lastP = uniqueHist[uniqueHist.length-1].value;
 
382
  dom.ticker.innerText = lastP.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
383
- }}
384
-
385
- // 2. Update OFI
386
- if (data.ofi.length) {{
387
- const ofiData = data.ofi.map(d => ({{ time: Math.floor(d.t), value: d.v }}));
388
- const uniqueOfi = [...new Map(ofiData.map(i => [i.time, i])).values()];
389
- ofiSeries.setData(uniqueOfi);
390
 
391
- // Update Sidebar Value
392
- const lastOfi = data.stats.ofi_val;
393
- dom.ofiVal.innerText = lastOfi.toLocaleString('en-US', {{ minimumFractionDigits: 0 }});
394
- dom.ofiVal.style.color = lastOfi >= 0 ? 'var(--yellow)' : 'var(--text-dim)';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  }}
396
 
397
- // 3. Update Candles
398
- if (data.ohlc.length) {{
399
  const candles = data.ohlc.map(c => ({{
400
- time: c.time, open: c.open, high: c.high, low: c.low, close: c.close
 
 
 
 
401
  }}));
402
  const uniqueCandles = [...new Map(candles.map(i => [i.time, i])).values()];
403
  candleSeries.setData(uniqueCandles);
404
  }}
405
 
406
- // 4. Update Volume
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  if (data.trade_history && data.trade_history.length) {{
408
  const buyData = [], sellData = [];
409
  data.trade_history.forEach(t => {{
@@ -414,6 +725,19 @@ HTML_PAGE = f"""
414
  volBuySeries.setData([...new Map(buyData.map(i => [i.time, i])).values()]);
415
  volSellSeries.setData([...new Map(sellData.map(i => [i.time, i])).values()]);
416
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  }};
418
  ws.onclose = () => setTimeout(connect, 2000);
419
  }}
@@ -426,8 +750,6 @@ HTML_PAGE = f"""
426
 
427
  async def kraken_worker():
428
  global market_state
429
-
430
- # 1. Fetch History
431
  try:
432
  async with aiohttp.ClientSession() as session:
433
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
@@ -452,16 +774,14 @@ async def kraken_worker():
452
  except Exception as e:
453
  logging.error(f"History fetch failed: {e}")
454
 
455
- # 2. Real-time Connection
456
  while True:
457
  try:
458
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
459
  logging.info(f"🔌 Connected to Kraken ({SYMBOL_KRAKEN})")
460
 
461
- # Subscribe to Book (Level 1 is actually enough for CKS, but we take more for vol check)
462
  await ws.send(json.dumps({
463
  "method": "subscribe",
464
- "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 10}
465
  }))
466
  await ws.send(json.dumps({
467
  "method": "subscribe",
@@ -478,7 +798,6 @@ async def kraken_worker():
478
  data = payload.get("data", [])
479
 
480
  if channel == "book":
481
- # Standard Book Maintenance
482
  for item in data:
483
  for bid in item.get('bids', []):
484
  q, p = float(bid['qty']), float(bid['price'])
@@ -490,7 +809,18 @@ async def kraken_worker():
490
  else: market_state['asks'][p] = q
491
 
492
  if market_state['bids'] and market_state['asks']:
 
 
 
 
 
493
  market_state['ready'] = True
 
 
 
 
 
 
494
 
495
  elif channel == "trade":
496
  for trade in data:
@@ -499,20 +829,27 @@ async def kraken_worker():
499
  price = float(trade['price'])
500
  side = trade['side']
501
 
502
- # Vol Accumulation
503
  if side == 'buy': market_state['current_vol_window']['buy'] += qty
504
  else: market_state['current_vol_window']['sell'] += qty
505
 
506
- # Live Candle Logic
507
  current_minute_start = int(time.time()) // 60 * 60
 
508
  if market_state['ohlc_history']:
509
  last_candle = market_state['ohlc_history'][-1]
 
510
  if last_candle['time'] == current_minute_start:
511
  last_candle['close'] = price
512
  if price > last_candle['high']: last_candle['high'] = price
513
  if price < last_candle['low']: last_candle['low'] = price
 
514
  elif current_minute_start > last_candle['time']:
515
- new_candle = {'time': current_minute_start, 'open': price, 'high': price, 'low': price, 'close': price}
 
 
 
 
 
 
516
  market_state['ohlc_history'].append(new_candle)
517
  if len(market_state['ohlc_history']) > 200:
518
  market_state['ohlc_history'].pop(0)
@@ -529,6 +866,7 @@ async def kraken_worker():
529
  'low': float(candle['low']),
530
  'close': float(candle['close'])
531
  }
 
532
  if market_state['ohlc_history']:
533
  if market_state['ohlc_history'][-1]['time'] == start_time:
534
  market_state['ohlc_history'][-1] = c_data
@@ -536,7 +874,8 @@ async def kraken_worker():
536
  market_state['ohlc_history'].append(c_data)
537
  if len(market_state['ohlc_history']) > 200:
538
  market_state['ohlc_history'].pop(0)
539
- except: pass
 
540
 
541
  except Exception as e:
542
  logging.warning(f"⚠️ Reconnecting: {e}")
@@ -586,7 +925,7 @@ async def main():
586
  await runner.setup()
587
  site = web.TCPSite(runner, '0.0.0.0', PORT)
588
  await site.start()
589
- print(f"🚀 Quant OFI Dashboard: http://localhost:{PORT}")
590
  await asyncio.Event().wait()
591
 
592
  if __name__ == "__main__":
 
2
  import json
3
  import logging
4
  import time
5
+ import bisect
6
+ import math
7
+ import statistics
8
  import aiohttp
9
  from aiohttp import web
10
  import websockets
 
12
  SYMBOL_KRAKEN = "BTC/USD"
13
  PORT = 7860
14
  HISTORY_LENGTH = 300
15
+ BROADCAST_RATE = 0.1
16
+
17
+ DECAY_LAMBDA = 50.0
18
+ IMPACT_SENSITIVITY = 2.0
19
+ Z_SCORE_THRESHOLD = 3.0
20
+ WALL_LOOKBACK = 200
21
+
22
+ # ML Hyperparameters
23
+ LEARNING_RATE = 0.01
24
+ MOMENTUM = 0.9
25
 
26
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
27
 
28
+ class OnlineScaler:
29
+ def __init__(self):
30
+ self.n = 0
31
+ self.mean = 0.0
32
+ self.M2 = 0.0
33
+
34
+ def update(self, x):
35
+ self.n += 1
36
+ delta = x - self.mean
37
+ self.mean += delta / self.n
38
+ delta2 = x - self.mean
39
+ self.M2 += delta * delta2
40
+ return self.transform(x)
41
+
42
+ def transform(self, x):
43
+ if self.n < 2: return 0.0
44
+ var = self.M2 / (self.n - 1)
45
+ if var == 0: return 0.0
46
+ std = math.sqrt(var)
47
+ return (x - self.mean) / std
48
+
49
+ class QuantModel:
50
+ def __init__(self, num_features):
51
+ self.weights = [0.0] * num_features
52
+ self.bias = 0.0
53
+ self.velocity = [0.0] * num_features
54
+ self.bias_velocity = 0.0
55
+ self.scalers = [OnlineScaler() for _ in range(num_features)]
56
+ self.prev_features = None
57
+ self.prev_price = None
58
+
59
+ def predict(self, features):
60
+ scaled = [s.transform(f) for s, f in zip(self.scalers, features)]
61
+ dot = sum(w * x for w, x in zip(self.weights, scaled))
62
+ return dot + self.bias
63
+
64
+ def train(self, current_price, current_features):
65
+ if self.prev_features is None or self.prev_price is None:
66
+ self.prev_features = [s.update(f) for s, f in zip(self.scalers, current_features)]
67
+ self.prev_price = current_price
68
+ return
69
+
70
+ # Target: Price Change (Delta)
71
+ actual_delta = current_price - self.prev_price
72
+
73
+ # Predict using PAST features
74
+ pred_delta = sum(w * x for w, x in zip(self.weights, self.prev_features)) + self.bias
75
+
76
+ # Error
77
+ error = pred_delta - actual_delta
78
+
79
+ # SGD with Momentum Update
80
+ for i in range(len(self.weights)):
81
+ grad = error * self.prev_features[i]
82
+ self.velocity[i] = MOMENTUM * self.velocity[i] - LEARNING_RATE * grad
83
+ self.weights[i] += self.velocity[i]
84
+
85
+ self.bias_velocity = MOMENTUM * self.bias_velocity - LEARNING_RATE * error
86
+ self.bias += self.bias_velocity
87
+
88
+ # Store for next tick
89
+ self.prev_features = [s.update(f) for s, f in zip(self.scalers, current_features)]
90
+ self.prev_price = current_price
91
+
92
+ def get_forecast(self, current_price, current_features):
93
+ # Predict NEXT delta based on CURRENT features
94
+ pred_delta = self.predict(current_features)
95
+ return current_price + pred_delta
96
+
97
+ # 4 Features: OFI, Depth Area, Best Imbalance, Velocity
98
+ ml_model = QuantModel(4)
99
+
100
  market_state = {
101
  "bids": {},
102
  "asks": {},
103
  "history": [],
104
+ "pred_history": [],
105
+ "ml_history": [],
106
  "trade_vol_history": [],
107
  "ohlc_history": [],
108
  "current_vol_window": {"buy": 0.0, "sell": 0.0, "start": time.time()},
109
  "current_mid": 0.0,
110
+ "prev_mid": 0.0,
 
111
  "ready": False
112
  }
113
 
114
  connected_clients = set()
115
 
116
+ def detect_anomalies(orders, scan_depth):
117
+ if len(orders) < 10: return []
118
+ relevant_orders = orders[:scan_depth]
119
+ volumes = [q for p, q in relevant_orders]
120
+ if not volumes: return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
+ try:
123
+ avg_vol = statistics.mean(volumes)
124
+ stdev_vol = statistics.stdev(volumes)
125
+ except statistics.StatisticsError:
126
+ return []
127
+
128
+ if stdev_vol == 0: return []
129
+
130
+ walls = []
131
+ for price, qty in relevant_orders:
132
+ z_score = (qty - avg_vol) / stdev_vol
133
+ if z_score > Z_SCORE_THRESHOLD:
134
+ walls.append({"price": price, "vol": qty, "z_score": z_score})
135
+
136
+ walls.sort(key=lambda x: x['z_score'], reverse=True)
137
+ return walls[:3]
138
+
139
+ def calculate_micro_price_structure(diff_x, diff_y_net, current_mid, best_bid, best_ask, walls):
140
+ if not diff_x or len(diff_x) < 5: return None, 0
141
+
142
+ weighted_imbalance = 0.0
143
+ total_weight = 0.0
144
+
145
+ for i in range(len(diff_x)):
146
+ dist = diff_x[i]
147
+ net_vol = diff_y_net[i]
148
+ weight = math.exp(-dist / DECAY_LAMBDA)
149
+ weighted_imbalance += net_vol * weight
150
+ total_weight += weight
151
+
152
+ rho = weighted_imbalance / total_weight if total_weight > 0 else 0
153
+
154
+ spread = best_ask - best_bid
155
+ theoretical_delta = (spread / 2) * rho * IMPACT_SENSITIVITY
156
+ projected_price = current_mid + theoretical_delta
157
+
158
+ final_delta = theoretical_delta
159
+ if final_delta > 0 and walls['asks']:
160
+ nearest_wall = walls['asks'][0]
161
+ if projected_price >= nearest_wall['price']:
162
+ damp_factor = 1.0 / (1.0 + (nearest_wall['z_score'] * 0.2))
163
+ final_delta *= damp_factor
164
+ elif final_delta < 0 and walls['bids']:
165
+ nearest_wall = walls['bids'][0]
166
+ if projected_price <= nearest_wall['price']:
167
+ damp_factor = 1.0 / (1.0 + (nearest_wall['z_score'] * 0.2))
168
+ final_delta *= damp_factor
169
 
170
+ return {
171
+ "projected": current_mid + final_delta,
172
+ "rho": rho
173
+ }, sum(diff_y_net)
174
 
175
+ def calculate_polr(bids, asks, mid):
176
+ if not bids or not asks: return []
 
 
177
 
178
+ sorted_bids = sorted(bids.items(), key=lambda x: -x[0])
179
+ sorted_asks = sorted(asks.items(), key=lambda x: x[0])
180
 
181
+ path_points = []
182
+ volume_steps = [i * 0.5 for i in range(1, 61)]
 
183
 
184
+ for i, target_vol in enumerate(volume_steps):
185
+ ask_cost_dist = 0
186
+ cum_vol = 0
187
+ target_ask_price = mid
188
+ for p, q in sorted_asks:
189
+ cum_vol += q
190
+ if cum_vol >= target_vol:
191
+ target_ask_price = p
192
+ break
193
+ ask_cost_dist = target_ask_price - mid
194
+
195
+ bid_cost_dist = 0
196
+ cum_vol = 0
197
+ target_bid_price = mid
198
+ for p, q in sorted_bids:
199
+ cum_vol += q
200
+ if cum_vol >= target_vol:
201
+ target_bid_price = p
202
+ break
203
+ bid_cost_dist = mid - target_bid_price
204
+
205
+ if bid_cost_dist <= 0: bid_cost_dist = 0.01
206
+ if ask_cost_dist <= 0: ask_cost_dist = 0.01
207
+
208
+ projected_p = mid
209
+ if ask_cost_dist > bid_cost_dist:
210
+ projected_p = target_ask_price
211
+ else:
212
+ projected_p = target_bid_price
213
 
214
+ path_points.append({'index': i, 'p': projected_p})
215
+
216
+ return path_points
217
+
218
+ def process_market_data():
219
+ if not market_state['ready']: return {"error": "Initializing..."}
220
 
221
+ mid = market_state['current_mid']
222
+
223
+ now = time.time()
224
  if now - market_state['current_vol_window']['start'] >= 1.0:
225
  market_state['trade_vol_history'].append({
226
  't': now,
 
231
  market_state['trade_vol_history'].pop(0)
232
  market_state['current_vol_window'] = {"buy": 0.0, "sell": 0.0, "start": now}
233
 
234
+ sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
235
+ sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
236
+
237
+ if not sorted_bids or not sorted_asks: return {"error": "Empty Book"}
238
+
239
+ best_bid_p, best_bid_q = sorted_bids[0]
240
+ best_ask_p, best_ask_q = sorted_asks[0]
241
+
242
+ bid_walls = detect_anomalies(sorted_bids, WALL_LOOKBACK)
243
+ ask_walls = detect_anomalies(sorted_asks, WALL_LOOKBACK)
244
+
245
+ d_b_x, d_b_y, cum = [], [], 0
246
+ for p, q in sorted_bids[:300]:
247
+ d = mid - p
248
+ if d >= 0:
249
+ cum += q
250
+ d_b_x.append(d); d_b_y.append(cum)
251
+
252
+ d_a_x, d_a_y, cum = [], [], 0
253
+ for p, q in sorted_asks[:300]:
254
+ d = p - mid
255
+ if d >= 0:
256
+ cum += q
257
+ d_a_x.append(d); d_a_y.append(cum)
258
+
259
+ diff_x, diff_y_net = [], []
260
+ chart_bids, chart_asks = [], []
261
+
262
+ if d_b_x and d_a_x:
263
+ max_dist = min(d_b_x[-1], d_a_x[-1])
264
+ step_size = max_dist / 100
265
+ steps = [i * step_size for i in range(1, 101)]
266
+
267
+ for s in steps:
268
+ idx_b = bisect.bisect_right(d_b_x, s)
269
+ vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
270
+ idx_a = bisect.bisect_right(d_a_x, s)
271
+ vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
272
+
273
+ diff_x.append(s)
274
+ diff_y_net.append(vol_b - vol_a)
275
+ chart_bids.append(vol_b)
276
+ chart_asks.append(vol_a)
277
+
278
+ analysis, depth_integral = calculate_micro_price_structure(
279
+ diff_x, diff_y_net, mid, best_bid_p, best_ask_p,
280
+ {"bids": bid_walls, "asks": ask_walls}
281
+ )
282
+
283
+ # --- MACHINE LEARNING FEATURE EXTRACTION ---
284
+ # 1. OFI: Net Buy-Sell Vol in current window
285
+ feat_ofi = market_state['current_vol_window']['buy'] - market_state['current_vol_window']['sell']
286
+ # 2. Depth Difference: Area under the Net Liquidity Curve (Bids - Asks)
287
+ feat_depth = depth_integral
288
+ # 3. Orderbook Imbalance at L1
289
+ feat_l1_imb = (best_bid_q - best_ask_q) / (best_bid_q + best_ask_q)
290
+ # 4. Price Momentum (Current - Prev)
291
+ feat_mom = mid - market_state['prev_mid']
292
+
293
+ features = [feat_ofi, feat_depth, feat_l1_imb, feat_mom]
294
+
295
+ # Train (Learn from last tick's prediction vs this tick's reality)
296
+ ml_model.train(mid, features)
297
+ # Predict (Forecast next tick)
298
+ ml_prediction = ml_model.get_forecast(mid, features)
299
+
300
+ if len(market_state['ml_history']) == 0 or (now - market_state['ml_history'][-1]['t'] > 0.5):
301
+ market_state['ml_history'].append({'t': now, 'p': ml_prediction})
302
+ if len(market_state['ml_history']) > HISTORY_LENGTH:
303
+ market_state['ml_history'].pop(0)
304
+ # -------------------------------------------
305
+
306
+ polr_path = calculate_polr(market_state['bids'], market_state['asks'], mid)
307
+
308
+ if analysis:
309
+ if not market_state['pred_history'] or (now - market_state['pred_history'][-1]['t'] > 0.5):
310
+ market_state['pred_history'].append({'t': now, 'p': analysis['projected']})
311
+ if len(market_state['pred_history']) > HISTORY_LENGTH:
312
+ market_state['pred_history'].pop(0)
313
+
314
  return {
315
  "mid": mid,
316
  "history": market_state['history'],
317
+ "pred_history": market_state['pred_history'],
318
+ "ml_history": market_state['ml_history'],
319
+ "polr": polr_path,
320
  "trade_history": market_state['trade_vol_history'],
321
  "ohlc": market_state['ohlc_history'],
322
+ "depth_x": diff_x,
323
+ "depth_net": diff_y_net,
324
+ "depth_bids": chart_bids,
325
+ "depth_asks": chart_asks,
326
+ "analysis": analysis,
327
+ "walls": {"bids": bid_walls, "asks": ask_walls}
328
  }
329
 
330
  HTML_PAGE = f"""
 
332
  <html lang="en">
333
  <head>
334
  <meta charset="UTF-8">
335
+ <title>{SYMBOL_KRAKEN}</title>
336
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
337
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
338
  <style>
 
346
  --red: #ff3b3b;
347
  --blue: #2979ff;
348
  --yellow: #ffeb3b;
349
+ --purple: #d500f9;
350
+ --cyan: #00bcd4;
351
  }}
352
  body {{
353
  margin: 0; padding: 0;
 
390
 
391
  #p-bottom {{
392
  grid-column: 1 / 2; grid-row: 3 / 4;
393
+ display: grid;
394
+ grid-template-columns: 1fr 1fr;
395
+ gap: 1px;
396
+ background: var(--border);
397
  }}
398
+ .bottom-sub {{ background: var(--bg-panel); display: flex; flex-direction: column; position: relative; }}
399
+
400
  #p-sidebar {{
401
  grid-column: 2 / 3;
402
  grid-row: 2 / 4;
 
426
  .label {{ font-size: 10px; color: var(--text-dim); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }}
427
  .value {{ font-family: 'JetBrains Mono', monospace; font-size: 20px; font-weight: 700; color: #fff; }}
428
  .value-lg {{ font-size: 26px; }}
429
+ .value-sub {{ font-family: 'JetBrains Mono', monospace; font-size: 11px; margin-top: 2px; color: #666; }}
430
+
431
  .divider {{ height: 1px; background: var(--border); width: 100%; }}
432
  .c-green {{ color: var(--green); }}
433
  .c-red {{ color: var(--red); }}
434
+ .c-dim {{ color: var(--text-dim); }}
435
+ .c-purp {{ color: var(--purple); }}
436
+ .c-cyan {{ color: var(--cyan); }}
437
+
438
+ .list-container {{ display: flex; flex-direction: column; gap: 8px; overflow-y: auto; height: 100px; }}
439
+ .list-item {{
440
+ display: flex; justify-content: space-between;
441
+ font-family: 'JetBrains Mono', monospace;
442
+ font-size: 11px;
443
+ border-bottom: 1px solid #151515;
444
+ padding-bottom: 4px;
445
+ }}
446
+ .list-item span:first-child {{ color: #e0e0e0; }}
447
+ .list-item:last-child {{ border: none; }}
448
 
449
  .sidebar-chart-box {{
450
  flex: 1;
 
473
 
474
  <div id="p-chart" class="panel">
475
  <div class="chart-header">
476
+ PRICE (BLUE) // <span class="c-purp">POLR</span> // <span style="color:var(--yellow)">MICRO</span> // <span class="c-cyan">ML MODEL</span>
477
  </div>
478
  <div id="tv-price" style="flex: 1; width: 100%;"></div>
479
  </div>
480
 
481
+ <div id="p-bottom">
482
+ <div class="bottom-sub">
483
+ <div class="chart-header">1M KLINE (KRAKEN OHLC)</div>
484
+ <div id="tv-candles" style="flex: 1; width: 100%;"></div>
485
+ </div>
486
+ <div class="bottom-sub">
487
+ <div class="chart-header">ORDER FLOW IMBALANCE</div>
488
+ <div id="tv-net" style="flex: 1; width: 100%;"></div>
489
+ </div>
490
  </div>
491
 
492
  <div id="p-sidebar" class="panel">
493
 
494
  <div class="data-group">
495
+ <span class="label">ML Prediction</span>
496
+ <span id="ml-val" class="value c-cyan">---</span>
497
+ </div>
498
+
499
+ <div class="data-group">
500
+ <span class="label">Micro-Price Delta</span>
501
+ <div style="display:flex; align-items: baseline; gap: 10px;">
502
+ <span id="proj-pct" class="value value-lg">--%</span>
503
+ <span id="proj-val" class="value-sub">---</span>
504
+ </div>
505
  </div>
506
 
507
  <div class="divider"></div>
508
 
509
  <div class="data-group">
510
+ <span class="label">OFI Imbalance Ratio</span>
511
+ <span id="score-val" class="value">0.00</span>
 
 
 
 
 
 
512
  </div>
513
 
514
  <div class="divider"></div>
515
 
516
+ <div class="data-group">
517
+ <span class="label">Detected Walls (Z > 3.0)</span>
518
+ <div id="wall-list" class="list-container">
519
+ <span class="c-dim" style="font-size: 11px;">Scanning...</span>
520
+ </div>
521
+ </div>
522
+
523
  <div class="sidebar-chart-box">
524
  <span class="label" style="margin-bottom:4px;">Real-time Volume Ticks</span>
525
  <div id="sidebar-vol" class="mini-chart"></div>
526
  </div>
527
+
528
+ <div class="sidebar-chart-box">
529
+ <span class="label" style="margin-bottom:4px;">Liquidity Density</span>
530
+ <div id="sidebar-density" class="mini-chart"></div>
531
+ </div>
532
  </div>
533
  </div>
534
 
 
541
  document.addEventListener('DOMContentLoaded', () => {{
542
  const dom = {{
543
  ticker: document.getElementById('price-ticker'),
544
+ score: document.getElementById('score-val'),
545
+ projVal: document.getElementById('proj-val'),
546
+ projPct: document.getElementById('proj-pct'),
547
+ mlVal: document.getElementById('ml-val'),
548
+ wallList: document.getElementById('wall-list')
549
  }};
550
 
551
+ const chartOpts = {{
 
552
  layout: {{ background: {{ type: 'solid', color: '#0a0a0a' }}, textColor: '#888', fontFamily: 'JetBrains Mono' }},
553
  grid: {{ vertLines: {{ color: '#151515' }}, horzLines: {{ color: '#151515' }} }},
554
+ rightPriceScale: {{ borderColor: '#222', scaleMargins: {{ top: 0.1, bottom: 0.1 }} }},
 
555
  timeScale: {{ borderColor: '#222', timeVisible: true, secondsVisible: true }},
556
+ crosshair: {{ mode: 1, vertLine: {{ color: '#444', labelBackgroundColor: '#444' }}, horzLine: {{ color: '#444', labelBackgroundColor: '#444' }} }}
557
+ }};
 
 
 
 
 
558
 
559
+ const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts);
560
+
561
+ const polrLines = [];
562
+ const polrCount = 60;
563
+
564
+ for(let i=0; i<polrCount; i++) {{
565
+ const opacity = 1.0 - (i / (polrCount + 5));
566
+ const color = `rgba(213, 0, 249, ${{opacity.toFixed(2)}})`;
567
+
568
+ polrLines.push(
569
+ priceChart.addLineSeries({{
570
+ color: color,
571
+ lineWidth: 1,
572
+ crosshairMarkerVisible: false,
573
+ lastValueVisible: false,
574
+ priceLineVisible: false,
575
+ title: ''
576
+ }})
577
+ );
578
+ }}
579
 
580
+ const priceSeries = priceChart.addLineSeries({{ color: '#2979ff', lineWidth: 2, title: 'Price' }});
581
+ const predSeries = priceChart.addLineSeries({{ color: '#ffeb3b', lineWidth: 2, lineStyle: 2, title: 'Micro-Structure' }});
582
+ const mlSeries = priceChart.addLineSeries({{ color: '#00bcd4', lineWidth: 2, lineStyle: 0, title: 'ML Forecast' }});
583
+
584
  const candleChart = LightweightCharts.createChart(document.getElementById('tv-candles'), {{
585
+ ...chartOpts,
586
+ timeScale: {{ timeVisible: true, secondsVisible: false }}
 
587
  }});
588
  const candleSeries = candleChart.addCandlestickSeries({{
589
  upColor: '#00ff9d', downColor: '#ff3b3b', borderVisible: false, wickUpColor: '#00ff9d', wickDownColor: '#ff3b3b'
590
  }});
591
 
592
+ const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{
593
+ ...chartOpts, localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
594
+ }});
595
+ const netSeries = netChart.addHistogramSeries({{ color: '#2979ff' }});
596
+
597
  const volChart = LightweightCharts.createChart(document.getElementById('sidebar-vol'), {{
598
+ ...chartOpts,
599
+ grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }},
600
  rightPriceScale: {{ visible: false }},
601
  timeScale: {{ visible: false }},
602
  handleScroll: false, handleScale: false
 
604
  const volBuySeries = volChart.addHistogramSeries({{ color: '#00ff9d' }});
605
  const volSellSeries = volChart.addHistogramSeries({{ color: '#ff3b3b' }});
606
 
607
+ const denChart = LightweightCharts.createChart(document.getElementById('sidebar-density'), {{
608
+ ...chartOpts,
609
+ grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }},
610
+ rightPriceScale: {{ visible: false }},
611
+ timeScale: {{ visible: false }},
612
+ handleScroll: false, handleScale: false
613
+ }});
614
+ const bidSeries = denChart.addAreaSeries({{ lineColor: '#00ff9d', topColor: 'rgba(0, 255, 157, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
615
+ const askSeries = denChart.addAreaSeries({{ lineColor: '#ff3b3b', topColor: 'rgba(255, 59, 59, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
616
+
617
+ let activeLines = [];
618
+ let activeCandleLines = [];
619
+
620
  new ResizeObserver(entries => {{
621
  for(let entry of entries) {{
622
  const {{width, height}} = entry.contentRect;
623
  if(entry.target.id === 'tv-price') priceChart.applyOptions({{width, height}});
624
  if(entry.target.id === 'tv-candles') candleChart.applyOptions({{width, height}});
625
+ if(entry.target.id === 'tv-net') netChart.applyOptions({{width, height}});
626
  if(entry.target.id === 'sidebar-vol') volChart.applyOptions({{width, height}});
627
+ if(entry.target.id === 'sidebar-density') denChart.applyOptions({{width, height}});
628
  }}
629
  }}).observe(document.body);
630
 
 
635
  const data = JSON.parse(e.data);
636
  if (data.error) return;
637
 
 
638
  if (data.history.length) {{
639
  const hist = data.history.map(d => ({{ time: Math.floor(d.t), value: d.p }}));
640
+ const cleanHist = [...new Map(hist.map(i => [i.time, i])).values()];
641
+ priceSeries.setData(cleanHist);
642
 
643
+ const lastP = cleanHist[cleanHist.length-1].value;
644
+ const lastTime = cleanHist[cleanHist.length-1].time;
645
  dom.ticker.innerText = lastP.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
 
 
 
 
 
 
 
646
 
647
+ if (data.analysis) {{
648
+ const proj = data.analysis.projected;
649
+ const rho = data.analysis.rho;
650
+ predSeries.setData([
651
+ cleanHist[cleanHist.length-1],
652
+ {{ time: lastTime + 60, value: proj }}
653
+ ]);
654
+ const pct = ((proj - lastP) / lastP) * 100;
655
+ const sign = pct >= 0 ? "+" : "";
656
+ dom.projPct.innerText = `${{sign}}${{pct.toFixed(4)}}%`;
657
+ dom.projPct.style.color = pct >= 0 ? "var(--green)" : "var(--red)";
658
+ dom.projVal.innerText = proj.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
659
+ dom.score.innerText = rho.toFixed(3);
660
+ dom.score.style.color = rho > 0 ? "var(--green)" : (rho < 0 ? "var(--red)" : "var(--text-main)");
661
+ }}
662
+
663
+ if (data.ml_history && data.ml_history.length) {{
664
+ const mlLast = data.ml_history[data.ml_history.length-1];
665
+ dom.mlVal.innerText = mlLast.p.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
666
+
667
+ mlSeries.setData([
668
+ cleanHist[cleanHist.length-1],
669
+ {{ time: lastTime + 30, value: mlLast.p }}
670
+ ]);
671
+ }}
672
+
673
+ if (data.polr && data.polr.length) {{
674
+ data.polr.forEach((point, index) => {{
675
+ if (index < polrLines.length) {{
676
+ polrLines[index].update({{
677
+ time: lastTime,
678
+ value: point.p
679
+ }});
680
+ }}
681
+ }});
682
+ }}
683
  }}
684
 
685
+ if (data.ohlc && data.ohlc.length) {{
 
686
  const candles = data.ohlc.map(c => ({{
687
+ time: c.time,
688
+ open: c.open,
689
+ high: c.high,
690
+ low: c.low,
691
+ close: c.close
692
  }}));
693
  const uniqueCandles = [...new Map(candles.map(i => [i.time, i])).values()];
694
  candleSeries.setData(uniqueCandles);
695
  }}
696
 
697
+ if (data.walls) {{
698
+ activeLines.forEach(l => priceSeries.removePriceLine(l));
699
+ activeLines = [];
700
+ activeCandleLines.forEach(l => candleSeries.removePriceLine(l));
701
+ activeCandleLines = [];
702
+
703
+ let html = "";
704
+ const addWall = (w, type) => {{
705
+ const color = type === 'BID' ? '#00ff9d' : '#ff3b3b';
706
+ const lineOpts = {{ price: w.price, color: color, lineWidth: 1, lineStyle: 2, axisLabelVisible: false }};
707
+
708
+ activeLines.push(priceSeries.createPriceLine(lineOpts));
709
+ activeCandleLines.push(candleSeries.createPriceLine(lineOpts));
710
+
711
+ 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>`;
712
+ }};
713
+ data.walls.asks.forEach(w => addWall(w, 'ASK'));
714
+ data.walls.bids.forEach(w => addWall(w, 'BID'));
715
+ dom.wallList.innerHTML = html || '<span class="c-dim" style="font-size:11px">Scanning...</span>';
716
+ }}
717
+
718
  if (data.trade_history && data.trade_history.length) {{
719
  const buyData = [], sellData = [];
720
  data.trade_history.forEach(t => {{
 
725
  volBuySeries.setData([...new Map(buyData.map(i => [i.time, i])).values()]);
726
  volSellSeries.setData([...new Map(sellData.map(i => [i.time, i])).values()]);
727
  }}
728
+
729
+ if (data.depth_x.length) {{
730
+ const bids = [], asks = [], nets = [];
731
+ for(let i=0; i<data.depth_x.length; i++) {{
732
+ const t = data.depth_x[i];
733
+ bids.push({{ time: t, value: data.depth_bids[i] }});
734
+ asks.push({{ time: t, value: data.depth_asks[i] }});
735
+ nets.push({{ time: t, value: data.depth_net[i], color: data.depth_net[i] > 0 ? '#00ff9d' : '#ff3b3b' }});
736
+ }}
737
+ bidSeries.setData(bids);
738
+ askSeries.setData(asks);
739
+ netSeries.setData(nets);
740
+ }}
741
  }};
742
  ws.onclose = () => setTimeout(connect, 2000);
743
  }}
 
750
 
751
  async def kraken_worker():
752
  global market_state
 
 
753
  try:
754
  async with aiohttp.ClientSession() as session:
755
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
 
774
  except Exception as e:
775
  logging.error(f"History fetch failed: {e}")
776
 
 
777
  while True:
778
  try:
779
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
780
  logging.info(f"🔌 Connected to Kraken ({SYMBOL_KRAKEN})")
781
 
 
782
  await ws.send(json.dumps({
783
  "method": "subscribe",
784
+ "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}
785
  }))
786
  await ws.send(json.dumps({
787
  "method": "subscribe",
 
798
  data = payload.get("data", [])
799
 
800
  if channel == "book":
 
801
  for item in data:
802
  for bid in item.get('bids', []):
803
  q, p = float(bid['qty']), float(bid['price'])
 
809
  else: market_state['asks'][p] = q
810
 
811
  if market_state['bids'] and market_state['asks']:
812
+ market_state['prev_mid'] = market_state['current_mid']
813
+ best_bid = max(market_state['bids'].keys())
814
+ best_ask = min(market_state['asks'].keys())
815
+ mid = (best_bid + best_ask) / 2
816
+ market_state['current_mid'] = mid
817
  market_state['ready'] = True
818
+
819
+ now = time.time()
820
+ if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
821
+ market_state['history'].append({'t': now, 'p': mid})
822
+ if len(market_state['history']) > HISTORY_LENGTH:
823
+ market_state['history'].pop(0)
824
 
825
  elif channel == "trade":
826
  for trade in data:
 
829
  price = float(trade['price'])
830
  side = trade['side']
831
 
 
832
  if side == 'buy': market_state['current_vol_window']['buy'] += qty
833
  else: market_state['current_vol_window']['sell'] += qty
834
 
 
835
  current_minute_start = int(time.time()) // 60 * 60
836
+
837
  if market_state['ohlc_history']:
838
  last_candle = market_state['ohlc_history'][-1]
839
+
840
  if last_candle['time'] == current_minute_start:
841
  last_candle['close'] = price
842
  if price > last_candle['high']: last_candle['high'] = price
843
  if price < last_candle['low']: last_candle['low'] = price
844
+
845
  elif current_minute_start > last_candle['time']:
846
+ new_candle = {
847
+ 'time': current_minute_start,
848
+ 'open': price,
849
+ 'high': price,
850
+ 'low': price,
851
+ 'close': price
852
+ }
853
  market_state['ohlc_history'].append(new_candle)
854
  if len(market_state['ohlc_history']) > 200:
855
  market_state['ohlc_history'].pop(0)
 
866
  'low': float(candle['low']),
867
  'close': float(candle['close'])
868
  }
869
+
870
  if market_state['ohlc_history']:
871
  if market_state['ohlc_history'][-1]['time'] == start_time:
872
  market_state['ohlc_history'][-1] = c_data
 
874
  market_state['ohlc_history'].append(c_data)
875
  if len(market_state['ohlc_history']) > 200:
876
  market_state['ohlc_history'].pop(0)
877
+ except Exception as e:
878
+ pass
879
 
880
  except Exception as e:
881
  logging.warning(f"⚠️ Reconnecting: {e}")
 
925
  await runner.setup()
926
  site = web.TCPSite(runner, '0.0.0.0', PORT)
927
  await site.start()
928
+ print(f"🚀 Quant Dashboard: http://localhost:{PORT}")
929
  await asyncio.Event().wait()
930
 
931
  if __name__ == "__main__":