Alvin3y1 commited on
Commit
d6a6045
·
verified ·
1 Parent(s): 3a80860

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +729 -248
app.py CHANGED
@@ -10,22 +10,20 @@ import websockets
10
 
11
  SYMBOL_KRAKEN = "BTC/USD"
12
  PORT = 7860
13
- HISTORY_LENGTH = 300
14
  BROADCAST_RATE = 0.1
15
- DECAY_LAMBDA = 100.0
16
- IMPACT_SENSITIVITY = 0.5
17
-
18
- # Wall Detection Parameters
19
- Z_SCORE_THRESHOLD = 3.0 # Statistical significance threshold (3 sigma)
20
- WALL_LOOKBACK = 200 # How many order book levels to scan
21
 
22
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
23
 
24
  market_state = {
25
  "bids": {},
26
  "asks": {},
27
- "history": [],
28
- "pred_history": [],
29
  "current_mid": 0.0,
30
  "prev_mid": 0.0,
31
  "ready": False
@@ -34,21 +32,15 @@ market_state = {
34
  connected_clients = set()
35
 
36
  def detect_anomalies(orders, scan_depth):
37
- """
38
- Mathematically detects liquidity walls using Z-Score outlier detection.
39
- Returns the top significant walls.
40
- """
41
  if len(orders) < 10:
42
  return []
43
 
44
- # Get volumes for the scan depth
45
  relevant_orders = orders[:scan_depth]
46
  volumes = [q for p, q in relevant_orders]
47
 
48
  if not volumes:
49
  return []
50
 
51
- # Calculate Distribution Statistics
52
  try:
53
  avg_vol = statistics.mean(volumes)
54
  stdev_vol = statistics.stdev(volumes)
@@ -59,12 +51,9 @@ def detect_anomalies(orders, scan_depth):
59
  return []
60
 
61
  walls = []
62
-
63
  for price, qty in relevant_orders:
64
- # Calculate Z-Score
65
  z_score = (qty - avg_vol) / stdev_vol
66
-
67
- # If Z-Score > Threshold, it is a mathematically significant wall
68
  if z_score > Z_SCORE_THRESHOLD:
69
  walls.append({
70
  "price": price,
@@ -72,7 +61,6 @@ def detect_anomalies(orders, scan_depth):
72
  "z_score": z_score
73
  })
74
 
75
- # Sort by Z-Score descending (strongest walls first) and take top 3
76
  walls.sort(key=lambda x: x['z_score'], reverse=True)
77
  return walls[:3]
78
 
@@ -82,14 +70,12 @@ def analyze_structure(diff_x, diff_y, current_mid):
82
 
83
  weighted_imbalance = 0.0
84
  prev_vol = 0.0
85
-
86
  for i in range(len(diff_x)):
87
  dist = diff_x[i]
88
  cum_vol = diff_y[i]
89
-
90
  marginal_vol = cum_vol - prev_vol
91
  prev_vol = cum_vol
92
-
93
  weight = math.exp(-dist / DECAY_LAMBDA)
94
  weighted_imbalance += marginal_vol * weight
95
 
@@ -112,57 +98,54 @@ def process_market_data():
112
  return {"error": "Initializing..."}
113
 
114
  mid = market_state['current_mid']
115
-
116
- # Sort orders for processing
117
- # Bids: High to Low
118
  sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
119
- # Asks: Low to High
120
  sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
121
 
122
  raw_bids = sorted_bids[:300]
123
  raw_asks = sorted_asks[:300]
124
 
125
- # --- WALL DETECTION LOGIC ---
126
  bid_walls = detect_anomalies(sorted_bids, WALL_LOOKBACK)
127
  ask_walls = detect_anomalies(sorted_asks, WALL_LOOKBACK)
128
- # ----------------------------
129
 
130
  d_b_x, d_b_y, cum = [], [], 0
131
  for p, q in raw_bids:
132
  d = mid - p
133
  if d >= 0:
134
  cum += q
135
- d_b_x.append(d); d_b_y.append(cum)
 
136
 
137
  d_a_x, d_a_y, cum = [], [], 0
138
  for p, q in raw_asks:
139
  d = p - mid
140
  if d >= 0:
141
  cum += q
142
- d_a_x.append(d); d_a_y.append(cum)
 
143
 
144
  diff_x, diff_y = [], []
145
  chart_bids, chart_asks = [], []
146
-
147
  if d_b_x and d_a_x:
148
  max_dist = min(d_b_x[-1], d_a_x[-1])
149
  step_size = max_dist / 100
150
  steps = [i * step_size for i in range(1, 101)]
151
-
152
  for s in steps:
153
  idx_b = bisect.bisect_right(d_b_x, s)
154
- vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
155
-
156
  idx_a = bisect.bisect_right(d_a_x, s)
157
- vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
158
-
159
  diff_x.append(s)
160
  diff_y.append(vol_b - vol_a)
161
  chart_bids.append(vol_b)
162
  chart_asks.append(vol_a)
163
 
164
  analysis = analyze_structure(diff_x, diff_y, mid)
165
-
166
  now = time.time()
167
  if analysis:
168
  if not market_state['pred_history'] or (now - market_state['pred_history'][-1]['t'] > 0.5):
@@ -185,286 +168,773 @@ def process_market_data():
185
  }
186
  }
187
 
