Alvin3y1 commited on
Commit
25c08cc
·
verified ·
1 Parent(s): 175cf4a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +534 -314
app.py CHANGED
@@ -2,219 +2,181 @@ import asyncio
2
  import json
3
  import logging
4
  import time
 
5
  import math
 
6
  import aiohttp
7
- from collections import deque
8
  from aiohttp import web
9
  import websockets
10
- import statistics
11
 
12
  SYMBOL_KRAKEN = "BTC/USD"
13
  PORT = 7860
14
  HISTORY_LENGTH = 300
15
  BROADCAST_RATE = 0.1
16
- MICROPRICE_DECAY = 0.05
17
 
18
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
 
 
 
 
19
 
20
- class OnlineStats:
21
- def __init__(self):
22
- self.count = 0
23
- self.mean = 0.0
24
- self.M2 = 0.0
25
-
26
- def update(self, value):
27
- self.count += 1
28
- delta = value - self.mean
29
- self.mean += delta / self.count
30
- delta2 = value - self.mean
31
- self.M2 += delta * delta2
32
-
33
- @property
34
- def variance(self):
35
- if self.count < 2: return 0.0
36
- return self.M2 / self.count
37
-
38
- @property
39
- def std_dev(self):
40
- return math.sqrt(self.variance)
41
-
42
- class KalmanVelocity:
43
- def __init__(self, R=0.001, Q=0.0001):
44
- self.z = 0.0
45
- self.v = 0.0
46
- self.P = 1.0
47
- self.R = R
48
- self.Q = Q
49
- self.last_ts = time.time()
50
-
51
- def update(self, price):
52
- now = time.time()
53
- dt = now - self.last_ts
54
- self.last_ts = now
55
- if dt <= 0: return
56
-
57
- pred_z = self.z + self.v * dt
58
- pred_v = self.v
59
- p_cov = self.P + self.Q
60
-
61
- y = price - pred_z
62
- K = p_cov / (p_cov + self.R)
63
-
64
- self.z = pred_z + K * y
65
- self.v = pred_v + (K / dt) * y
66
- self.P = (1 - K) * p_cov
67
 
68
  market_state = {
69
  "bids": {},
70
  "asks": {},
71
  "history": [],
72
- "trade_history": deque(maxlen=2000),
 
73
  "ohlc_history": [],
74
  "current_vol_window": {"buy": 0.0, "sell": 0.0, "start": time.time()},
75
  "current_mid": 0.0,
76
- "ready": False,
77
- "kalman": KalmanVelocity(),
78
- "stats": OnlineStats(),
79
- "walls": {"bids": [], "asks": []}
80
  }
81
 
82
  connected_clients = set()
83
 
84
- def detect_walls(order_book, side_name):
85
- if not order_book: return []
86
-
87
- sorted_book = sorted(order_book.items(), key=lambda x: x[0], reverse=(side_name == 'bids'))
88
- relevant = sorted_book[:50]
89
-
90
- volumes = [q for p, q in relevant]
91
  if not volumes: return []
92
-
93
- avg_vol = statistics.mean(volumes)
94
- std_vol = statistics.stdev(volumes) if len(volumes) > 1 else 0
95
-
 
 
 
 
 
96
  walls = []
97
- for p, q in relevant:
98
- if std_vol > 0:
99
- z = (q - avg_vol) / std_vol
100
- if z > 2.5:
101
- walls.append({'p': p, 'q': q, 'z': z})
102
-
103
- return walls[:3]
104
 
105
- def calculate_weighted_micro_price(mid_price):
106
- bids = sorted(market_state['bids'].items(), reverse=True)[:50]
107
- asks = sorted(market_state['asks'].items())[:50]
108
 
109
- if not bids or not asks: return mid_price
 
110
 
111
- sum_wb = 0.0
112
- sum_wa = 0.0
113
 
114
- for p, q in bids:
115
- distance = abs(mid_price - p)
116
- weight = q * math.exp(-MICROPRICE_DECAY * distance)
117
- sum_wb += weight
118
-
119
- for p, q in asks:
120
- distance = abs(p - mid_price)
121
- weight = q * math.exp(-MICROPRICE_DECAY * distance)
122
- sum_wa += weight
123
-
124
- total_w = sum_wb + sum_wa
125
- if total_w == 0: return mid_price
126
-
127
- imbalance = (sum_wb - sum_wa) / total_w
128
- spread = asks[0][0] - bids[0][0]
129
- micro_price = mid_price + (imbalance * (spread / 2))
130
- return micro_price
131
-
132
- def calculate_vwap_1m():
133
- cutoff = time.time() - 60
134
- v_sum = 0.0
135
- pv_sum = 0.0
136
-
137
- for trade in reversed(market_state['trade_history']):
138
- if trade['t'] < cutoff: break
139
- pv_sum += trade['p'] * trade['q']
140
- v_sum += trade['q']
141
-
142
- return pv_sum / v_sum if v_sum > 0 else market_state['current_mid']
143
 
144
- def calculate_kyle_lambda(volatility, volume_window):
145
- if volume_window <= 0: return 0
146
- return (volatility * 1000) / (math.sqrt(volume_window) + 1)
 
147
 
148
  def process_market_data():
149
  if not market_state['ready']: return {"error": "Initializing..."}
150
 
151
  mid = market_state['current_mid']
152
- now = time.time()
153
-
154
- market_state['stats'].update(mid)
155
- volatility = market_state['stats'].std_dev
156
- if volatility == 0: volatility = 1.0
157
-
158
- market_state['kalman'].update(mid)
159
 
160
- micro_price = calculate_weighted_micro_price(mid)
161
- vwap = calculate_vwap_1m()
 
 
 
 
 
 
 
 
 
 
 
162
 
163
- ofi_buy = 0.0
164
- ofi_sell = 0.0
165
- ofi_window = 10.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
- for t in reversed(market_state['trade_history']):
168
- if t['t'] < (now - ofi_window): break
169
- if t['side'] == 'buy': ofi_buy += t['q']
170
- else: ofi_sell += t['q']
171
 
