Alvin3y1 commited on
Commit
a574eba
·
verified ·
1 Parent(s): f95b3cf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +132 -77
app.py CHANGED
@@ -30,7 +30,8 @@ market_state = {
30
  "asks": {},
31
  "history": [],
32
  "pred_history": [],
33
- "trade_vol_history": [], # New: Store trade volume history
 
34
  "current_vol_window": {"buy": 0.0, "sell": 0.0, "start": time.time()},
35
  "current_mid": 0.0,
36
  "ready": False
@@ -66,7 +67,6 @@ def detect_anomalies(orders, scan_depth):
66
  def calculate_micro_price_structure(diff_x, diff_y_net, current_mid, best_bid, best_ask, walls):
67
  if not diff_x or len(diff_x) < 5: return None
68
 
69
- # Weighted Imbalance Calculation
70
  weighted_imbalance = 0.0
71
  total_weight = 0.0
72
 
@@ -79,12 +79,10 @@ def calculate_micro_price_structure(diff_x, diff_y_net, current_mid, best_bid, b
79
 
80
  rho = weighted_imbalance / total_weight if total_weight > 0 else 0
81
 
82
- # Base Projection
83
  spread = best_ask - best_bid
84
  theoretical_delta = (spread / 2) * rho * IMPACT_SENSITIVITY
85
  projected_price = current_mid + theoretical_delta
86
 
87
- # Wall Friction
88
  final_delta = theoretical_delta
89
  if final_delta > 0 and walls['asks']:
90
  nearest_wall = walls['asks'][0]
@@ -115,13 +113,10 @@ def process_market_data():
115
  'buy': market_state['current_vol_window']['buy'],
116
  'sell': market_state['current_vol_window']['sell']
117
  })
118
- if len(market_state['trade_vol_history']) > 60: # Keep last 60 seconds
119
  market_state['trade_vol_history'].pop(0)
120
-
121
- # Reset window
122
  market_state['current_vol_window'] = {"buy": 0.0, "sell": 0.0, "start": now}
123
 
124
- # Order Book Processing
125
  sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
126
  sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
127
 
@@ -182,6 +177,7 @@ def process_market_data():
182
  "history": market_state['history'],
183
  "pred_history": market_state['pred_history'],
184
  "trade_history": market_state['trade_vol_history'],
 
185
  "depth_x": diff_x,
186
  "depth_net": diff_y_net,
187
  "depth_bids": chart_bids,
@@ -247,30 +243,31 @@ HTML_PAGE = f"""
247
  z-index: 50;
248
  }}
249
  .status-left {{ display: flex; gap: 20px; align-items: center; }}
250
- .status-right {{ color: var(--text-dim); letter-spacing: 1px; }}
251
  .live-dot {{ width: 8px; height: 8px; background-color: var(--green); border-radius: 50%; display: inline-block; box-shadow: 0 0 8px var(--green); }}
252
  .ticker-val {{ font-weight: 700; color: #fff; font-size: 13px; }}
253
 
254
  #p-chart {{ grid-column: 1 / 2; grid-row: 2 / 3; }}
255
 
256
- #p-depth {{
 
257
  grid-column: 1 / 2; grid-row: 3 / 4;
258
  display: grid;
259
  grid-template-columns: 1fr 1fr;
260
  gap: 1px;
261
  background: var(--border);
262
  }}
263
- .depth-sub {{ background: var(--bg-panel); display: flex; flex-direction: column; position: relative; }}
264
 
 
265
  #p-sidebar {{
266
  grid-column: 2 / 3;
267
  grid-row: 2 / 4;
268
- padding: 20px;
269
  display: flex;
270
  flex-direction: column;
271
- gap: 15px; /* Tighter gap to fit the new chart */
272
  border-left: 1px solid var(--border);
273
- overflow-y: hidden;
274
  }}
275
 
276
  .chart-header {{
@@ -298,7 +295,7 @@ HTML_PAGE = f"""
298
  .c-red {{ color: var(--red); }}