188
- HTML_PAGE = f"""
189
  <!DOCTYPE html>
190
  <html lang="en">
191
  <head>
192
  <meta charset="UTF-8">
193
- <title>HFT Liquidity Dashboard | {SYMBOL_KRAKEN}</title>
 
194
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
 
195
  <style>
196
- :root {{
197
- --bg-color: #0b0c10;
198
- --panel-bg: #1f2833;
199
- --text-main: #c5c6c7;
200
- --accent-green: #66fcf1;
201
- --accent-red: #ff3b3b;
202
- --border: #2d3842;
203
- }}
204
- body {{ margin: 0; padding: 0; background-color: var(--bg-color); color: var(--text-main); font-family: monospace; overflow: hidden; height: 100vh; width: 100vw; }}
205
-
206
- .grid-container {{
207
- display: grid;
208
- grid-template-columns: 3fr 1fr;
209
- grid-template-rows: 55% 45%;
210
- gap: 4px;
211
- height: 100vh;
212
- padding: 4px;
213
- box-sizing: border-box;
214
- }}
215
-
216
- .panel {{ background: #12141a; border: 1px solid var(--border); border-radius: 4px; position: relative; display: flex; flex-direction: column; overflow: hidden; }}
217
-
218
- #p-price {{ grid-column: 1 / 2; grid-row: 1 / 2; }}
219
- #p-stats {{ grid-column: 2 / 3; grid-row: 1 / 3; border-left: 2px solid #45a29e; }}
220
- #p-bottom {{ grid-column: 1 / 2; grid-row: 2 / 3; display: flex; gap: 4px; background: transparent; border: none; }}
221
-
222
- .sub-panel {{ flex: 1; background: #12141a; border: 1px solid var(--border); border-radius: 4px; display: flex; flex-direction: column; overflow: hidden; }}
223
-
224
- .panel-header {{ padding: 6px 10px; background: #0f1116; border-bottom: 1px solid var(--border); font-size: 11px; font-weight: bold; display: flex; justify-content: space-between; color: var(--accent-green); text-transform: uppercase; }}
225
-
226
- #tv-price, #tv-raw, #tv-net {{ flex: 1; width: 100%; position: relative; }}
227
-
228
- .stats-content {{ padding: 15px; overflow-y: auto; flex: 1; }}
229
- .stat-box {{ margin-bottom: 20px; padding: 10px; background: rgba(255,255,255,0.02); border-radius: 4px; }}
230
- .stat-label {{ font-size: 11px; color: #666; display: block; margin-bottom: 4px; }}
231
- .stat-value {{ font-size: 24px; font-weight: bold; }}
232
- .stat-sub {{ font-size: 10px; color: #888; margin-top: 5px; }}
233
- .green {{ color: var(--accent-green); }}
234
- .red {{ color: var(--accent-red); }}
235
-
236
- #loader {{ position: absolute; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.95); z-index: 999; display: flex; flex-direction: column; justify-content: center; align-items: center; color: var(--accent-green); }}
237
-
238
- .wall-item {{ display: flex; justify-content: space-between; font-size: 11px; margin-bottom: 4px; padding: 4px; background: rgba(0,0,0,0.2); }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  </style>
240
  </head>
241
  <body>
242
-
243
  <div id="loader">
244
- <div style="font-size: 24px;">ESTABLISHING UPLINK...</div>
245
- <div id="loading-status">Connecting to WebSocket Stream...</div>
 
246
  </div>
247
 
248
- <div class="grid-container">
249
- <div id="p-price" class="panel">
250
- <div class="panel-header"><span>BTC/USD Price + Liquidity Walls</span><span id="live-price">---</span></div>
251
- <div id="tv-price"></div>
252
- </div>
 
 
 
 
253
 
254
- <div id="p-bottom">
255
- <div class="sub-panel">
256
- <div class="panel-header"><span>Market Depth (Bids vs Asks)</span></div>
257
- <div id="tv-raw"></div>
 
 
 
 
 
258
  </div>
259
- <div class="sub-panel">
260
- <div class="panel-header"><span>Net Delta (Bids - Asks)</span></div>
261
- <div id="tv-net"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  </div>
263
- </div>
264
 
265
- <div id="p-stats" class="panel">
266
- <div class="panel-header">HFT ANALYTICS ENGINE</div>
267
- <div class="stats-content">
268
- <div class="stat-box">
269
- <span class="stat-label">WEIGHTED IMBALANCE SCORE</span>
270
- <span id="score-val" class="stat-value">0</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  </div>
272
- <div class="stat-box" style="border: 1px solid #444;">
273
- <span class="stat-label" style="color:var(--accent-green);">IMPACT PROJECTION</span>
274
- <span id="proj-val" class="stat-value">---</span>
 
 
 
 
 
 
275
  </div>
276
-
277
- <div class="stat-box">
278
- <span class="stat-label">DETECTED WALLS (Z-SCORE > 3)</span>
279
- <div id="wall-list">Waiting for data...</div>
 
 
 
 
 
280
  </div>
281
  </div>
282
  </div>
283
  </div>
284
 
285
  <script>
286
- document.addEventListener('DOMContentLoaded', () => {{
287
- const dom = {{
288
  loader: document.getElementById('loader'),
289
  status: document.getElementById('loading-status'),
 
 
290
  price: document.getElementById('live-price'),
291
  scoreVal: document.getElementById('score-val'),
292
  projVal: document.getElementById('proj-val'),
 
 
293
  wallList: document.getElementById('wall-list')
294
- }};
295
 
296
- const chartCommon = {{
297
- layout: {{ background: {{ type: 'solid', color: '#12141a' }}, textColor: '#888' }},
298
- grid: {{ vertLines: {{ color: '#1f2833' }}, horzLines: {{ color: '#1f2833' }} }},
299
- rightPriceScale: {{ borderColor: '#2d3842' }},
300
- timeScale: {{ borderColor: '#2d3842', timeVisible: true, secondsVisible: true }},
301
- crosshair: {{ mode: 0 }}
302
- }};
303
 
304
  const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartCommon);
305
- const priceSeries = priceChart.addLineSeries({{ color: '#2962FF', lineWidth: 2, title: 'Price' }});
306
-
307
- const pastPredSeries = priceChart.addLineSeries({{
308
- color: '#555555',
309
- lineWidth: 1,
310
- title: 'Past Prediction'
311
- }});
312
-
313
- const futurePredSeries = priceChart.addLineSeries({{
314
- color: '#ff9800',
315
- lineWidth: 2,
316
- lineStyle: 2,
317
- title: 'Projection'
318
- }});
319
-
320
- const rawChart = LightweightCharts.createChart(document.getElementById('tv-raw'), {{
321
  ...chartCommon,
322
- timeScale: {{ tickMarkFormatter: (time) => parseFloat(time).toFixed(0) }},
323
- localization: {{ timeFormatter: (time) => 'Dist: $' + parseFloat(time).toFixed(2) }}
324
- }});
325
- const rawBidSeries = rawChart.addAreaSeries({{ lineColor: '#00e676', topColor: 'rgba(0, 230, 118, 0.2)', bottomColor: 'rgba(0, 230, 118, 0.0)', lineWidth: 2, title: "Bids" }});
326
- const rawAskSeries = rawChart.addAreaSeries({{ lineColor: '#ff1744', topColor: 'rgba(255, 23, 68, 0.2)', bottomColor: 'rgba(255, 23, 68, 0.0)', lineWidth: 2, title: "Asks" }});
327
 
328
- const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{
329
  ...chartCommon,
330
- timeScale: {{ tickMarkFormatter: (time) => parseFloat(time).toFixed(0) }},
331
- localization: {{ timeFormatter: (time) => 'Dist: $' + parseFloat(time).toFixed(2) }}
332
- }});
333
- const netSeries = netChart.addAreaSeries({{
334
- topColor: 'rgba(33, 150, 243, 0.56)',
335
- bottomColor: 'rgba(33, 150, 243, 0.0)',
336
- lineColor: '#2196f3',
337
- lineWidth: 2
338
- }});
339
-
340
- // Refs for Price Lines (Walls)
341
  let activePriceLines = [];
342
 
343
- const resizeObserver = new ResizeObserver(entries => {{
344
- for(let entry of entries) {{
345
- const {{width, height}} = entry.contentRect;
346
- if(entry.target.id === 'tv-price') priceChart.applyOptions({{width, height}});
347
- if(entry.target.id === 'tv-raw') rawChart.applyOptions({{width, height}});
348
- if(entry.target.id === 'tv-net') netChart.applyOptions({{width, height}});
349
- }}
350
- }});
351
  ['tv-price', 'tv-raw', 'tv-net'].forEach(id => resizeObserver.observe(document.getElementById(id)));
352
 
353
- function connect() {{
354
  const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
355
- const url = `${{proto}}://${{window.location.host}}/ws`;
356
  const ws = new WebSocket(url);
357
 
358
- ws.onopen = () => {{ dom.status.innerText = "Receiving Data Stream..."; }};
359
- ws.onclose = () => {{ dom.loader.style.display = 'flex'; dom.status.innerText = "Reconnecting..."; setTimeout(connect, 3000); }};
360
-
361
- ws.onmessage = (event) => {{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  const data = JSON.parse(event.data);
363
  if (data.error) return;
364
  dom.loader.style.display = 'none';
365
 
366
- // --- History & Prediction Plotting ---
367
  const cleanHistory = [];
368
  const seen = new Set();
369
- data.history.forEach(d => {{
370
- const t = Math.floor(d.t);
371
- if (!seen.has(t)) {{ seen.add(t); cleanHistory.push({{ time: t, value: d.p }}); }}
372
- }});
373
 
374
  const predHistory = [];
375
  const seenP = new Set();
376
- if(data.pred_history) {{
377
- data.pred_history.forEach(d => {{
378
  const t = Math.floor(d.t);
379
- if(!seenP.has(t)) {{ seenP.add(t); predHistory.push({{ time: t, value: d.p }}); }}
380
- }});
381
- }}
382
 
383
- if (cleanHistory.length) {{
384
  priceSeries.setData(cleanHistory);
385
  pastPredSeries.setData(predHistory);
386
 
387
- const last = cleanHistory[cleanHistory.length-1];
388
- dom.price.innerText = last.value.toLocaleString(undefined, {{minimumFractionDigits: 2}});
389
-
390
- if (data.analysis) {{
391
- const {{ projected, net_score }} = data.analysis;
392
-
393
  futurePredSeries.setData([
394
  last,
395
- {{ time: last.time + 60, value: projected }}
396
  ]);
397
-
398
- dom.projVal.innerText = projected.toLocaleString(undefined, {{minimumFractionDigits: 0, maximumFractionDigits: 0}});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
 
400
  dom.scoreVal.innerText = net_score.toFixed(2);
401
- dom.scoreVal.className = net_score > 0 ? "stat-value green" : "stat-value red";
402
- }}
403
- }}
404
 
405
- // --- Wall Visualization ---
406
- if (data.walls) {{
407
- // Clear old lines
408
  activePriceLines.forEach(line => priceSeries.removePriceLine(line));
409
  activePriceLines = [];
410
-
411
  let wallHtml = "";
412
 
413
- // Draw Bid Walls
414
- data.walls.bids.forEach(w => {{
415
- const line = priceSeries.createPriceLine({{
416
  price: w.price,
417
- color: 'rgba(0, 230, 118, 0.8)',
418
  lineWidth: 1,
419
  lineStyle: 2,
420
  axisLabelVisible: true,
421
- title: `BUY WALL (Z: ${{w.z_score.toFixed(1)}})`,
422
- }});
423
  activePriceLines.push(line);
424
- wallHtml += `<div class="wall-item"><span class="green">BUY ${{w.price}}</span><span>Z: ${{w.z_score.toFixed(1)}}</span></div>`;
425
- }});
426
 
427
- // Draw Ask Walls
428
- data.walls.asks.forEach(w => {{
429
- const line = priceSeries.createPriceLine({{
430
  price: w.price,
431
- color: 'rgba(255, 23, 68, 0.8)',
432
  lineWidth: 1,
433
  lineStyle: 2,
434
  axisLabelVisible: true,
435
- title: `SELL WALL (Z: ${{w.z_score.toFixed(1)}})`,
436
- }});
437
  activePriceLines.push(line);
438
- wallHtml += `<div class="wall-item"><span class="red">SELL ${{w.price}}</span><span>Z: ${{w.z_score.toFixed(1)}}</span></div>`;
439
- }});
440
-
441
- dom.wallList.innerHTML = wallHtml || "No significant walls.";
442
- }}
443
-
444
- // --- Depth Charts ---
445
- if (data.depth_x && data.depth_x.length) {{
446
  const netData = [];
447
  const rawBids = [], rawAsks = [];
448
-
449
- for (let i = 0; i < data.depth_x.length; i++) {{
450
  const x = data.depth_x[i];
451
- const netY = data.depth_net[i];
452
- const bidY = data.depth_bids[i];
453
- const askY = data.depth_asks[i];
454
-
455
- netData.push({{ time: x, value: netY }});
456
- rawBids.push({{ time: x, value: bidY }});
457
- rawAsks.push({{ time: x, value: askY }});
458
- }}
459
-
460
  netSeries.setData(netData);
461
  rawBidSeries.setData(rawBids);
462
  rawAskSeries.setData(rawAsks);
463
- }}
464
- }};
465
- }}
466
  connect();
467
- }});
468
  </script>
469
  </body>
470
  </html>
@@ -475,7 +945,7 @@ async def kraken_worker():
475
  while True:
476
  try:
477
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
478
- logging.info(f"🔌 Connected to Kraken ({SYMBOL_KRAKEN})")
479
  await ws.send(json.dumps({
480
  "method": "subscribe",
481
  "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}
@@ -490,13 +960,17 @@ async def kraken_worker():
490
  for item in data:
491
  for bid in item.get('bids', []):
492
  q, p = float(bid['qty']), float(bid['price'])
493
- if q == 0: market_state['bids'].pop(p, None)
494
- else: market_state['bids'][p] = q
 
 
495
  for ask in item.get('asks', []):
496
  q, p = float(ask['qty']), float(ask['price'])
497
- if q == 0: market_state['asks'].pop(p, None)
498
- else: market_state['asks'][p] = q
499
-
 
 
500
  if market_state['bids'] and market_state['asks']:
501
  best_bid = max(market_state['bids'].keys())
502
  best_ask = min(market_state['asks'].keys())
@@ -504,7 +978,7 @@ async def kraken_worker():
504
  market_state['prev_mid'] = market_state['current_mid']
505
  market_state['current_mid'] = mid
506
  market_state['ready'] = True
507
-
508
  now = time.time()
509
  if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
510
  market_state['history'].append({'t': now, 'p': mid})
@@ -512,7 +986,7 @@ async def kraken_worker():
512
  market_state['history'].pop(0)
513
 
514
  except Exception as e:
515
- logging.warning(f"⚠️ Reconnecting: {e}")
516
  await asyncio.sleep(3)
517
 
518
  async def broadcast_worker():
@@ -521,8 +995,10 @@ async def broadcast_worker():
521
  payload = process_market_data()
522
  msg = json.dumps(payload)
523
  for ws in list(connected_clients):
524
- try: await ws.send_str(msg)
525
- except: pass
 
 
526
  await asyncio.sleep(BROADCAST_RATE)
527
 
528
  async def websocket_handler(request):
@@ -546,8 +1022,11 @@ async def start_background(app):
546
  async def cleanup_background(app):
547
  app['kraken_task'].cancel()
548
  app['broadcast_task'].cancel()
549
- try: await app['kraken_task']; await app['broadcast_task']
550
- except: pass
 
 
 
551
 
552
  async def main():
553
  app = web.Application()
@@ -559,9 +1038,11 @@ async def main():
559
  await runner.setup()
560
  site = web.TCPSite(runner, '0.0.0.0', PORT)
561
  await site.start()
562
- print(f"🚀 AI Dashboard: http://localhost:{PORT}")
563
  await asyncio.Event().wait()
564
 
565
  if __name__ == "__main__":
566
- try: asyncio.run(main())
567
- except KeyboardInterrupt: pass
 
 
 
10
 
11
  SYMBOL_KRAKEN = "BTC/USD"
12
  PORT = 7860
13
+ HISTORY_LENGTH = 300
14
  BROADCAST_RATE = 0.1
15
+ DECAY_LAMBDA = 100.0
16
+ IMPACT_SENSITIVITY = 0.5
17
+ Z_SCORE_THRESHOLD = 3.0
18
+ WALL_LOOKBACK = 200
 
 
19
 
20
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
21
 
22
  market_state = {
23
  "bids": {},
24
  "asks": {},
25
+ "history": [],
26
+ "pred_history": [],
27
  "current_mid": 0.0,
28
  "prev_mid": 0.0,
29
  "ready": False
 
32
  connected_clients = set()
33
 
34
  def detect_anomalies(orders, scan_depth):
 
 
 
 
35
  if len(orders) < 10:
36
  return []
37
 
 
38
  relevant_orders = orders[:scan_depth]
39
  volumes = [q for p, q in relevant_orders]
40
 
41
  if not volumes:
42
  return []
43
 
 
44
  try:
45
  avg_vol = statistics.mean(volumes)
46
  stdev_vol = statistics.stdev(volumes)
 
51
  return []
52
 
53
  walls = []
54
+
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({
59
  "price": price,
 
61
  "z_score": z_score
62
  })
63
 
 
64
  walls.sort(key=lambda x: x['z_score'], reverse=True)
65
  return walls[:3]
66
 
 
70
 
71
  weighted_imbalance = 0.0
72
  prev_vol = 0.0
73
+
74
  for i in range(len(diff_x)):
75
  dist = diff_x[i]
76
  cum_vol = diff_y[i]
 
77
  marginal_vol = cum_vol - prev_vol
78
  prev_vol = cum_vol
 
79
  weight = math.exp(-dist / DECAY_LAMBDA)
80
  weighted_imbalance += marginal_vol * weight
81
 
 
98
  return {"error": "Initializing..."}
99
 
100
  mid = market_state['current_mid']
101
+
 
 
102
  sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
 
103
  sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
104
 
105
  raw_bids = sorted_bids[:300]
106
  raw_asks = sorted_asks[:300]
107
 
 
108
  bid_walls = detect_anomalies(sorted_bids, WALL_LOOKBACK)
109
  ask_walls = detect_anomalies(sorted_asks, WALL_LOOKBACK)
 
110
 
111
  d_b_x, d_b_y, cum = [], [], 0
112
  for p, q in raw_bids:
113
  d = mid - p
114
  if d >= 0:
115
  cum += q
116
+ d_b_x.append(d)
117
+ d_b_y.append(cum)
118
 
119
  d_a_x, d_a_y, cum = [], [], 0
120
  for p, q in raw_asks:
121
  d = p - mid
122
  if d >= 0:
123
  cum += q
124
+ d_a_x.append(d)
125
+ d_a_y.append(cum)
126
 
127
  diff_x, diff_y = [], []
128
  chart_bids, chart_asks = [], []
129
+
130
  if d_b_x and d_a_x:
131
  max_dist = min(d_b_x[-1], d_a_x[-1])
132
  step_size = max_dist / 100
133
  steps = [i * step_size for i in range(1, 101)]
134
+
135
  for s in steps:
136
  idx_b = bisect.bisect_right(d_b_x, s)
137
+ vol_b = d_b_y[idx_b - 1] if idx_b > 0 else 0
138
+
139
  idx_a = bisect.bisect_right(d_a_x, s)
140
+ vol_a = d_a_y[idx_a - 1] if idx_a > 0 else 0
141
+
142
  diff_x.append(s)
143
  diff_y.append(vol_b - vol_a)
144
  chart_bids.append(vol_b)
145
  chart_asks.append(vol_a)
146
 
147
  analysis = analyze_structure(diff_x, diff_y, mid)
148
+
149
  now = time.time()
150
  if analysis:
151
  if not market_state['pred_history'] or (now - market_state['pred_history'][-1]['t'] > 0.5):
 
168
  }
169
  }
170
 
171
+ HTML_PAGE = """
172
  <!DOCTYPE html>