172
- net_ofi = ofi_buy - ofi_sell
173
- total_vol_10s = ofi_buy + ofi_sell
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
- k_lambda = calculate_kyle_lambda(volatility, total_vol_10s)
176
- impact_term = net_ofi * k_lambda
177
-
178
- mean_reversion_alpha = 0.1
179
- reversion_term = (vwap - mid) * mean_reversion_alpha
180
-
181
- micro_alpha = (micro_price - mid) * 0.8
182
-
183
- trend_term = market_state['kalman'].v * 60.0
184
-
185
- predicted_delta = impact_term + micro_alpha + trend_term + reversion_term
186
- pred_close = mid + predicted_delta
187
-
188
- sigma_1m = volatility * math.sqrt(60)
189
-
190
- pred_candle = {
191
- 'time': int(now) + 60,
192
- 'open': mid,
193
- 'close': pred_close,
194
- 'high': max(mid, pred_close) + (2 * sigma_1m),
195
- 'low': min(mid, pred_close) - (2 * sigma_1m)
196
- }
197
-
198
- if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
199
- market_state['history'].append({'t': now, 'p': mid})
200
- if len(market_state['history']) > HISTORY_LENGTH:
201
- market_state['history'].pop(0)
202
-
203
- bid_walls = detect_walls(market_state['bids'], 'bids')
204
- ask_walls = detect_walls(market_state['asks'], 'asks')
205
-
206
- analysis = {
207
- "projected": pred_close,
208
- "rho": (micro_price - mid),
209
- "vwap": vwap,
210
- "lambda": k_lambda
211
- }
212
 
213
  return {
214
  "mid": mid,
215
  "history": market_state['history'],
 
 
216
  "ohlc": market_state['ohlc_history'],
217
- "pred_candle": pred_candle,
 
 
 
218
  "analysis": analysis,
219
  "walls": {"bids": bid_walls, "asks": ask_walls}
220
  }