299
  .c-dim {{ color: var(--text-dim); }}
300
 
301
- .list-container {{ display: flex; flex-direction: column; gap: 8px; overflow-y: auto; max-height: 120px; }}
302
  .list-item {{
303
  display: flex; justify-content: space-between;
304
  font-family: 'JetBrains Mono', monospace;
@@ -309,12 +306,18 @@ HTML_PAGE = f"""
309
  .list-item span:first-child {{ color: #e0e0e0; }}
310
  .list-item:last-child {{ border: none; }}
311
 
312
- #sidebar-chart {{
 
 
 
 
 
 
 
313
  flex: 1;
314
  background: rgba(255,255,255,0.02);
315
  border: 1px solid var(--border);
316
  border-radius: 4px;
317
- min-height: 100px;
318
  }}
319
  </style>
320
  </head>
@@ -330,24 +333,28 @@ HTML_PAGE = f"""
330
  <div class="status-right" id="clock">00:00:00 UTC</div>
331
  </div>
332
 
 
333
  <div id="p-chart" class="panel">
334
  <div class="chart-header">PRICE ACTION (BLUE) // PREDICTION (YELLOW)</div>
335
  <div id="tv-price" style="flex: 1; width: 100%;"></div>
336
  </div>
337
 
338
- <div id="p-depth">
339
- <div class="depth-sub">
340
- <div class="chart-header">LIQUIDITY DENSITY</div>
341
- <div id="tv-raw" style="flex: 1; width: 100%;"></div>
 
342
  </div>
343
- <div class="depth-sub">
344
  <div class="chart-header">ORDER FLOW IMBALANCE</div>
345
  <div id="tv-net" style="flex: 1; width: 100%;"></div>
346
  </div>
347
  </div>
348
 
 
349
  <div id="p-sidebar" class="panel">
350
 
 
351
  <div class="data-group">
352
  <span class="label">Micro-Price Delta</span>
353
  <div style="display:flex; align-items: baseline; gap: 10px;">
@@ -365,6 +372,7 @@ HTML_PAGE = f"""
365
 
366
  <div class="divider"></div>
367
 
 
368
  <div class="data-group">
369
  <span class="label">Detected Walls (Z > 3.0)</span>
370
  <div id="wall-list" class="list-container">
@@ -372,10 +380,16 @@ HTML_PAGE = f"""
372
  </div>
373
  </div>
374
 
375
- <!-- NEW CHART UNDER WALLS -->
376
- <div class="data-group" style="flex: 1; display:flex; flex-direction:column;">
377
- <span class="label">Real-time Volume (Ticks)</span>
378
- <div id="sidebar-chart"></div>
 
 
 
 
 
 
379
  </div>
380
  </div>
381
  </div>
@@ -403,35 +417,48 @@ HTML_PAGE = f"""
403
  crosshair: {{ mode: 1, vertLine: {{ color: '#444', labelBackgroundColor: '#444' }}, horzLine: {{ color: '#444', labelBackgroundColor: '#444' }} }}
404
  }};
405
 
406
- // 1. MAIN PRICE CHART
407
  const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts);
408
- const priceSeries = priceChart.addLineSeries({{ color: '#2979ff', lineWidth: 2, title: 'Price' }}); // BLUE
409
- const predSeries = priceChart.addLineSeries({{ color: '#ffeb3b', lineWidth: 2, lineStyle: 2, title: 'Forecast' }}); // YELLOW
410
 
411
- // 2. DEPTH CHARTS
412
- const rawChart = LightweightCharts.createChart(document.getElementById('tv-raw'), {{
413
- ...chartOpts, localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
 
 
 
 
414
  }});
415
- const bidSeries = rawChart.addAreaSeries({{ lineColor: '#00ff9d', topColor: 'rgba(0, 255, 157, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
416
- const askSeries = rawChart.addAreaSeries({{ lineColor: '#ff3b3b', topColor: 'rgba(255, 59, 59, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
417
 
 
418
  const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{
419
  ...chartOpts, localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
420
  }});
421
  const netSeries = netChart.addHistogramSeries({{ color: '#2979ff' }});
422
 
423
- // 3. SIDEBAR VOLUME CHART
424
- const volChart = LightweightCharts.createChart(document.getElementById('sidebar-chart'), {{
425
  ...chartOpts,
426
  grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }},
427
  rightPriceScale: {{ visible: false }},
428
  timeScale: {{ visible: false }},
429
- handleScroll: false,
430
- handleScale: false
431
  }});
432
  const volBuySeries = volChart.addHistogramSeries({{ color: '#00ff9d' }});
433
  const volSellSeries = volChart.addHistogramSeries({{ color: '#ff3b3b' }});
434
 
 
 
 
 
 
 
 
 
 
 
 
435
  let activeLines = [];
436
 
437
  // RESIZE OBSERVER
@@ -439,19 +466,24 @@ HTML_PAGE = f"""
439
  for(let entry of entries) {{
440
  const {{width, height}} = entry.contentRect;
441
  if(entry.target.id === 'tv-price') priceChart.applyOptions({{width, height}});
442
- if(entry.target.id === 'tv-raw') rawChart.applyOptions({{width, height}});
443
  if(entry.target.id === 'tv-net') netChart.applyOptions({{width, height}});
444
- if(entry.target.id === 'sidebar-chart') volChart.applyOptions({{width, height}});
 
445
  }}
446
  }}).observe(document.body);