173
  <html lang="en">
174
  <head>
175
  <meta charset="UTF-8">
176
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
177
+ <title>Quantum Flow | BTC/USD</title>
178
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
179
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
180
  <style>
181
+ * { margin: 0; padding: 0; box-sizing: border-box; }
182
+
183
+ :root {
184
+ --bg-primary: #06080d;
185
+ --bg-secondary: #0c1018;
186
+ --bg-card: #111620;
187
+ --bg-elevated: #161d2a;
188
+ --border-primary: #1e2738;
189
+ --border-glow: #2a3f5f;
190
+ --text-primary: #e8eaed;
191
+ --text-secondary: #8b95a5;
192
+ --text-muted: #4a5568;
193
+ --accent-cyan: #00d4ff;
194
+ --accent-cyan-dim: rgba(0, 212, 255, 0.15);
195
+ --accent-green: #00ff88;
196
+ --accent-green-dim: rgba(0, 255, 136, 0.12);
197
+ --accent-red: #ff3366;
198
+ --accent-red-dim: rgba(255, 51, 102, 0.12);
199
+ --accent-orange: #ff9500;
200
+ --accent-purple: #a855f7;
201
+ --gradient-cyan: linear-gradient(135deg, #00d4ff 0%, #0099cc 100%);
202
+ --gradient-green: linear-gradient(135deg, #00ff88 0%, #00cc6a 100%);
203
+ --gradient-red: linear-gradient(135deg, #ff3366 0%, #cc2952 100%);
204
+ }
205
+
206
+ body {
207
+ background: var(--bg-primary);
208
+ color: var(--text-primary);
209
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
210
+ overflow: hidden;
211
+ height: 100vh;
212
+ width: 100vw;
213
+ }
214
+
215
+ .app-container {
216
+ display: flex;
217
+ flex-direction: column;
218
+ height: 100vh;
219
+ background: radial-gradient(ellipse at top, #0d1520 0%, var(--bg-primary) 70%);
220
+ }
221
+
222
+ .header {
223
+ display: flex;
224
+ align-items: center;
225
+ justify-content: space-between;
226
+ padding: 12px 20px;
227
+ background: linear-gradient(180deg, rgba(17, 22, 32, 0.95) 0%, rgba(17, 22, 32, 0.8) 100%);
228
+ border-bottom: 1px solid var(--border-primary);
229
+ backdrop-filter: blur(20px);
230
+ z-index: 100;
231
+ }
232
+
233
+ .logo-section {
234
+ display: flex;
235
+ align-items: center;
236
+ gap: 14px;
237
+ }
238
+
239
+ .logo-icon {
240
+ width: 36px;
241
+ height: 36px;
242
+ background: var(--gradient-cyan);
243
+ border-radius: 10px;
244
+ display: flex;
245
+ align-items: center;
246
+ justify-content: center;
247
+ font-size: 18px;
248
+ box-shadow: 0 4px 20px rgba(0, 212, 255, 0.3);
249
+ }
250
+
251
+ .logo-text {
252
+ font-family: 'JetBrains Mono', monospace;
253
+ font-size: 18px;
254
+ font-weight: 700;
255
+ background: var(--gradient-cyan);
256
+ -webkit-background-clip: text;
257
+ -webkit-text-fill-color: transparent;
258
+ letter-spacing: -0.5px;
259
+ }
260
+
261
+ .logo-sub {
262
+ font-size: 10px;
263
+ color: var(--text-muted);
264
+ text-transform: uppercase;
265
+ letter-spacing: 2px;
266
+ margin-top: 2px;
267
+ }
268
+
269
+ .header-center {
270
+ display: flex;
271
+ align-items: center;
272
+ gap: 24px;
273
+ }
274
+
275
+ .pair-display {
276
+ display: flex;
277
+ align-items: center;
278
+ gap: 12px;
279
+ padding: 8px 16px;
280
+ background: var(--bg-elevated);
281
+ border: 1px solid var(--border-primary);
282
+ border-radius: 12px;
283
+ }
284
+
285
+ .pair-icon {
286
+ width: 28px;
287
+ height: 28px;
288
+ background: linear-gradient(135deg, #f7931a 0%, #ffab40 100%);
289
+ border-radius: 50%;
290
+ display: flex;
291
+ align-items: center;
292
+ justify-content: center;
293
+ font-weight: bold;
294
+ font-size: 14px;
295
+ color: #fff;
296
+ }
297
+
298
+ .pair-name {
299
+ font-family: 'JetBrains Mono', monospace;
300
+ font-size: 15px;
301
+ font-weight: 600;
302
+ color: var(--text-primary);
303
+ }
304
+
305
+ .live-price-container {
306
+ display: flex;
307
+ align-items: center;
308
+ gap: 8px;
309
+ }
310
+
311
+ .live-indicator {
312
+ width: 8px;
313
+ height: 8px;
314
+ background: var(--accent-green);
315
+ border-radius: 50%;
316
+ animation: pulse-glow 2s ease-in-out infinite;
317
+ box-shadow: 0 0 10px var(--accent-green);
318
+ }
319
+
320
+ @keyframes pulse-glow {
321
+ 0%, 100% { opacity: 1; transform: scale(1); }
322
+ 50% { opacity: 0.6; transform: scale(0.9); }
323
+ }
324
+
325
+ .live-price {
326
+ font-family: 'JetBrains Mono', monospace;
327
+ font-size: 22px;
328
+ font-weight: 700;
329
+ color: var(--text-primary);
330
+ }
331
+
332
+ .header-right {
333
+ display: flex;
334
+ align-items: center;
335
+ gap: 12px;
336
+ }
337
+
338
+ .status-badge {
339
+ display: flex;
340
+ align-items: center;
341
+ gap: 8px;
342
+ padding: 6px 14px;
343
+ background: var(--accent-green-dim);
344
+ border: 1px solid rgba(0, 255, 136, 0.3);
345
+ border-radius: 20px;
346
+ font-size: 11px;
347
+ font-weight: 600;
348
+ color: var(--accent-green);
349
+ text-transform: uppercase;
350
+ letter-spacing: 0.5px;
351
+ }
352
+
353
+ .main-grid {
354
+ flex: 1;
355
+ display: grid;
356
+ grid-template-columns: 1fr 320px;
357
+ grid-template-rows: 1fr 1fr;
358
+ gap: 4px;
359
+ padding: 4px;
360
+ min-height: 0;
361
+ }
362
+
363
+ .chart-panel {
364
+ grid-row: 1 / 3;
365
+ display: flex;
366
+ flex-direction: column;
367
+ background: var(--bg-card);
368
+ border: 1px solid var(--border-primary);
369
+ border-radius: 12px;
370
+ overflow: hidden;
371
+ }
372
+
373
+ .panel-header {
374
+ display: flex;
375
+ align-items: center;
376
+ justify-content: space-between;
377
+ padding: 12px 16px;
378
+ background: linear-gradient(180deg, var(--bg-elevated) 0%, var(--bg-card) 100%);
379
+ border-bottom: 1px solid var(--border-primary);
380
+ }
381
+
382
+ .panel-title {
383
+ display: flex;
384
+ align-items: center;
385
+ gap: 10px;
386
+ font-size: 12px;
387
+ font-weight: 600;
388
+ color: var(--text-secondary);
389
+ text-transform: uppercase;
390
+ letter-spacing: 1px;
391
+ }
392
+
393
+ .panel-title-icon {
394
+ width: 20px;
395
+ height: 20px;
396
+ background: var(--accent-cyan-dim);
397
+ border-radius: 6px;
398
+ display: flex;
399
+ align-items: center;
400
+ justify-content: center;
401
+ font-size: 10px;
402
+ }
403
+
404
+ .chart-container {
405
+ flex: 1;
406
+ position: relative;
407
+ min-height: 0;
408
+ }
409
+
410
+ .depth-row {
411
+ display: flex;
412
+ gap: 4px;
413
+ }
414
+
415
+ .depth-panel {
416
+ flex: 1;
417
+ display: flex;
418
+ flex-direction: column;
419
+ background: var(--bg-card);
420
+ border: 1px solid var(--border-primary);
421
+ border-radius: 12px;
422
+ overflow: hidden;
423
+ }
424
+
425
+ .stats-column {
426
+ display: flex;
427
+ flex-direction: column;
428
+ gap: 4px;
429
+ }
430
+
431
+ .stats-panel {
432
+ flex: 1;
433
+ display: flex;
434
+ flex-direction: column;
435
+ background: var(--bg-card);
436
+ border: 1px solid var(--border-primary);
437
+ border-radius: 12px;
438
+ overflow: hidden;
439
+ }
440
+
441
+ .stats-content {
442
+ flex: 1;
443
+ padding: 16px;
444
+ overflow-y: auto;
445
+ }
446
+
447
+ .stat-card {
448
+ background: var(--bg-elevated);
449
+ border: 1px solid var(--border-primary);
450
+ border-radius: 10px;
451
+ padding: 14px;
452
+ margin-bottom: 12px;
453
+ transition: all 0.3s ease;
454
+ }
455
+
456
+ .stat-card:hover {
457
+ border-color: var(--border-glow);
458
+ transform: translateY(-1px);
459
+ }
460
+
461
+ .stat-card.highlight {
462
+ border-color: var(--accent-cyan);
463
+ background: linear-gradient(135deg, var(--bg-elevated) 0%, rgba(0, 212, 255, 0.05) 100%);
464
+ box-shadow: 0 4px 20px rgba(0, 212, 255, 0.1);
465
+ }
466
+
467
+ .stat-label {
468
+ font-size: 10px;
469
+ font-weight: 600;
470
+ color: var(--text-muted);
471
+ text-transform: uppercase;
472
+ letter-spacing: 1px;
473
+ margin-bottom: 8px;
474
+ display: flex;
475
+ align-items: center;
476
+ gap: 6px;
477
+ }
478
+
479
+ .stat-value {
480
+ font-family: 'JetBrains Mono', monospace;
481
+ font-size: 26px;
482
+ font-weight: 700;
483
+ line-height: 1.2;
484
+ }
485
+
486
+ .stat-value.green { color: var(--accent-green); }
487
+ .stat-value.red { color: var(--accent-red); }
488
+ .stat-value.cyan { color: var(--accent-cyan); }
489
+
490
+ .walls-section {
491
+ margin-top: 8px;
492
+ }
493
+
494
+ .walls-header {
495
+ display: flex;
496
+ align-items: center;
497
+ justify-content: space-between;
498
+ margin-bottom: 10px;
499
+ }
500
+
501
+ .walls-title {
502
+ font-size: 11px;
503
+ font-weight: 600;
504
+ color: var(--text-secondary);
505
+ text-transform: uppercase;
506
+ letter-spacing: 0.5px;
507
+ }
508
+
509
+ .walls-badge {
510
+ font-size: 9px;
511
+ padding: 3px 8px;
512
+ background: var(--accent-purple);
513
+ border-radius: 10px;
514
+ color: #fff;
515
+ font-weight: 600;
516
+ }
517
+
518
+ .wall-item {
519
+ display: flex;
520
+ align-items: center;
521
+ justify-content: space-between;
522
+ padding: 10px 12px;
523
+ background: var(--bg-secondary);
524
+ border: 1px solid var(--border-primary);
525
+ border-radius: 8px;
526
+ margin-bottom: 8px;
527
+ transition: all 0.2s ease;
528
+ }
529
+
530
+ .wall-item:hover {
531
+ background: var(--bg-elevated);
532
+ }
533
+
534
+ .wall-item.bid {
535
+ border-left: 3px solid var(--accent-green);
536
+ }
537
+
538
+ .wall-item.ask {
539
+ border-left: 3px solid var(--accent-red);
540
+ }
541
+
542
+ .wall-type {
543
+ font-size: 9px;
544
+ font-weight: 700;
545
+ text-transform: uppercase;
546
+ letter-spacing: 0.5px;
547
+ padding: 3px 8px;
548
+ border-radius: 4px;
549
+ }
550
+
551
+ .wall-type.bid {
552
+ background: var(--accent-green-dim);
553
+ color: var(--accent-green);
554
+ }
555
+
556
+ .wall-type.ask {
557
+ background: var(--accent-red-dim);
558
+ color: var(--accent-red);
559
+ }
560
+
561
+ .wall-price {
562
+ font-family: 'JetBrains Mono', monospace;
563
+ font-size: 13px;
564
+ font-weight: 600;
565
+ color: var(--text-primary);
566
+ }
567
+
568
+ .wall-zscore {
569
+ font-family: 'JetBrains Mono', monospace;
570
+ font-size: 11px;
571
+ color: var(--accent-orange);
572
+ font-weight: 600;
573
+ }
574
+
575
+ .empty-walls {
576
+ text-align: center;
577
+ padding: 20px;
578
+ color: var(--text-muted);
579
+ font-size: 12px;
580
+ }
581
+
582
+ #loader {
583
+ position: fixed;
584
+ top: 0;
585
+ left: 0;
586
+ width: 100%;
587
+ height: 100%;
588
+ background: var(--bg-primary);
589
+ z-index: 9999;
590
+ display: flex;
591
+ flex-direction: column;
592
+ justify-content: center;
593
+ align-items: center;
594
+ gap: 24px;
595
+ }
596
+
597
+ .loader-spinner {
598
+ width: 60px;
599
+ height: 60px;
600
+ border: 3px solid var(--border-primary);
601
+ border-top-color: var(--accent-cyan);
602
+ border-radius: 50%;
603
+ animation: spin 1s linear infinite;
604
+ }
605
+
606
+ @keyframes spin {
607
+ to { transform: rotate(360deg); }
608
+ }
609
+
610
+ .loader-text {
611
+ font-family: 'JetBrains Mono', monospace;
612
+ font-size: 14px;
613
+ color: var(--text-secondary);
614
+ letter-spacing: 2px;
615
+ }
616
+
617
+ .loader-sub {
618
+ font-size: 11px;
619
+ color: var(--text-muted);
620
+ }
621
+
622
+ .projection-row {
623
+ display: flex;
624
+ align-items: center;
625
+ gap: 8px;
626
+ margin-top: 6px;
627
+ }
628
+
629
+ .projection-arrow {
630
+ font-size: 14px;
631
+ }
632
+
633
+ .projection-value {
634
+ font-family: 'JetBrains Mono', monospace;
635
+ font-size: 18px;
636
+ font-weight: 600;
637
+ }
638
  </style>
639
  </head>
640
  <body>
 
641
  <div id="loader">
642
+ <div class="loader-spinner"></div>
643
+ <div class="loader-text">INITIALIZING QUANTUM FLOW</div>
644
+ <div class="loader-sub" id="loading-status">Connecting to market data stream...</div>
645
  </div>
646
 
647
+ <div class="app-container">
648
+ <header class="header">
649
+ <div class="logo-section">
650
+ <div class="logo-icon"></div>
651
+ <div>
652
+ <div class="logo-text">QUANTUM FLOW</div>
653
+ <div class="logo-sub">HFT Analytics</div>
654
+ </div>
655
+ </div>
656
 
657
+ <div class="header-center">
658
+ <div class="pair-display">
659
+ <div class="pair-icon"></div>
660
+ <span class="pair-name">BTC/USD</span>
661
+ </div>
662
+ <div class="live-price-container">
663
+ <div class="live-indicator"></div>
664
+ <span class="live-price" id="live-price">---</span>
665
+ </div>
666
  </div>
667
+
668
+ <div class="header-right">
669
+ <div class="status-badge" id="status-badge">
670
+ <span>●</span>
671
+ <span id="status-text">LIVE</span>
672
+ </div>
673
+ </div>
674
+ </header>
675
+
676
+ <div class="main-grid">
677
+ <div class="chart-panel">
678
+ <div class="panel-header">
679
+ <div class="panel-title">
680
+ <div class="panel-title-icon">📈</div>
681
+ Price Action & Liquidity Walls
682
+ </div>
683
+ </div>
684
+ <div class="chart-container" id="tv-price"></div>
685
  </div>
 
686
 
687
+ <div class="stats-column">
688
+ <div class="stats-panel">
689
+ <div class="panel-header">
690
+ <div class="panel-title">
691
+ <div class="panel-title-icon"></div>
692
+ Analytics Engine
693
+ </div>
694
+ </div>
695
+ <div class="stats-content">
696
+ <div class="stat-card highlight">
697
+ <div class="stat-label">◎ Impact Projection</div>
698
+ <div class="stat-value cyan" id="proj-val">---</div>
699
+ <div class="projection-row">
700
+ <span class="projection-arrow" id="proj-arrow">→</span>
701
+ <span class="projection-value" id="proj-direction">Calculating...</span>
702
+ </div>
703
+ </div>
704
+
705
+ <div class="stat-card">
706
+ <div class="stat-label">◉ Weighted Imbalance</div>
707
+ <div class="stat-value" id="score-val">0.00</div>
708
+ </div>
709
+
710
+ <div class="walls-section">
711
+ <div class="walls-header">
712
+ <span class="walls-title">Detected Walls</span>
713
+ <span class="walls-badge">Z > 3.0</span>
714
+ </div>
715
+ <div id="wall-list">
716
+ <div class="empty-walls">Scanning for anomalies...</div>
717
+ </div>
718
+ </div>
719
+ </div>
720
  </div>
721
+
722
+ <div class="depth-panel">
723
+ <div class="panel-header">
724
+ <div class="panel-title">
725
+ <div class="panel-title-icon">📊</div>
726
+ Market Depth
727
+ </div>
728
+ </div>
729
+ <div class="chart-container" id="tv-raw"></div>
730
  </div>
731
+
732
+ <div class="depth-panel">
733
+ <div class="panel-header">
734
+ <div class="panel-title">
735
+ <div class="panel-title-icon">Δ</div>
736
+ Net Delta
737
+ </div>
738
+ </div>
739
+ <div class="chart-container" id="tv-net"></div>
740
  </div>
741
  </div>
742
  </div>
743
  </div>
744
 
745
  <script>
746
+ document.addEventListener('DOMContentLoaded', () => {
747
+ const dom = {
748
  loader: document.getElementById('loader'),
749
  status: document.getElementById('loading-status'),
750
+ statusBadge: document.getElementById('status-badge'),
751
+ statusText: document.getElementById('status-text'),
752
  price: document.getElementById('live-price'),
753
  scoreVal: document.getElementById('score-val'),
754
  projVal: document.getElementById('proj-val'),
755
+ projArrow: document.getElementById('proj-arrow'),
756
+ projDirection: document.getElementById('proj-direction'),
757
  wallList: document.getElementById('wall-list')
758
+ };
759
 
760
+ const chartCommon = {
761
+ layout: { background: { type: 'solid', color: '#111620' }, textColor: '#8b95a5', fontFamily: 'JetBrains Mono' },
762
+ grid: { vertLines: { color: '#1e2738' }, horzLines: { color: '#1e2738' } },
763
+ rightPriceScale: { borderColor: '#1e2738', scaleMargins: { top: 0.1, bottom: 0.1 } },
764
+ timeScale: { borderColor: '#1e2738', timeVisible: true, secondsVisible: true },
765
+ crosshair: { mode: 0, vertLine: { color: '#00d4ff', width: 1, style: 2 }, horzLine: { color: '#00d4ff', width: 1, style: 2 } }
766
+ };
767
 
768
  const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartCommon);
769
+ const priceSeries = priceChart.addLineSeries({ color: '#00d4ff', lineWidth: 2 });
770
+ const pastPredSeries = priceChart.addLineSeries({ color: '#4a5568', lineWidth: 1 });
771
+ const futurePredSeries = priceChart.addLineSeries({ color: '#ff9500', lineWidth: 2, lineStyle: 2 });
772
+
773
+ const rawChart = LightweightCharts.createChart(document.getElementById('tv-raw'), {
 
 
 
 
 
 
 
 
 
 
 
774
  ...chartCommon,
775
+ timeScale: { tickMarkFormatter: (time) => parseFloat(time).toFixed(0) },
776
+ localization: { timeFormatter: (time) => '$' + parseFloat(time).toFixed(2) }
777
+ });
778
+ const rawBidSeries = rawChart.addAreaSeries({ lineColor: '#00ff88', topColor: 'rgba(0, 255, 136, 0.15)', bottomColor: 'rgba(0, 255, 136, 0.0)', lineWidth: 2 });
779
+ const rawAskSeries = rawChart.addAreaSeries({ lineColor: '#ff3366', topColor: 'rgba(255, 51, 102, 0.15)', bottomColor: 'rgba(255, 51, 102, 0.0)', lineWidth: 2 });
780
 
781
+ const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {
782
  ...chartCommon,
783
+ timeScale: { tickMarkFormatter: (time) => parseFloat(time).toFixed(0) },
784
+ localization: { timeFormatter: (time) => '$' + parseFloat(time).toFixed(2) }
785
+ });
786
+ const netSeries = netChart.addAreaSeries({ topColor: 'rgba(0, 212, 255, 0.3)', bottomColor: 'rgba(0, 212, 255, 0.0)', lineColor: '#00d4ff', lineWidth: 2 });
787
+
 
 
 
 
 
 
788
  let activePriceLines = [];
789
 
790
+ const resizeObserver = new ResizeObserver(entries => {
791
+ for (let entry of entries) {
792
+ const { width, height } = entry.contentRect;
793
+ if (entry.target.id === 'tv-price') priceChart.applyOptions({ width, height });
794
+ if (entry.target.id === 'tv-raw') rawChart.applyOptions({ width, height });
795
+ if (entry.target.id === 'tv-net') netChart.applyOptions({ width, height });
796
+ }
797
+ });
798
  ['tv-price', 'tv-raw', 'tv-net'].forEach(id => resizeObserver.observe(document.getElementById(id)));
799
 
800
+ function connect() {
801
  const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
802
+ const url = `${proto}://${window.location.host}/ws`;
803
  const ws = new WebSocket(url);
804
 
805
+ ws.onopen = () => {
806
+ dom.status.innerText = "Receiving data stream...";
807
+ dom.statusText.innerText = "LIVE";
808
+ dom.statusBadge.style.background = "var(--accent-green-dim)";
809
+ dom.statusBadge.style.borderColor = "rgba(0, 255, 136, 0.3)";
810
+ dom.statusBadge.style.color = "var(--accent-green)";
811
+ };
812
+
813
+ ws.onclose = () => {
814
+ dom.loader.style.display = 'flex';
815
+ dom.status.innerText = "Connection lost. Reconnecting...";
816
+ dom.statusText.innerText = "OFFLINE";
817
+ dom.statusBadge.style.background = "var(--accent-red-dim)";
818
+ dom.statusBadge.style.borderColor = "rgba(255, 51, 102, 0.3)";
819
+ dom.statusBadge.style.color = "var(--accent-red)";
820
+ setTimeout(connect, 3000);
821
+ };
822
+
823
+ ws.onmessage = (event) => {
824
  const data = JSON.parse(event.data);
825
  if (data.error) return;
826
  dom.loader.style.display = 'none';
827
 
 
828
  const cleanHistory = [];
829
  const seen = new Set();
830
+ data.history.forEach(d => {
831
+ const t = Math.floor(d.t);
832
+ if (!seen.has(t)) { seen.add(t); cleanHistory.push({ time: t, value: d.p }); }
833
+ });
834
 
835
  const predHistory = [];
836
  const seenP = new Set();
837
+ if (data.pred_history) {
838
+ data.pred_history.forEach(d => {
839
  const t = Math.floor(d.t);
840
+ if (!seenP.has(t)) { seenP.add(t); predHistory.push({ time: t, value: d.p }); }
841
+ });
842
+ }
843
 
844
+ if (cleanHistory.length) {
845
  priceSeries.setData(cleanHistory);
846
  pastPredSeries.setData(predHistory);
847
 
848
+ const last = cleanHistory[cleanHistory.length - 1];
849
+ dom.price.innerText = '$' + last.value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
850
+
851
+ if (data.analysis) {
852
+ const { projected, net_score } = data.analysis;
853
+
854
  futurePredSeries.setData([
855
  last,
856
+ { time: last.time + 60, value: projected }
857
  ]);
858
+
859
+ dom.projVal.innerText = '$' + projected.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 });
860
+
861
+ const diff = projected - last.value;
862
+ if (diff > 0) {
863
+ dom.projArrow.innerText = '↑';
864
+ dom.projArrow.style.color = 'var(--accent-green)';
865
+ dom.projDirection.innerText = '+$' + Math.abs(diff).toFixed(2);
866
+ dom.projDirection.style.color = 'var(--accent-green)';
867
+ } else if (diff < 0) {
868
+ dom.projArrow.innerText = '↓';
869
+ dom.projArrow.style.color = 'var(--accent-red)';
870
+ dom.projDirection.innerText = '-$' + Math.abs(diff).toFixed(2);
871
+ dom.projDirection.style.color = 'var(--accent-red)';
872
+ } else {
873
+ dom.projArrow.innerText = '→';
874
+ dom.projArrow.style.color = 'var(--text-muted)';
875
+ dom.projDirection.innerText = 'Neutral';
876
+ dom.projDirection.style.color = 'var(--text-muted)';
877
+ }
878
 
879
  dom.scoreVal.innerText = net_score.toFixed(2);
880
+ dom.scoreVal.className = net_score > 0 ? "stat-value green" : (net_score < 0 ? "stat-value red" : "stat-value");
881
+ }
882
+ }
883
 
884
+ if (data.walls) {
 
 
885
  activePriceLines.forEach(line => priceSeries.removePriceLine(line));
886
  activePriceLines = [];
887
+
888
  let wallHtml = "";
889
 
890
+ data.walls.bids.forEach(w => {
891
+ const line = priceSeries.createPriceLine({
 
892
  price: w.price,
893
+ color: 'rgba(0, 255, 136, 0.7)',
894
  lineWidth: 1,
895
  lineStyle: 2,
896
  axisLabelVisible: true,
897
+ title: 'BUY'
898
+ });
899
  activePriceLines.push(line);
900
+ wallHtml += `<div class="wall-item bid"><span class="wall-type bid">BUY</span><span class="wall-price">$${w.price.toLocaleString()}</span><span class="wall-zscore">Z: ${w.z_score.toFixed(1)}</span></div>`;
901
+ });
902
 
903
+ data.walls.asks.forEach(w => {
904
+ const line = priceSeries.createPriceLine({
 
905
  price: w.price,
906
+ color: 'rgba(255, 51, 102, 0.7)',
907
  lineWidth: 1,
908
  lineStyle: 2,
909
  axisLabelVisible: true,
910
+ title: 'SELL'
911
+ });
912
  activePriceLines.push(line);
913
+ wallHtml += `<div class="wall-item ask"><span class="wall-type ask">SELL</span><span class="wall-price">$${w.price.toLocaleString()}</span><span class="wall-zscore">Z: ${w.z_score.toFixed(1)}</span></div>`;
914
+ });
915
+
916
+ dom.wallList.innerHTML = wallHtml || '<div class="empty-walls">No significant walls detected</div>';
917
+ }
918
+
919
+ if (data.depth_x && data.depth_x.length) {
 
920
  const netData = [];
921
  const rawBids = [], rawAsks = [];
922
+
923
+ for (let i = 0; i < data.depth_x.length; i++) {
924
  const x = data.depth_x[i];
925
+ netData.push({ time: x, value: data.depth_net[i] });
926
+ rawBids.push({ time: x, value: data.depth_bids[i] });
927
+ rawAsks.push({ time: x, value: data.depth_asks[i] });
928
+ }
929
+
 
 
 
 
930
  netSeries.setData(netData);
931
  rawBidSeries.setData(rawBids);
932
  rawAskSeries.setData(rawAsks);
933
+ }
934
+ };
935
+ }
936
  connect();
937
+ });
938
  </script>