@@ -224,145 +186,373 @@ HTML_PAGE = f"""
224
  <html lang="en">
225
  <head>
226
  <meta charset="UTF-8">
227
- <title>{SYMBOL_KRAKEN} Quant</title>
228
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
229
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
230
  <style>
231
- :root {{ --bg-base: #000000; --bg-panel: #0a0a0a; --border: #252525; --text-main: #FFFFFF; --text-dim: #999999; --green: #00ff9d; --red: #ff3b3b; --purple: #d500f9; }}
232
- body {{ margin: 0; padding: 0; background-color: var(--bg-base); color: var(--text-main); font-family: 'Inter', sans-serif; overflow: hidden; height: 100vh; width: 100vw; }}
233
- .layout {{ display: grid; grid-template-rows: 34px 1fr 1fr; grid-template-columns: 3fr 1fr; gap: 1px; background-color: var(--border); height: 100vh; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  .panel {{ background: var(--bg-panel); display: flex; flex-direction: column; overflow: hidden; }}
235
- .status-bar {{ grid-column: 1 / 3; grid-row: 1 / 2; background: var(--bg-panel); display: flex; align-items: center; justify-content: space-between; padding: 0 12px; font-family: 'JetBrains Mono', monospace; font-size: 12px; border-bottom: 1px solid var(--border); }}
236
- .live-dot {{ width: 8px; height: 8px; background-color: var(--green); border-radius: 50%; display: inline-block; margin-right: 8px; box-shadow: 0 0 8px var(--green); }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  #p-chart {{ grid-column: 1 / 2; grid-row: 2 / 3; }}
238
- #p-bottom {{ grid-column: 1 / 2; grid-row: 3 / 4; display: grid; grid-template-columns: 1fr; gap: 1px; background: var(--border); }}
239
- #p-sidebar {{ grid-column: 2 / 3; grid-row: 2 / 4; padding: 15px; display: flex; flex-direction: column; gap: 20px; border-left: 1px solid var(--border); }}
240
- .chart-header {{ height: 24px; display: flex; align-items: center; padding-left: 12px; font-size: 10px; font-weight: 700; color: var(--text-dim); background: #050505; border-bottom: 1px solid #151515; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  .data-group {{ display: flex; flex-direction: column; gap: 4px; }}
242
- .label {{ font-size: 10px; color: var(--text-dim); font-weight: 600; text-transform: uppercase; }}
243
  .value {{ font-family: 'JetBrains Mono', monospace; font-size: 20px; font-weight: 700; color: #fff; }}
244
  .value-lg {{ font-size: 26px; }}
245
  .value-sub {{ font-family: 'JetBrains Mono', monospace; font-size: 11px; margin-top: 2px; color: #666; }}
 
246
  .divider {{ height: 1px; background: var(--border); width: 100%; }}
247
- .c-purple {{ color: var(--purple); }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  </style>
249
  </head>
250
  <body>
 
251
  <div class="layout">
252
  <div class="status-bar">
253
- <div><span class="live-dot"></span><span style="font-weight:700;">{SYMBOL_KRAKEN} MATH MODEL</span></div>
254
- <div id="price-ticker">---</div>
 
 
 
 
255
  </div>
 
256
  <div id="p-chart" class="panel">
257
- <div class="chart-header">PRICE (BLUE) vs PREDICTION (YELLOW) vs VWAP (WHITE)</div>
258
- <div id="tv-price" style="flex: 1;"></div>
259
  </div>
260
- <div id="p-bottom" class="panel">
261
- <div class="chart-header">1M KLINE + WALLS + GHOST PREDICTION (PURPLE)</div>
262
- <div id="tv-candles" style="flex: 1;"></div>
 
 
 
 
 
 
 
263
  </div>
 
264
  <div id="p-sidebar" class="panel">
 
265
  <div class="data-group">
266
- <span class="label">Predicted Close (1m)</span>
267
  <div style="display:flex; align-items: baseline; gap: 10px;">
268
  <span id="proj-pct" class="value value-lg">--%</span>
269
- <span id="proj-val" class="value-sub c-purple">---</span>
270
  </div>
271
- <span class="label" style="margin-top:4px;">MicroPrice + OFI Impact</span>
272
  </div>
 
273
  <div class="divider"></div>
 
274
  <div class="data-group">
275
- <span class="label">VWAP Divergence</span>
276
- <span id="vwap-div" class="value">0.00</span>
277
  </div>
 
278
  <div class="divider"></div>
 
279
  <div class="data-group">
280
- <span class="label">Kyle's Lambda (Liq. Cost)</span>
281
- <span id="lambda-val" class="value">0.00</span>
282
- <span class="value-sub">Impact per Volume Unit</span>
 
 
 
 
 
 
 
 
 
 
 
283
  </div>
284
  </div>
285
  </div>
 
286
  <script>
287
- const dom = {{ ticker: document.getElementById('price-ticker'), projVal: document.getElementById('proj-val'), projPct: document.getElementById('proj-pct'), vwapDiv: document.getElementById('vwap-div'), lambdaVal: document.getElementById('lambda-val') }};
288
- const chartOpts = {{ layout: {{ background: {{ type: 'solid', color: '#0a0a0a' }}, textColor: '#888', fontFamily: 'JetBrains Mono' }}, grid: {{ vertLines: {{ color: '#151515' }}, horzLines: {{ color: '#151515' }} }}, crosshair: {{ mode: 1 }} }};
289
-
290
- const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts);
291
- const priceSeries = priceChart.addLineSeries({{ color: '#2979ff', lineWidth: 2 }});
292
- const predSeries = priceChart.addLineSeries({{ color: '#ffeb3b', lineWidth: 2, lineStyle: 2 }});
293
- const vwapSeries = priceChart.addLineSeries({{ color: 'rgba(255,255,255,0.3)', lineWidth: 1, lineStyle: 0 }});
294
-
295
- const candleChart = LightweightCharts.createChart(document.getElementById('tv-candles'), chartOpts);
296
- const candleSeries = candleChart.addCandlestickSeries({{ upColor: '#00ff9d', downColor: '#ff3b3b', borderVisible: false }});
297
- const ghostSeries = candleChart.addCandlestickSeries({{ upColor: 'rgba(213, 0, 249, 0.5)', downColor: 'rgba(213, 0, 249, 0.5)', borderVisible: true, borderColor: '#d500f9' }});
298
-
299
- let activeWallLines = [];
300
-
301
- new ResizeObserver(e => {{
302
- const t1 = document.getElementById('tv-price');
303
- const t2 = document.getElementById('tv-candles');
304
- priceChart.applyOptions({{ width: t1.clientWidth, height: t1.clientHeight }});
305
- candleChart.applyOptions({{ width: t2.clientWidth, height: t2.clientHeight }});
306
- }}).observe(document.body);
307
-
308
- function connect() {{
309
- const ws = new WebSocket((location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/ws');
310
- ws.onmessage = (e) => {{
311
- const data = JSON.parse(e.data);
312
- if (data.error) return;
313
-
314
- if (data.history.length) {{
315
- const hist = data.history.map(d => ({{ time: Math.floor(d.t), value: d.p }}));
316
- const cleanHist = [...new Map(hist.map(i => [i.time, i])).values()];
317
- priceSeries.setData(cleanHist);
318
- dom.ticker.innerText = cleanHist[cleanHist.length-1].value.toFixed(2);
319
-
320
- if(data.analysis) {{
321
- predSeries.setData([
322
- cleanHist[cleanHist.length-1],
323
- {{ time: cleanHist[cleanHist.length-1].time + 60, value: data.analysis.projected }}
324
- ]);
325
-
326
- vwapSeries.setData([
327
- {{ time: cleanHist[0].time, value: data.analysis.vwap }},
328
- {{ time: cleanHist[cleanHist.length-1].time, value: data.analysis.vwap }}
329
- ]);
330
-
331
- dom.lambdaVal.innerText = (data.analysis.lambda * 1000).toFixed(4);
332
- const vwapDiff = cleanHist[cleanHist.length-1].value - data.analysis.vwap;
333
- dom.vwapDiv.innerText = vwapDiff.toFixed(2);
334
- dom.vwapDiv.style.color = vwapDiff > 0 ? '#ff3b3b' : '#00ff9d';
335
- }}
336
- }}
337
 
338
- if (data.ohlc && data.ohlc.length) {{
339
- candleSeries.setData(data.ohlc.map(c => ({{ time: c.time, open: c.open, high: c.high, low: c.low, close: c.close }})));
340
- }}
 
 
 
 
341
 
342
- if (data.walls) {{
343
- activeWallLines.forEach(l => candleSeries.removePriceLine(l));
344
- activeWallLines = [];
345
- data.walls.bids.forEach(w => {{
346
- activeWallLines.push(candleSeries.createPriceLine({{ price: w.p, color: '#00ff9d', lineWidth: 2, lineStyle: 2, axisLabelVisible: true, title: 'BID WALL' }}));
347
- }});
348
- data.walls.asks.forEach(w => {{
349
- activeWallLines.push(candleSeries.createPriceLine({{ price: w.p, color: '#ff3b3b', lineWidth: 2, lineStyle: 2, axisLabelVisible: true, title: 'ASK WALL' }}));
350
- }});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
 
353
- if (data.pred_candle) {{
354
- ghostSeries.setData([data.pred_candle]);
355
- const currentP = parseFloat(dom.ticker.innerText);
356
- const pClose = data.pred_candle.close;
357
- dom.projVal.innerText = pClose.toFixed(2);
358
- const pct = ((pClose - currentP) / currentP) * 100;
359
- dom.projPct.innerText = (pct >= 0 ? "+" : "") + pct.toFixed(3) + "%";
360
- dom.projPct.style.color = pct >= 0 ? "var(--green)" : "var(--red)";
361
- }}
362
- }};
363
- ws.onclose = () => setTimeout(connect, 2000);
364
- }}
365
- connect();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  </script>
367
  </body>
368
  </html>
@@ -370,6 +560,7 @@ HTML_PAGE = f"""
370
 