447
 
448
- ['tv-price', 'tv-raw', 'tv-net', 'sidebar-chart'].forEach(id => {{
449
  new ResizeObserver(e => {{
450
  const t = document.getElementById(id);
451
- if(id === 'tv-price') priceChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
452
- if(id === 'tv-raw') rawChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
453
- if(id === 'tv-net') netChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
454
- if(id === 'sidebar-chart') volChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
 
 
 
 
455
  }}).observe(document.getElementById(id));
456
  }});
457
 
@@ -462,6 +494,7 @@ HTML_PAGE = f"""
462
  const data = JSON.parse(e.data);
463
  if (data.error) return;
464
 
 
465
  if (data.history.length) {{
466
  const hist = data.history.map(d => ({{ time: Math.floor(d.t), value: d.p }}));
467
  const cleanHist = [...new Map(hist.map(i => [i.time, i])).values()];
@@ -484,52 +517,55 @@ HTML_PAGE = f"""
484
 
485
  dom.projPct.innerText = `${{sign}}${{pct.toFixed(4)}}%`;
486
  dom.projPct.style.color = pct >= 0 ? "var(--green)" : "var(--red)";
487
-
488
  dom.projVal.innerText = proj.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
489
-
490
  dom.score.innerText = rho.toFixed(3);
491
  dom.score.style.color = rho > 0 ? "var(--green)" : (rho < 0 ? "var(--red)" : "var(--text-main)");
492
  }}
493
  }}
494
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
  // WALLS
496
  if (data.walls) {{
497
  activeLines.forEach(l => priceSeries.removePriceLine(l));
498
  activeLines = [];
499
  let html = "";
500
-
501
  const addWall = (w, type) => {{
502
  const color = type === 'BID' ? '#00ff9d' : '#ff3b3b';
503
  activeLines.push(priceSeries.createPriceLine({{ price: w.price, color: color, lineWidth: 1, lineStyle: 2, axisLabelVisible: false }}));
504
- html += `<div class="list-item">
505
- <span style="color:${{color}}">${{type}} ${{w.price}}</span>
506
- <span class="c-dim">Z:${{w.z_score.toFixed(1)}}</span>
507
- </div>`;
508
  }};
509
-
510
  data.walls.asks.forEach(w => addWall(w, 'ASK'));
511
  data.walls.bids.forEach(w => addWall(w, 'BID'));
512
  dom.wallList.innerHTML = html || '<span class="c-dim" style="font-size:11px">Scanning...</span>';
513
  }}