939
  </body>
940
  </html>
 
945
  while True:
946
  try:
947
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
948
+ logging.info(f"Connected to Kraken ({SYMBOL_KRAKEN})")
949
  await ws.send(json.dumps({
950
  "method": "subscribe",
951
  "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}
 
960
  for item in data:
961
  for bid in item.get('bids', []):
962
  q, p = float(bid['qty']), float(bid['price'])
963
+ if q == 0:
964
+ market_state['bids'].pop(p, None)
965
+ else:
966
+ market_state['bids'][p] = q
967
  for ask in item.get('asks', []):
968
  q, p = float(ask['qty']), float(ask['price'])
969
+ if q == 0:
970
+ market_state['asks'].pop(p, None)
971
+ else:
972
+ market_state['asks'][p] = q
973
+
974
  if market_state['bids'] and market_state['asks']:
975
  best_bid = max(market_state['bids'].keys())
976
  best_ask = min(market_state['asks'].keys())
 
978
  market_state['prev_mid'] = market_state['current_mid']
979
  market_state['current_mid'] = mid
980
  market_state['ready'] = True
981
+
982
  now = time.time()
983
  if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
984
  market_state['history'].append({'t': now, 'p': mid})
 
986
  market_state['history'].pop(0)
987
 
988
  except Exception as e:
989
+ logging.warning(f"Reconnecting: {e}")
990
  await asyncio.sleep(3)
991
 
992
  async def broadcast_worker():
 
995
  payload = process_market_data()
996
  msg = json.dumps(payload)
997
  for ws in list(connected_clients):
998
+ try:
999
+ await ws.send_str(msg)
1000
+ except:
1001
+ pass
1002
  await asyncio.sleep(BROADCAST_RATE)
1003
 
1004
  async def websocket_handler(request):
 
1022
  async def cleanup_background(app):
1023
  app['kraken_task'].cancel()
1024
  app['broadcast_task'].cancel()
1025
+ try:
1026
+ await app['kraken_task']
1027
+ await app['broadcast_task']
1028
+ except:
1029
+ pass
1030
 
1031
  async def main():
1032
  app = web.Application()
 
1038
  await runner.setup()
1039
  site = web.TCPSite(runner, '0.0.0.0', PORT)
1040
  await site.start()
1041
+ print(f"Dashboard running at http://localhost:{PORT}")
1042
  await asyncio.Event().wait()
1043
 
1044
  if __name__ == "__main__":
1045
+ try:
1046
+ asyncio.run(main())
1047
+ except KeyboardInterrupt:
1048
+ pass