371
  async def kraken_worker():
372
  global market_state
 
373
  try:
374
  async with aiohttp.ClientSession() as session:
375
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
@@ -377,22 +568,40 @@ async def kraken_worker():
377
  if response.status == 200:
378
  data = await response.json()
379
  if 'result' in data:
380
- raw = list(data['result'].values())[0]
381
- market_state['ohlc_history'] = [
382
- {'time': int(c[0]), 'open': float(c[1]), 'high': float(c[2]), 'low': float(c[3]), 'close': float(c[4])}
383
- for c in raw[-120:]
384
- ]
 
 
 
 
 
 
 
 
 
385
  except Exception as e:
386
- logging.error(f"Init Error: {e}")
387
 
388
  while True:
389
  try:
390
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
391
- logging.info(f"Connected to Kraken ({SYMBOL_KRAKEN})")
392
 
393
- await ws.send(json.dumps({"method": "subscribe", "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 100}}))
394
- await ws.send(json.dumps({"method": "subscribe", "params": {"channel": "trade", "symbol": [SYMBOL_KRAKEN]}}))
395
- await ws.send(json.dumps({"method": "subscribe", "params": {"channel": "ohlc", "symbol": [SYMBOL_KRAKEN], "interval": 1}}))
 
 
 
 
 
 
 
 
 
396
 
397
  async for message in ws:
398
  payload = json.loads(message)
@@ -402,59 +611,68 @@ async def kraken_worker():
402
  if channel == "book":
403
  for item in data:
404
  for bid in item.get('bids', []):
405
- market_state['bids'][float(bid['price'])] = float(bid['qty'])
 
 
406
  for ask in item.get('asks', []):
407
- market_state['asks'][float(ask['price'])] = float(ask['qty'])
 
 
408
 
409
- market_state['bids'] = {k: v for k, v in market_state['bids'].items() if v > 0}
410
- market_state['asks'] = {k: v for k, v in market_state['asks'].items() if v > 0}
411
-
412
  if market_state['bids'] and market_state['asks']:
413
  best_bid = max(market_state['bids'].keys())
414
  best_ask = min(market_state['asks'].keys())
415
- market_state['current_mid'] = (best_bid + best_ask) / 2
 
 
416
  market_state['ready'] = True
417
-
 
 
 
 
 
 
418
  elif channel == "trade":
419
  for trade in data:
420
  try:
421
- t_obj = {
422
- 't': time.time(),
423
- 'p': float(trade['price']),
424
- 'q': float(trade['qty']),
425
- 'side': trade['side']
426
- }
427
- market_state['trade_history'].append(t_obj)
428
  except: pass
429
 
430
  elif channel == "ohlc":
431
- for c in data:
432
- c_data = {
433
- 'time': int(float(c['endtime'])),
434
- 'open': float(c['open']),
435
- 'high': float(c['high']),
436
- 'low': float(c['low']),
437
- 'close': float(c['close'])
438
- }
439
- if market_state['ohlc_history'] and market_state['ohlc_history'][-1]['time'] == c_data['time']:
440
- market_state['ohlc_history'][-1] = c_data
441
- else:
442
- market_state['ohlc_history'].append(c_data)
443
- if len(market_state['ohlc_history']) > 100: market_state['ohlc_history'].pop(0)
 
 
 
 
444
 
445
  except Exception as e:
446
- logging.warning(f"Reconnecting: {e}")
447
- await asyncio.sleep(2)
448
 
449
  async def broadcast_worker():
450
  while True:
451
  if connected_clients and market_state['ready']:
452
  payload = process_market_data()
453
- if "error" not in payload:
454
- msg = json.dumps(payload)
455
- for ws in list(connected_clients):
456
- try: await ws.send_str(msg)
457
- except: pass
458
  await asyncio.sleep(BROADCAST_RATE)
459
 
460
  async def websocket_handler(request):
@@ -478,6 +696,8 @@ async def start_background(app):
478
  async def cleanup_background(app):
479
  app['kraken_task'].cancel()
480
  app['broadcast_task'].cancel()
 
 
481
 
482
  async def main():
483
  app = web.Application()
@@ -489,7 +709,7 @@ async def main():
489
  await runner.setup()
490
  site = web.TCPSite(runner, '0.0.0.0', PORT)
491
  await site.start()
492
- print(f"Quant Dashboard: http://localhost:{PORT}")
493
  await asyncio.Event().wait()
494
 
495
  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 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 process_market_data():
100
  if not market_state['ready']: return {"error": "Initializing..."}
101
 
102
  mid = market_state['current_mid']
 
 
 
 
 
 
 
103
 
104
+ now = time.time()
105
+ if now - market_state['current_vol_window']['start'] >= 1.0:
106
+ market_state['trade_vol_history'].append({
107
+ 't': now,
108
+ 'buy': market_state['current_vol_window']['buy'],
109
+ 'sell': market_state['current_vol_window']['sell']
110
+ })
111
+ if len(market_state['trade_vol_history']) > 60:
112
+ market_state['trade_vol_history'].pop(0)
113
+ market_state['current_vol_window'] = {"buy": 0.0, "sell": 0.0, "start": now}
114
+
115
+ sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
116
+ sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
117
 
118
+ if not sorted_bids or not sorted_asks: return {"error": "Empty Book"}
119
+
120
+ best_bid = sorted_bids[0][0]
121
+ best_ask = sorted_asks[0][0]
122
+
123
+ bid_walls = detect_anomalies(sorted_bids, WALL_LOOKBACK)
124
+ ask_walls = detect_anomalies(sorted_asks, WALL_LOOKBACK)
125
+
126
+ d_b_x, d_b_y, cum = [], [], 0
127
+ for p, q in sorted_bids[:300]:
128
+ d = mid - p
129
+ if d >= 0:
130
+ cum += q
131
+ d_b_x.append(d); d_b_y.append(cum)
132
+
133
+ d_a_x, d_a_y, cum = [], [], 0
134
+ for p, q in sorted_asks[:300]:
135
+ d = p - mid
136
+ if d >= 0:
137
+ cum += q
138
+ d_a_x.append(d); d_a_y.append(cum)
139
+
140
+ diff_x, diff_y_net = [], []
141
+ chart_bids, chart_asks = [], []
142
 
143
+ if d_b_x and d_a_x:
144
+ max_dist = min(d_b_x[-1], d_a_x[-1])
145
+ step_size = max_dist / 100
146
+ steps = [i * step_size for i in range(1, 101)]
147
 
148
+ for s in steps:
149
+ idx_b = bisect.bisect_right(d_b_x, s)
150
+ vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
151
+ idx_a = bisect.bisect_right(d_a_x, s)
152
+ vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
153
+
154
+ diff_x.append(s)
155
+ diff_y_net.append(vol_b - vol_a)
156
+ chart_bids.append(vol_b)
157
+ chart_asks.append(vol_a)
158
+
159
+ analysis = calculate_micro_price_structure(
160
+ diff_x, diff_y_net, mid, best_bid, best_ask,
161
+ {"bids": bid_walls, "asks": ask_walls}
162
+ )
163
 
164
+ if analysis:
165
+ if not market_state['pred_history'] or (now - market_state['pred_history'][-1]['t'] > 0.5):
166
+ market_state['pred_history'].append({'t': now, 'p': analysis['projected']})
167
+ if len(market_state['pred_history']) > HISTORY_LENGTH:
168
+ market_state['pred_history'].pop(0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
  return {
171
  "mid": mid,
172
  "history": market_state['history'],
173
+ "pred_history": market_state['pred_history'],
174
+ "trade_history": market_state['trade_vol_history'],
175
  "ohlc": market_state['ohlc_history'],
176
+ "depth_x": diff_x,
177
+ "depth_net": diff_y_net,
178
+ "depth_bids": chart_bids,
179
+ "depth_asks": chart_asks,
180
  "analysis": analysis,
181
  "walls": {"bids": bid_walls, "asks": ask_walls}
182
  }
 
186
  <html lang="en">
187
  <head>
188
  <meta charset="UTF-8">
189
+ <title>{SYMBOL_KRAKEN}</title>
190
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
191
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
192
  <style>
193
+ :root {{
194
+ --bg-base: #000000;
195
+ --bg-panel: #0a0a0a;
196
+ --border: #252525;
197
+ --text-main: #FFFFFF;
198
+ --text-dim: #999999;
199
+ --green: #00ff9d;
200
+ --red: #ff3b3b;
201
+ --blue: #2979ff;
202
+ --yellow: #ffeb3b;
203
+ }}
204
+ body {{
205
+ margin: 0; padding: 0;
206
+ background-color: var(--bg-base);
207
+ color: var(--text-main);
208
+ font-family: 'Inter', sans-serif;
209
+ overflow: hidden;
210
+ height: 100vh; width: 100vw;
211
+ }}
212
+
213
+ .layout {{
214
+ display: grid;
215
+ grid-template-rows: 34px 1fr 1fr;
216
+ grid-template-columns: 3fr 1fr;
217
+ gap: 1px;
218
+ background-color: var(--border);
219
+ height: 100vh;
220
+ box-sizing: border-box;
221
+ }}
222
+
223
  .panel {{ background: var(--bg-panel); display: flex; flex-direction: column; overflow: hidden; }}
224
+
225
+ .status-bar {{
226
+ grid-column: 1 / 3;
227
+ grid-row: 1 / 2;
228
+ background: var(--bg-panel);
229
+ display: flex;
230
+ align-items: center;
231
+ justify-content: space-between;
232
+ padding: 0 12px;
233
+ font-family: 'JetBrains Mono', monospace;
234
+ font-size: 12px;
235
+ text-transform: uppercase;
236
+ border-bottom: 1px solid var(--border);
237
+ z-index: 50;
238
+ }}
239
+ .status-left {{ display: flex; gap: 20px; align-items: center; }}
240
+ .live-dot {{ width: 8px; height: 8px; background-color: var(--green); border-radius: 50%; display: inline-block; box-shadow: 0 0 8px var(--green); }}
241
+ .ticker-val {{ font-weight: 700; color: #fff; font-size: 13px; }}
242
+
243
  #p-chart {{ grid-column: 1 / 2; grid-row: 2 / 3; }}
244
+
245
+ #p-bottom {{
246
+ grid-column: 1 / 2; grid-row: 3 / 4;
247
+ display: grid;
248
+ grid-template-columns: 1fr 1fr;
249
+ gap: 1px;
250
+ background: var(--border);
251
+ }}
252
+ .bottom-sub {{ background: var(--bg-panel); display: flex; flex-direction: column; position: relative; }}
253
+
254
+ #p-sidebar {{
255
+ grid-column: 2 / 3;
256
+ grid-row: 2 / 4;
257
+ padding: 15px;
258
+ display: flex;
259
+ flex-direction: column;
260
+ gap: 15px;
261
+ border-left: 1px solid var(--border);
262
+ overflow: hidden;
263
+ }}
264
+
265
+ .chart-header {{
266
+ height: 24px;
267
+ min-height: 24px;
268
+ display: flex;
269
+ align-items: center;
270
+ padding-left: 12px;
271
+ font-size: 10px;
272
+ font-weight: 700;
273
+ color: var(--text-dim);
274
+ background: #050505;
275
+ border-bottom: 1px solid #151515;
276
+ letter-spacing: 0.5px;
277
+ }}
278
+
279
  .data-group {{ display: flex; flex-direction: column; gap: 4px; }}
280
+ .label {{ font-size: 10px; color: var(--text-dim); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }}
281
  .value {{ font-family: 'JetBrains Mono', monospace; font-size: 20px; font-weight: 700; color: #fff; }}
282
  .value-lg {{ font-size: 26px; }}
283
  .value-sub {{ font-family: 'JetBrains Mono', monospace; font-size: 11px; margin-top: 2px; color: #666; }}
284
+
285
  .divider {{ height: 1px; background: var(--border); width: 100%; }}
286
+ .c-green {{ color: var(--green); }}
287
+ .c-red {{ color: var(--red); }}
288
+ .c-dim {{ color: var(--text-dim); }}
289
+
290
+ .list-container {{ display: flex; flex-direction: column; gap: 8px; overflow-y: auto; height: 100px; }}
291
+ .list-item {{
292
+ display: flex; justify-content: space-between;
293
+ font-family: 'JetBrains Mono', monospace;
294
+ font-size: 11px;
295
+ border-bottom: 1px solid #151515;
296
+ padding-bottom: 4px;
297
+ }}
298
+ .list-item span:first-child {{ color: #e0e0e0; }}
299
+ .list-item:last-child {{ border: none; }}
300
+
301
+ .sidebar-chart-box {{
302
+ flex: 1;
303
+ display: flex;
304
+ flex-direction: column;
305
+ min-height: 0;
306
+ }}
307
+ .mini-chart {{
308
+ flex: 1;
309
+ background: rgba(255,255,255,0.02);
310
+ border: 1px solid var(--border);
311
+ border-radius: 4px;
312
+ }}
313
  </style>
314
  </head>
315
  <body>
316
+
317
  <div class="layout">
318
  <div class="status-bar">
319
+ <div class="status-left">
320
+ <span class="live-dot"></span>
321
+ <span style="font-weight:700; color:#fff;">{SYMBOL_KRAKEN}</span>
322
+ <span id="price-ticker" class="ticker-val">---</span>
323
+ </div>
324
+ <div class="status-right" id="clock">00:00:00 UTC</div>
325
  </div>
326
+
327
  <div id="p-chart" class="panel">
328
+ <div class="chart-header">PRICE ACTION (BLUE) // PREDICTION (YELLOW)</div>
329
+ <div id="tv-price" style="flex: 1; width: 100%;"></div>
330
  </div>
331
+
332
+ <div id="p-bottom">
333
+ <div class="bottom-sub">
334
+ <div class="chart-header">1M KLINE (KRAKEN OHLC)</div>
335
+ <div id="tv-candles" style="flex: 1; width: 100%;"></div>
336
+ </div>
337
+ <div class="bottom-sub">
338
+ <div class="chart-header">ORDER FLOW IMBALANCE</div>
339
+ <div id="tv-net" style="flex: 1; width: 100%;"></div>
340
+ </div>
341
  </div>
342
+
343
  <div id="p-sidebar" class="panel">
344
+
345
  <div class="data-group">
346
+ <span class="label">Micro-Price Delta</span>
347
  <div style="display:flex; align-items: baseline; gap: 10px;">
348
  <span id="proj-pct" class="value value-lg">--%</span>
349
+ <span id="proj-val" class="value-sub">---</span>
350
  </div>
 
351
  </div>
352
+
353
  <div class="divider"></div>
354
+
355
  <div class="data-group">
356
+ <span class="label">OFI Imbalance Ratio</span>
357
+ <span id="score-val" class="value">0.00</span>
358
  </div>
359
+
360
  <div class="divider"></div>
361
+
362
  <div class="data-group">
363
+ <span class="label">Detected Walls (Z > 3.0)</span>
364
+ <div id="wall-list" class="list-container">
365
+ <span class="c-dim" style="font-size: 11px;">Scanning...</span>
366
+ </div>
367
+ </div>
368
+
369
+ <div class="sidebar-chart-box">
370
+ <span class="label" style="margin-bottom:4px;">Real-time Volume Ticks</span>
371
+ <div id="sidebar-vol" class="mini-chart"></div>
372
+ </div>
373
+
374
+ <div class="sidebar-chart-box">
375
+ <span class="label" style="margin-bottom:4px;">Liquidity Density</span>
376
+ <div id="sidebar-density" class="mini-chart"></div>
377
  </div>
378
  </div>
379
  </div>
380
+
381
  <script>
382
+ setInterval(() => {{
383
+ const now = new Date();
384
+ document.getElementById('clock').innerText = now.toISOString().split('T')[1].split('.')[0] + ' UTC';
385
+ }}, 1000);
386
+
387
+ document.addEventListener('DOMContentLoaded', () => {{
388
+ const dom = {{
389
+ ticker: document.getElementById('price-ticker'),
390
+ score: document.getElementById('score-val'),
391
+ projVal: document.getElementById('proj-val'),
392
+ projPct: document.getElementById('proj-pct'),
393
+ wallList: document.getElementById('wall-list')
394
+ }};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
 
396
+ const chartOpts = {{
397
+ layout: {{ background: {{ type: 'solid', color: '#0a0a0a' }}, textColor: '#888', fontFamily: 'JetBrains Mono' }},
398
+ grid: {{ vertLines: {{ color: '#151515' }}, horzLines: {{ color: '#151515' }} }},
399
+ rightPriceScale: {{ borderColor: '#222', scaleMargins: {{ top: 0.1, bottom: 0.1 }} }},
400
+ timeScale: {{ borderColor: '#222', timeVisible: true, secondsVisible: true }},
401
+ crosshair: {{ mode: 1, vertLine: {{ color: '#444', labelBackgroundColor: '#444' }}, horzLine: {{ color: '#444', labelBackgroundColor: '#444' }} }}
402
+ }};
403
 
404
+ const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts);
405
+ const priceSeries = priceChart.addLineSeries({{ color: '#2979ff', lineWidth: 2, title: 'Price' }});
406
+ const predSeries = priceChart.addLineSeries({{ color: '#ffeb3b', lineWidth: 2, lineStyle: 2, title: 'Forecast' }});
407
+
408
+ const candleChart = LightweightCharts.createChart(document.getElementById('tv-candles'), {{
409
+ ...chartOpts,
410
+ timeScale: {{ timeVisible: true, secondsVisible: false }}
411
+ }});
412
+ const candleSeries = candleChart.addCandlestickSeries({{
413
+ upColor: '#00ff9d', downColor: '#ff3b3b', borderVisible: false, wickUpColor: '#00ff9d', wickDownColor: '#ff3b3b'
414
+ }});
415
+
416
+ const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{
417
+ ...chartOpts, localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
418
+ }});
419
+ const netSeries = netChart.addHistogramSeries({{ color: '#2979ff' }});
420
+
421
+ const volChart = LightweightCharts.createChart(document.getElementById('sidebar-vol'), {{
422
+ ...chartOpts,
423
+ grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }},
424
+ rightPriceScale: {{ visible: false }},
425
+ timeScale: {{ visible: false }},
426
+ handleScroll: false, handleScale: false
427
+ }});
428
+ const volBuySeries = volChart.addHistogramSeries({{ color: '#00ff9d' }});
429
+ const volSellSeries = volChart.addHistogramSeries({{ color: '#ff3b3b' }});
430
+
431
+ const denChart = LightweightCharts.createChart(document.getElementById('sidebar-density'), {{
432
+ ...chartOpts,
433
+ grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }},
434
+ rightPriceScale: {{ visible: false }},
435
+ timeScale: {{ visible: false }},
436
+ handleScroll: false, handleScale: false
437
+ }});
438
+ const bidSeries = denChart.addAreaSeries({{ lineColor: '#00ff9d', topColor: 'rgba(0, 255, 157, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
439
+ const askSeries = denChart.addAreaSeries({{ lineColor: '#ff3b3b', topColor: 'rgba(255, 59, 59, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
440
+
441
+ let activeLines = [];
442
+
443
+ new ResizeObserver(entries => {{
444
+ for(let entry of entries) {{
445
+ const {{width, height}} = entry.contentRect;
446
+ if(entry.target.id === 'tv-price') priceChart.applyOptions({{width, height}});
447
+ if(entry.target.id === 'tv-candles') candleChart.applyOptions({{width, height}});
448
+ if(entry.target.id === 'tv-net') netChart.applyOptions({{width, height}});
449
+ if(entry.target.id === 'sidebar-vol') volChart.applyOptions({{width, height}});
450
+ if(entry.target.id === 'sidebar-density') denChart.applyOptions({{width, height}});
451
  }}
452
+ }}).observe(document.body);
453
+
454
+ ['tv-price', 'tv-candles', 'tv-net', 'sidebar-vol', 'sidebar-density'].forEach(id => {{
455
+ new ResizeObserver(e => {{
456
+ const t = document.getElementById(id);
457
+ if (t.clientWidth && t.clientHeight) {{
458
+ if(id === 'tv-price') priceChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
459
+ if(id === 'tv-candles') candleChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
460
+ if(id === 'tv-net') netChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
461
+ if(id === 'sidebar-vol') volChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
462
+ if(id === 'sidebar-density') denChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
463
+ }}
464
+ }}).observe(document.getElementById(id));
465
+ }});
466
+
467
+ function connect() {{
468
+ const ws = new WebSocket((location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/ws');
469
+
470
+ ws.onmessage = (e) => {{
471
+ const data = JSON.parse(e.data);
472
+ if (data.error) return;
473
+
474
+ if (data.history.length) {{
475
+ const hist = data.history.map(d => ({{ time: Math.floor(d.t), value: d.p }}));
476
+ const cleanHist = [...new Map(hist.map(i => [i.time, i])).values()];
477
+ priceSeries.setData(cleanHist);
478
+
479
+ const lastP = cleanHist[cleanHist.length-1].value;
480
+ dom.ticker.innerText = lastP.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
481
+
482
+ if (data.analysis) {{
483
+ const proj = data.analysis.projected;
484
+ const rho = data.analysis.rho;
485
+
486
+ predSeries.setData([
487
+ cleanHist[cleanHist.length-1],
488
+ {{ time: cleanHist[cleanHist.length-1].time + 60, value: proj }}
489
+ ]);
490
+
491
+ const pct = ((proj - lastP) / lastP) * 100;
492
+ const sign = pct >= 0 ? "+" : "";
493
+
494
+ dom.projPct.innerText = `${{sign}}${{pct.toFixed(4)}}%`;
495
+ dom.projPct.style.color = pct >= 0 ? "var(--green)" : "var(--red)";
496
+ dom.projVal.innerText = proj.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
497
+ dom.score.innerText = rho.toFixed(3);
498
+ dom.score.style.color = rho > 0 ? "var(--green)" : (rho < 0 ? "var(--red)" : "var(--text-main)");
499
+ }}
500
+ }}
501
 
502
+ if (data.ohlc && data.ohlc.length) {{
503
+ const candles = data.ohlc.map(c => ({{
504
+ time: c.time,
505
+ open: c.open,
506
+ high: c.high,
507
+ low: c.low,
508
+ close: c.close
509
+ }}));
510
+ const uniqueCandles = [...new Map(candles.map(i => [i.time, i])).values()];
511
+ candleSeries.setData(uniqueCandles);
512
+ }}
513
+
514
+ if (data.walls) {{
515
+ activeLines.forEach(l => priceSeries.removePriceLine(l));
516
+ activeLines = [];
517
+ let html = "";
518
+ const addWall = (w, type) => {{
519
+ const color = type === 'BID' ? '#00ff9d' : '#ff3b3b';
520
+ activeLines.push(priceSeries.createPriceLine({{ price: w.price, color: color, lineWidth: 1, lineStyle: 2, axisLabelVisible: false }}));
521
+ 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>`;
522
+ }};
523
+ data.walls.asks.forEach(w => addWall(w, 'ASK'));
524
+ data.walls.bids.forEach(w => addWall(w, 'BID'));
525
+ dom.wallList.innerHTML = html || '<span class="c-dim" style="font-size:11px">Scanning...</span>';
526
+ }}
527
+
528
+ if (data.trade_history && data.trade_history.length) {{
529
+ const buyData = [], sellData = [];
530
+ data.trade_history.forEach(t => {{
531
+ const time = Math.floor(t.t);
532
+ buyData.push({{ time: time, value: t.buy }});
533
+ sellData.push({{ time: time, value: t.sell }});
534
+ }});
535
+ volBuySeries.setData([...new Map(buyData.map(i => [i.time, i])).values()]);
536
+ volSellSeries.setData([...new Map(sellData.map(i => [i.time, i])).values()]);
537
+ }}
538
+
539
+ if (data.depth_x.length) {{
540
+ const bids = [], asks = [], nets = [];
541
+ for(let i=0; i<data.depth_x.length; i++) {{
542
+ const t = data.depth_x[i];
543
+ bids.push({{ time: t, value: data.depth_bids[i] }});
544
+ asks.push({{ time: t, value: data.depth_asks[i] }});
545
+ nets.push({{ time: t, value: data.depth_net[i], color: data.depth_net[i] > 0 ? '#00ff9d' : '#ff3b3b' }});
546
+ }}
547
+ bidSeries.setData(bids);
548
+ askSeries.setData(asks);
549
+ netSeries.setData(nets);
550
+ }}
551
+ }};
552
+ ws.onclose = () => setTimeout(connect, 2000);
553
+ }}
554
+ connect();
555
+ }});
556
  </script>
557
  </body>
558
  </html>
 
560
 
561
  async def kraken_worker():
562
  global market_state
563
+
564
  try:
565
  async with aiohttp.ClientSession() as session:
566
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
 
568
  if response.status == 200:
569
  data = await response.json()
570
  if 'result' in data:
571
+ for key in data['result']:
572
+ if key != 'last':
573
+ raw_candles = data['result'][key]
574
+ market_state['ohlc_history'] = [
575
+ {
576
+ 'time': int(c[0]),
577
+ 'open': float(c[1]),
578
+ 'high': float(c[2]),
579
+ 'low': float(c[3]),
580
+ 'close': float(c[4])
581
+ }
582
+ for c in raw_candles[-120:]
583
+ ]
584
+ break
585
  except Exception as e:
586
+ logging.error(f"History fetch failed: {e}")
587
 
588
  while True:
589
  try:
590
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
591
+ logging.info(f"🔌 Connected to Kraken ({SYMBOL_KRAKEN})")
592
 
593
+ await ws.send(json.dumps({
594
+ "method": "subscribe",
595
+ "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}
596
+ }))
597
+ await ws.send(json.dumps({
598
+ "method": "subscribe",
599
+ "params": {"channel": "trade", "symbol": [SYMBOL_KRAKEN]}
600
+ }))
601
+ await ws.send(json.dumps({
602
+ "method": "subscribe",
603
+ "params": {"channel": "ohlc", "symbol": [SYMBOL_KRAKEN], "interval": 1}
604
+ }))
605
 
606
  async for message in ws:
607
  payload = json.loads(message)
 
611
  if channel == "book":
612
  for item in data:
613
  for bid in item.get('bids', []):
614
+ q, p = float(bid['qty']), float(bid['price'])
615
+ if q == 0: market_state['bids'].pop(p, None)
616
+ else: market_state['bids'][p] = q
617
  for ask in item.get('asks', []):
618
+ q, p = float(ask['qty']), float(ask['price'])
619
+ if q == 0: market_state['asks'].pop(p, None)
620
+ else: market_state['asks'][p] = q
621
 
 
 
 
622
  if market_state['bids'] and market_state['asks']:
623
  best_bid = max(market_state['bids'].keys())
624
  best_ask = min(market_state['asks'].keys())
625
+ mid = (best_bid + best_ask) / 2
626
+ market_state['prev_mid'] = market_state['current_mid']
627
+ market_state['current_mid'] = mid
628
  market_state['ready'] = True
629
+
630
+ now = time.time()
631
+ if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
632
+ market_state['history'].append({'t': now, 'p': mid})
633
+ if len(market_state['history']) > HISTORY_LENGTH:
634
+ market_state['history'].pop(0)
635
+
636
  elif channel == "trade":
637
  for trade in data:
638
  try:
639
+ qty = float(trade['qty'])
640
+ side = trade['side']
641
+ if side == 'buy': market_state['current_vol_window']['buy'] += qty
642
+ else: market_state['current_vol_window']['sell'] += qty
 
 
 
643
  except: pass
644
 
645
  elif channel == "ohlc":
646
+ for candle in data:
647
+ try:
648
+ c_data = {
649
+ 'time': int(float(candle['endtime'])),
650
+ 'open': float(candle['open']),
651
+ 'high': float(candle['high']),
652
+ 'low': float(candle['low']),
653
+ 'close': float(candle['close'])
654
+ }
655
+ if market_state['ohlc_history'] and market_state['ohlc_history'][-1]['time'] == c_data['time']:
656
+ market_state['ohlc_history'][-1] = c_data
657
+ else:
658
+ market_state['ohlc_history'].append(c_data)
659
+ if len(market_state['ohlc_history']) > 100:
660
+ market_state['ohlc_history'].pop(0)
661
+ except Exception as e:
662
+ pass
663
 
664
  except Exception as e:
665
+ logging.warning(f"⚠️ Reconnecting: {e}")
666
+ await asyncio.sleep(3)
667
 
668
  async def broadcast_worker():
669
  while True:
670
  if connected_clients and market_state['ready']:
671
  payload = process_market_data()
672
+ msg = json.dumps(payload)
673
+ for ws in list(connected_clients):
674
+ try: await ws.send_str(msg)
675
+ except: pass
 
676
  await asyncio.sleep(BROADCAST_RATE)
677
 
678
  async def websocket_handler(request):
 
696
  async def cleanup_background(app):
697
  app['kraken_task'].cancel()
698
  app['broadcast_task'].cancel()
699
+ try: await app['kraken_task']; await app['broadcast_task']
700
+ except: pass
701
 
702
  async def main():
703
  app = web.Application()
 
709
  await runner.setup()
710
  site = web.TCPSite(runner, '0.0.0.0', PORT)
711
  await site.start()
712
+ print(f"🚀 Quant Dashboard: http://localhost:{PORT}")
713
  await asyncio.Event().wait()
714
 
715
  if __name__ == "__main__":