514
 
515
- // VOLUME CHART IN SIDEBAR
516
  if (data.trade_history && data.trade_history.length) {{
517
- const buyData = [];
518
- const sellData = [];
519
  data.trade_history.forEach(t => {{
520
  const time = Math.floor(t.t);
521
  buyData.push({{ time: time, value: t.buy }});
522
  sellData.push({{ time: time, value: t.sell }});
523
  }});
524
- // Ensure unique time points for LW Charts
525
- const uniqueBuys = [...new Map(buyData.map(i => [i.time, i])).values()];
526
- const uniqueSells = [...new Map(sellData.map(i => [i.time, i])).values()];
527
-
528
- volBuySeries.setData(uniqueBuys);
529
- volSellSeries.setData(uniqueSells);
530
  }}
531
 
532
- // DEPTH
533
  if (data.depth_x.length) {{
534
  const bids = [], asks = [], nets = [];
535
  for(let i=0; i<data.depth_x.length; i++) {{
@@ -543,7 +579,6 @@ HTML_PAGE = f"""
543
  netSeries.setData(nets);
544
  }}
545
  }};
546
-
547
  ws.onclose = () => setTimeout(connect, 2000);
548
  }}
549
  connect();
@@ -561,7 +596,6 @@ async def kraken_worker():
561
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
562
  logging.info(f"🔌 Connected to Kraken ({SYMBOL_KRAKEN})")
563
 
564
- # SUBSCRIBE TO BOOK AND TRADES
565
  await ws.send(json.dumps({
566
  "method": "subscribe",
567
  "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}
@@ -570,6 +604,11 @@ async def kraken_worker():
570
  "method": "subscribe",
571
  "params": {"channel": "trade", "symbol": [SYMBOL_KRAKEN]}
572
  }))
 
 
 
 
 
573
 
574
  async for message in ws:
575
  payload = json.loads(message)
@@ -602,18 +641,36 @@ async def kraken_worker():
602
  market_state['history'].pop(0)
603
 
604
  elif channel == "trade":
605
- # Process trades for volume history
606
  for trade in data:
607
- # Kraken Trade format: [price, qty, time, side, order_type, misc]
608
- # side: 'buy' or 'sell'
609
  try:
610
  qty = float(trade['qty'])
611
- side = trade['side'] # 'buy' or 'sell'
612
- if side == 'buy':
613
- market_state['current_vol_window']['buy'] += qty
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
  else:
615
- market_state['current_vol_window']['sell'] += qty
616
- except:
 
 
617
  pass
618
 
619
  except Exception as e:
@@ -634,10 +691,8 @@ async def websocket_handler(request):
634
  ws = web.WebSocketResponse()
635
  await ws.prepare(request)
636
  connected_clients.add(ws)
637
- try:
638
- async for msg in ws: pass
639
- finally:
640
- connected_clients.remove(ws)
641
  return ws
642
 
643
  async def handle_index(request):
 
30
  "asks": {},
31
  "history": [],
32
  "pred_history": [],
33
+ "trade_vol_history": [],
34
+ "ohlc_history": [], # Stores 1m candles
35
  "current_vol_window": {"buy": 0.0, "sell": 0.0, "start": time.time()},
36
  "current_mid": 0.0,
37
  "ready": False
 
67
  def calculate_micro_price_structure(diff_x, diff_y_net, current_mid, best_bid, best_ask, walls):
68
  if not diff_x or len(diff_x) < 5: return None
69
 
 
70
  weighted_imbalance = 0.0
71
  total_weight = 0.0
72
 
 
79
 
80
  rho = weighted_imbalance / total_weight if total_weight > 0 else 0
81
 
 
82
  spread = best_ask - best_bid
83
  theoretical_delta = (spread / 2) * rho * IMPACT_SENSITIVITY
84
  projected_price = current_mid + theoretical_delta
85
 
 
86
  final_delta = theoretical_delta
87
  if final_delta > 0 and walls['asks']:
88
  nearest_wall = walls['asks'][0]
 
113
  'buy': market_state['current_vol_window']['buy'],
114
  'sell': market_state['current_vol_window']['sell']
115
  })
116
+ if len(market_state['trade_vol_history']) > 60:
117
  market_state['trade_vol_history'].pop(0)
 
 
118
  market_state['current_vol_window'] = {"buy": 0.0, "sell": 0.0, "start": now}
119
 
 
120
  sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
121
  sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
122
 
 
177
  "history": market_state['history'],
178
  "pred_history": market_state['pred_history'],
179
  "trade_history": market_state['trade_vol_history'],
180
+ "ohlc": market_state['ohlc_history'],
181
  "depth_x": diff_x,
182
  "depth_net": diff_y_net,
183
  "depth_bids": chart_bids,
 
243
  z-index: 50;
244
  }}
245
  .status-left {{ display: flex; gap: 20px; align-items: center; }}
 
246
  .live-dot {{ width: 8px; height: 8px; background-color: var(--green); border-radius: 50%; display: inline-block; box-shadow: 0 0 8px var(--green); }}
247
  .ticker-val {{ font-weight: 700; color: #fff; font-size: 13px; }}
248
 
249
  #p-chart {{ grid-column: 1 / 2; grid-row: 2 / 3; }}
250
 
251
+ /* BOTTOM GRID */
252
+ #p-bottom {{
253
  grid-column: 1 / 2; grid-row: 3 / 4;
254
  display: grid;
255
  grid-template-columns: 1fr 1fr;
256
  gap: 1px;
257
  background: var(--border);
258
  }}
259
+ .bottom-sub {{ background: var(--bg-panel); display: flex; flex-direction: column; position: relative; }}
260
 
261
+ /* SIDEBAR */
262
  #p-sidebar {{
263
  grid-column: 2 / 3;
264
  grid-row: 2 / 4;
265
+ padding: 15px;
266
  display: flex;
267
  flex-direction: column;
268
+ gap: 15px;
269
  border-left: 1px solid var(--border);
270
+ overflow: hidden;
271
  }}
272
 
273
  .chart-header {{
 
295
  .c-red {{ color: var(--red); }}
296
  .c-dim {{ color: var(--text-dim); }}
297
 
298
+ .list-container {{ display: flex; flex-direction: column; gap: 8px; overflow-y: auto; height: 100px; }}
299
  .list-item {{
300
  display: flex; justify-content: space-between;
301
  font-family: 'JetBrains Mono', monospace;
 
306
  .list-item span:first-child {{ color: #e0e0e0; }}
307
  .list-item:last-child {{ border: none; }}
308
 
309
+ /* SIDEBAR CHARTS */
310
+ .sidebar-chart-box {{
311
+ flex: 1;
312
+ display: flex;
313
+ flex-direction: column;
314
+ min-height: 0; /* Important for flex scaling */
315
+ }}
316
+ .mini-chart {{
317
  flex: 1;
318
  background: rgba(255,255,255,0.02);
319
  border: 1px solid var(--border);
320
  border-radius: 4px;
 
321
  }}
322
  </style>
323
  </head>
 
333
  <div class="status-right" id="clock">00:00:00 UTC</div>
334
  </div>
335
 
336
+ <!-- TOP LEFT: Price Line -->
337
  <div id="p-chart" class="panel">
338
  <div class="chart-header">PRICE ACTION (BLUE) // PREDICTION (YELLOW)</div>
339
  <div id="tv-price" style="flex: 1; width: 100%;"></div>
340
  </div>
341
 
342
+ <!-- BOTTOM: Candles + Imbalance -->
343
+ <div id="p-bottom">
344
+ <div class="bottom-sub">
345
+ <div class="chart-header">1M KLINE (KRAKEN OHLC)</div>
346
+ <div id="tv-candles" style="flex: 1; width: 100%;"></div>
347
  </div>
348
+ <div class="bottom-sub">
349
  <div class="chart-header">ORDER FLOW IMBALANCE</div>
350
  <div id="tv-net" style="flex: 1; width: 100%;"></div>
351
  </div>
352
  </div>
353
 
354
+ <!-- SIDEBAR -->
355
  <div id="p-sidebar" class="panel">
356
 
357
+ <!-- STATS -->
358
  <div class="data-group">
359
  <span class="label">Micro-Price Delta</span>
360
  <div style="display:flex; align-items: baseline; gap: 10px;">
 
372
 
373
  <div class="divider"></div>
374
 
375
+ <!-- WALLS -->
376
  <div class="data-group">
377
  <span class="label">Detected Walls (Z > 3.0)</span>
378
  <div id="wall-list" class="list-container">
 
380
  </div>
381
  </div>
382
 
383
+ <!-- VOL CHART -->
384
+ <div class="sidebar-chart-box">
385
+ <span class="label" style="margin-bottom:4px;">Real-time Volume Ticks</span>
386
+ <div id="sidebar-vol" class="mini-chart"></div>
387
+ </div>
388
+
389
+ <!-- DENSITY CHART -->
390
+ <div class="sidebar-chart-box">
391
+ <span class="label" style="margin-bottom:4px;">Liquidity Density</span>
392
+ <div id="sidebar-density" class="mini-chart"></div>
393
  </div>
394
  </div>
395
  </div>
 
417
  crosshair: {{ mode: 1, vertLine: {{ color: '#444', labelBackgroundColor: '#444' }}, horzLine: {{ color: '#444', labelBackgroundColor: '#444' }} }}
418
  }};
419
 
420
+ // 1. PRICE LINE
421
  const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts);
422
+ const priceSeries = priceChart.addLineSeries({{ color: '#2979ff', lineWidth: 2, title: 'Price' }});
423
+ const predSeries = priceChart.addLineSeries({{ color: '#ffeb3b', lineWidth: 2, lineStyle: 2, title: 'Forecast' }});
424
 
425
+ // 2. CANDLESTICK CHART (NEW)
426
+ const candleChart = LightweightCharts.createChart(document.getElementById('tv-candles'), {{
427
+ ...chartOpts,
428
+ timeScale: {{ timeVisible: true, secondsVisible: false }}
429
+ }});
430
+ const candleSeries = candleChart.addCandlestickSeries({{
431
+ upColor: '#00ff9d', downColor: '#ff3b3b', borderVisible: false, wickUpColor: '#00ff9d', wickDownColor: '#ff3b3b'
432
  }});
 
 
433
 
434
+ // 3. NET IMBALANCE
435
  const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{
436
  ...chartOpts, localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
437
  }});
438
  const netSeries = netChart.addHistogramSeries({{ color: '#2979ff' }});
439
 
440
+ // 4. SIDEBAR: VOLUME
441
+ const volChart = LightweightCharts.createChart(document.getElementById('sidebar-vol'), {{
442
  ...chartOpts,
443
  grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }},
444
  rightPriceScale: {{ visible: false }},
445
  timeScale: {{ visible: false }},
446
+ handleScroll: false, handleScale: false
 
447
  }});
448
  const volBuySeries = volChart.addHistogramSeries({{ color: '#00ff9d' }});
449
  const volSellSeries = volChart.addHistogramSeries({{ color: '#ff3b3b' }});
450
 
451
+ // 5. SIDEBAR: DENSITY (MOVED)
452
+ const denChart = LightweightCharts.createChart(document.getElementById('sidebar-density'), {{
453
+ ...chartOpts,
454
+ grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }},
455
+ rightPriceScale: {{ visible: false }},
456
+ timeScale: {{ visible: false }}, /* Hide X axis to fit */
457
+ handleScroll: false, handleScale: false
458
+ }});
459
+ const bidSeries = denChart.addAreaSeries({{ lineColor: '#00ff9d', topColor: 'rgba(0, 255, 157, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
460
+ const askSeries = denChart.addAreaSeries({{ lineColor: '#ff3b3b', topColor: 'rgba(255, 59, 59, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
461
+
462
  let activeLines = [];
463
 
464
  // RESIZE OBSERVER
 
466
  for(let entry of entries) {{
467
  const {{width, height}} = entry.contentRect;
468
  if(entry.target.id === 'tv-price') priceChart.applyOptions({{width, height}});
469
+ if(entry.target.id === 'tv-candles') candleChart.applyOptions({{width, height}});
470
  if(entry.target.id === 'tv-net') netChart.applyOptions({{width, height}});
471
+ if(entry.target.id === 'sidebar-vol') volChart.applyOptions({{width, height}});
472
+ if(entry.target.id === 'sidebar-density') denChart.applyOptions({{width, height}});
473
  }}
474
  }}).observe(document.body);
475
 
476
+ ['tv-price', 'tv-candles', 'tv-net', 'sidebar-vol', 'sidebar-density'].forEach(id => {{
477
  new ResizeObserver(e => {{
478
  const t = document.getElementById(id);
479
+ // Simple check to ensure chart fits container
480
+ if (t.clientWidth && t.clientHeight) {{
481
+ if(id === 'tv-price') priceChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
482
+ if(id === 'tv-candles') candleChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
483
+ if(id === 'tv-net') netChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
484
+ if(id === 'sidebar-vol') volChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
485
+ if(id === 'sidebar-density') denChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
486
+ }}
487
  }}).observe(document.getElementById(id));
488
  }});
489
 
 
494
  const data = JSON.parse(e.data);
495
  if (data.error) return;
496
 
497
+ // MAIN LINE & TEXT
498
  if (data.history.length) {{
499
  const hist = data.history.map(d => ({{ time: Math.floor(d.t), value: d.p }}));
500
  const cleanHist = [...new Map(hist.map(i => [i.time, i])).values()];
 
517
 
518
  dom.projPct.innerText = `${{sign}}${{pct.toFixed(4)}}%`;
519
  dom.projPct.style.color = pct >= 0 ? "var(--green)" : "var(--red)";
 
520
  dom.projVal.innerText = proj.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
 
521
  dom.score.innerText = rho.toFixed(3);
522
  dom.score.style.color = rho > 0 ? "var(--green)" : (rho < 0 ? "var(--red)" : "var(--text-main)");
523
  }}
524
  }}
525
 
526
+ // CANDLES
527
+ if (data.ohlc && data.ohlc.length) {{
528
+ // Map backend format to LW Charts
529
+ const candles = data.ohlc.map(c => ({{
530
+ time: c.time,
531
+ open: c.open,
532
+ high: c.high,
533
+ low: c.low,
534
+ close: c.close
535
+ }}));
536
+ // De-dupe by time
537
+ const uniqueCandles = [...new Map(candles.map(i => [i.time, i])).values()];
538
+ candleSeries.setData(uniqueCandles);
539
+ }}
540
+
541
  // WALLS
542
  if (data.walls) {{
543
  activeLines.forEach(l => priceSeries.removePriceLine(l));
544
  activeLines = [];
545
  let html = "";
 
546
  const addWall = (w, type) => {{
547
  const color = type === 'BID' ? '#00ff9d' : '#ff3b3b';
548
  activeLines.push(priceSeries.createPriceLine({{ price: w.price, color: color, lineWidth: 1, lineStyle: 2, axisLabelVisible: false }}));
549
+ 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>`;
 
 
 
550
  }};
 
551
  data.walls.asks.forEach(w => addWall(w, 'ASK'));
552
  data.walls.bids.forEach(w => addWall(w, 'BID'));
553
  dom.wallList.innerHTML = html || '<span class="c-dim" style="font-size:11px">Scanning...</span>';
554
  }}
555
 
556
+ // SIDEBAR: VOL
557
  if (data.trade_history && data.trade_history.length) {{
558
+ const buyData = [], sellData = [];
 
559
  data.trade_history.forEach(t => {{
560
  const time = Math.floor(t.t);
561
  buyData.push({{ time: time, value: t.buy }});
562
  sellData.push({{ time: time, value: t.sell }});
563
  }});
564
+ volBuySeries.setData([...new Map(buyData.map(i => [i.time, i])).values()]);
565
+ volSellSeries.setData([...new Map(sellData.map(i => [i.time, i])).values()]);
 
 
 
 
566
  }}
567
 
568
+ // SIDEBAR: DENSITY (Moved)
569
  if (data.depth_x.length) {{
570
  const bids = [], asks = [], nets = [];
571
  for(let i=0; i<data.depth_x.length; i++) {{
 
579
  netSeries.setData(nets);
580
  }}
581
  }};
 
582
  ws.onclose = () => setTimeout(connect, 2000);
583
  }}
584
  connect();
 
596
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
597
  logging.info(f"🔌 Connected to Kraken ({SYMBOL_KRAKEN})")
598
 
 
599
  await ws.send(json.dumps({
600
  "method": "subscribe",
601
  "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}
 
604
  "method": "subscribe",
605
  "params": {"channel": "trade", "symbol": [SYMBOL_KRAKEN]}
606
  }))
607
+ # OHLC Interval 1 minute
608
+ await ws.send(json.dumps({
609
+ "method": "subscribe",
610
+ "params": {"channel": "ohlc", "symbol": [SYMBOL_KRAKEN], "interval": 1}
611
+ }))
612
 
613
  async for message in ws:
614
  payload = json.loads(message)
 
641
  market_state['history'].pop(0)
642
 
643
  elif channel == "trade":
 
644
  for trade in data:
 
 
645
  try:
646
  qty = float(trade['qty'])
647
+ side = trade['side']
648
+ if side == 'buy': market_state['current_vol_window']['buy'] += qty
649
+ else: market_state['current_vol_window']['sell'] += qty
650
+ except: pass
651
+
652
+ elif channel == "ohlc":
653
+ # Kraken v2 OHLC data format:
654
+ # [{"time": "...", "open": "...", "high": "...", "low": "...", "close": "...", ...}]
655
+ for candle in data:
656
+ try:
657
+ c_data = {
658
+ # Kraken sends end time as timestamp, we use it for chart
659
+ 'time': int(float(candle['endtime'])),
660
+ 'open': float(candle['open']),
661
+ 'high': float(candle['high']),
662
+ 'low': float(candle['low']),
663
+ 'close': float(candle['close'])
664
+ }
665
+ # Update or Append
666
+ # If last candle has same time, replace it, else append
667
+ if market_state['ohlc_history'] and market_state['ohlc_history'][-1]['time'] == c_data['time']:
668
+ market_state['ohlc_history'][-1] = c_data
669
  else:
670
+ market_state['ohlc_history'].append(c_data)
671
+ if len(market_state['ohlc_history']) > 100:
672
+ market_state['ohlc_history'].pop(0)
673
+ except Exception as e:
674
  pass
675
 
676
  except Exception as e:
 
691
  ws = web.WebSocketResponse()
692
  await ws.prepare(request)
693
  connected_clients.add(ws)
694
+ try: async for msg in ws: pass
695
+ finally: connected_clients.remove(ws)
 
 
696
  return ws
697
 
698
  async def handle_index(request):