Alvin3y1 commited on
Commit
7c6576b
·
verified ·
1 Parent(s): 25c08cc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +239 -230
app.py CHANGED
@@ -12,24 +12,24 @@ 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
@@ -65,7 +65,7 @@ def calculate_micro_price_structure(diff_x, diff_y_net, current_mid, best_bid, b
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]
@@ -114,7 +114,7 @@ def process_market_data():
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]
@@ -139,7 +139,7 @@ def process_market_data():
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
@@ -160,7 +160,7 @@ def process_market_data():
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']})
@@ -209,7 +209,6 @@ HTML_PAGE = f"""
209
  overflow: hidden;
210
  height: 100vh; width: 100vw;
211
  }}
212
-
213
  .layout {{
214
  display: grid;
215
  grid-template-rows: 34px 1fr 1fr;
@@ -219,7 +218,6 @@ HTML_PAGE = f"""
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 {{
@@ -313,254 +311,265 @@ HTML_PAGE = f"""
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>
559
  """
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"
 
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
 
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]
 
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]
 
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
 
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']})
 
209
  overflow: hidden;
210
  height: 100vh; width: 100vw;
211
  }}
 
212
  .layout {{
213
  display: grid;
214
  grid-template-rows: 34px 1fr 1fr;
 
218
  height: 100vh;
219
  box-sizing: border-box;
220
  }}
 
221
  .panel {{ background: var(--bg-panel); display: flex; flex-direction: column; overflow: hidden; }}
222
 
223
  .status-bar {{
 
311
  </style>
312
  </head>
313
  <body>
314
+ <div class="layout">
315
+ <div class="status-bar">
316
+ <div class="status-left">
317
+ <span class="live-dot"></span>
318
+ <span style="font-weight:700; color:#fff;">{SYMBOL_KRAKEN}</span>
319
+ <span id="price-ticker" class="ticker-val">---</span>
 
 
 
320
  </div>
321
+ <div class="status-right" id="clock">00:00:00 UTC</div>
322
+ </div>
323
 
324
+ <div id="p-chart" class="panel">
325
+ <div class="chart-header">PRICE ACTION (BLUE) // PREDICTION (YELLOW)</div>
326
+ <div id="tv-price" style="flex: 1; width: 100%;"></div>
327
+ </div>
328
 
329
+ <div id="p-bottom">
330
+ <div class="bottom-sub">
331
+ <div class="chart-header">1M KLINE (KRAKEN OHLC)</div>
332
+ <div id="tv-candles" style="flex: 1; width: 100%;"></div>
 
 
 
 
 
333
  </div>
334
+ <div class="bottom-sub">
335
+ <div class="chart-header">ORDER FLOW IMBALANCE</div>
336
+ <div id="tv-net" style="flex: 1; width: 100%;"></div>
337
+ </div>
338
+ </div>
339
 
340
+ <div id="p-sidebar" class="panel">
341
+
342
+ <div class="data-group">
343
+ <span class="label">Micro-Price Delta</span>
344
+ <div style="display:flex; align-items: baseline; gap: 10px;">
345
+ <span id="proj-pct" class="value value-lg">--%</span>
346
+ <span id="proj-val" class="value-sub">---</span>
 
347
  </div>
348
+ </div>
349
 
350
+ <div class="divider"></div>
351
 
352
+ <div class="data-group">
353
+ <span class="label">OFI Imbalance Ratio</span>
354
+ <span id="score-val" class="value">0.00</span>
355
+ </div>
356
 
357
+ <div class="divider"></div>
358
 
359
+ <div class="data-group">
360
+ <span class="label">Detected Walls (Z > 3.0)</span>
361
+ <div id="wall-list" class="list-container">
362
+ <span class="c-dim" style="font-size: 11px;">Scanning...</span>
 
363
  </div>
364
+ </div>
365
 
366
+ <div class="sidebar-chart-box">
367
+ <span class="label" style="margin-bottom:4px;">Real-time Volume Ticks</span>
368
+ <div id="sidebar-vol" class="mini-chart"></div>
369
+ </div>
370
 
371
+ <div class="sidebar-chart-box">
372
+ <span class="label" style="margin-bottom:4px;">Liquidity Density</span>
373
+ <div id="sidebar-density" class="mini-chart"></div>
 
374
  </div>
375
  </div>
376
+ </div>
377
+
378
+ <script>
379
+ setInterval(() => {{
380
+ const now = new Date();
381
+ document.getElementById('clock').innerText = now.toISOString().split('T')[1].split('.')[0] + ' UTC';
382
+ }}, 1000);
383
+
384
+ document.addEventListener('DOMContentLoaded', () => {{
385
+ const dom = {{
386
+ ticker: document.getElementById('price-ticker'),
387
+ score: document.getElementById('score-val'),
388
+ projVal: document.getElementById('proj-val'),
389
+ projPct: document.getElementById('proj-pct'),
390
+ wallList: document.getElementById('wall-list')
391
+ }};
392
+
393
+ const chartOpts = {{
394
+ layout: {{ background: {{ type: 'solid', color: '#0a0a0a' }}, textColor: '#888', fontFamily: 'JetBrains Mono' }},
395
+ grid: {{ vertLines: {{ color: '#151515' }}, horzLines: {{ color: '#151515' }} }},
396
+ rightPriceScale: {{ borderColor: '#222', scaleMargins: {{ top: 0.1, bottom: 0.1 }} }},
397
+ timeScale: {{ borderColor: '#222', timeVisible: true, secondsVisible: true }},
398
+ crosshair: {{ mode: 1, vertLine: {{ color: '#444', labelBackgroundColor: '#444' }}, horzLine: {{ color: '#444', labelBackgroundColor: '#444' }} }}
399
+ }};
400
+
401
+ const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts);
402
+ const priceSeries = priceChart.addLineSeries({{ color: '#2979ff', lineWidth: 2, title: 'Price' }});
403
+ const predSeries = priceChart.addLineSeries({{ color: '#ffeb3b', lineWidth: 2, lineStyle: 2, title: 'Forecast' }});
404
+
405
+ const candleChart = LightweightCharts.createChart(document.getElementById('tv-candles'), {{
406
+ ...chartOpts,
407
+ timeScale: {{ timeVisible: true, secondsVisible: false }}
408
+ }});
409
+ const candleSeries = candleChart.addCandlestickSeries({{
410
+ upColor: '#00ff9d', downColor: '#ff3b3b', borderVisible: false, wickUpColor: '#00ff9d', wickDownColor: '#ff3b3b'
411
+ }});
412
 
413
+ const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{
414
+ ...chartOpts, localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
415
+ }});
416
+ const netSeries = netChart.addHistogramSeries({{ color: '#2979ff' }});
417
+
418
+ const volChart = LightweightCharts.createChart(document.getElementById('sidebar-vol'), {{
419
+ ...chartOpts,
420
+ grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }},
421
+ rightPriceScale: {{ visible: false }},
422
+ timeScale: {{ visible: false }},
423
+ handleScroll: false, handleScale: false
424
+ }});
425
+ const volBuySeries = volChart.addHistogramSeries({{ color: '#00ff9d' }});
426
+ const volSellSeries = volChart.addHistogramSeries({{ color: '#ff3b3b' }});
427
+
428
+ const denChart = LightweightCharts.createChart(document.getElementById('sidebar-density'), {{
429
+ ...chartOpts,
430
+ grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }},
431
+ rightPriceScale: {{ visible: false }},
432
+ timeScale: {{ visible: false }},
433
+ handleScroll: false, handleScale: false
434
+ }});
435
+ const bidSeries = denChart.addAreaSeries({{ lineColor: '#00ff9d', topColor: 'rgba(0, 255, 157, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
436
+ const askSeries = denChart.addAreaSeries({{ lineColor: '#ff3b3b', topColor: 'rgba(255, 59, 59, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
437
+
438
+ let activeLines = [];
439
+ let activeCandleLines = []; // New array to track lines on the candle chart
440
+
441
+ new ResizeObserver(entries => {{
442
+ for(let entry of entries) {{
443
+ const {{width, height}} = entry.contentRect;
444
+ if(entry.target.id === 'tv-price') priceChart.applyOptions({{width, height}});
445
+ if(entry.target.id === 'tv-candles') candleChart.applyOptions({{width, height}});
446
+ if(entry.target.id === 'tv-net') netChart.applyOptions({{width, height}});
447
+ if(entry.target.id === 'sidebar-vol') volChart.applyOptions({{width, height}});
448
+ if(entry.target.id === 'sidebar-density') denChart.applyOptions({{width, height}});
449
+ }}
450
+ }}).observe(document.body);
451
+
452
+ ['tv-price', 'tv-candles', 'tv-net', 'sidebar-vol', 'sidebar-density'].forEach(id => {{
453
+ new ResizeObserver(e => {{
454
+ const t = document.getElementById(id);
455
+ if (t.clientWidth && t.clientHeight) {{
456
+ if(id === 'tv-price') priceChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
457
+ if(id === 'tv-candles') candleChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
458
+ if(id === 'tv-net') netChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
459
+ if(id === 'sidebar-vol') volChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
460
+ if(id === 'sidebar-density') denChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
461
+ }}
462
+ }}).observe(document.getElementById(id));
463
+ }});
464
 
465
+ function connect() {{
466
+ const ws = new WebSocket((location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/ws');
467
+
468
+ ws.onmessage = (e) => {{
469
+ const data = JSON.parse(e.data);
470
+ if (data.error) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
 
472
+ if (data.history.length) {{
473
+ const hist = data.history.map(d => ({{ time: Math.floor(d.t), value: d.p }}));
474
+ const cleanHist = [...new Map(hist.map(i => [i.time, i])).values()];
475
+ priceSeries.setData(cleanHist);
476
 
477
+ const lastP = cleanHist[cleanHist.length-1].value;
478
+ dom.ticker.innerText = lastP.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
479
 
480
+ if (data.analysis) {{
481
+ const proj = data.analysis.projected;
482
+ const rho = data.analysis.rho;
483
 
484
+ predSeries.setData([
485
+ cleanHist[cleanHist.length-1],
486
+ {{ time: cleanHist[cleanHist.length-1].time + 60, value: proj }}
487
+ ]);
488
 
489
+ const pct = ((proj - lastP) / lastP) * 100;
490
+ const sign = pct >= 0 ? "+" : "";
491
+
492
+ dom.projPct.innerText = `${{sign}}${{pct.toFixed(4)}}%`;
493
+ dom.projPct.style.color = pct >= 0 ? "var(--green)" : "var(--red)";
494
+ dom.projVal.innerText = proj.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
495
+ dom.score.innerText = rho.toFixed(3);
496
+ dom.score.style.color = rho > 0 ? "var(--green)" : (rho < 0 ? "var(--red)" : "var(--text-main)");
 
497
  }}
498
+ }}
499
 
500
+ if (data.ohlc && data.ohlc.length) {{
501
+ const candles = data.ohlc.map(c => ({{
502
+ time: c.time,
503
+ open: c.open,
504
+ high: c.high,
505
+ low: c.low,
506
+ close: c.close
507
+ }}));
508
+ const uniqueCandles = [...new Map(candles.map(i => [i.time, i])).values()];
509
+ candleSeries.setData(uniqueCandles);
510
+ }}
511
 
512
+ if (data.walls) {{
513
+ // Remove old lines from both charts
514
+ activeLines.forEach(l => priceSeries.removePriceLine(l));
515
+ activeLines = [];
516
+ activeCandleLines.forEach(l => candleSeries.removePriceLine(l));
517
+ activeCandleLines = [];
518
+
519
+ let html = "";
520
+ const addWall = (w, type) => {{
521
+ const color = type === 'BID' ? '#00ff9d' : '#ff3b3b';
522
+ const lineOpts = {{ price: w.price, color: color, lineWidth: 1, lineStyle: 2, axisLabelVisible: false }};
523
+
524
+ // Add to Price Chart
525
+ activeLines.push(priceSeries.createPriceLine(lineOpts));
526
+
527
+ // Add to Candle Chart
528
+ activeCandleLines.push(candleSeries.createPriceLine(lineOpts));
529
 
530
+ 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>`;
531
+ }};
532
+
533
+ data.walls.asks.forEach(w => addWall(w, 'ASK'));
534
+ data.walls.bids.forEach(w => addWall(w, 'BID'));
535
+ dom.wallList.innerHTML = html || '<span class="c-dim" style="font-size:11px">Scanning...</span>';
536
+ }}
 
 
 
537
 
538
+ if (data.trade_history && data.trade_history.length) {{
539
+ const buyData = [], sellData = [];
540
+ data.trade_history.forEach(t => {{
541
+ const time = Math.floor(t.t);
542
+ buyData.push({{ time: time, value: t.buy }});
543
+ sellData.push({{ time: time, value: t.sell }});
544
+ }});
545
+ volBuySeries.setData([...new Map(buyData.map(i => [i.time, i])).values()]);
546
+ volSellSeries.setData([...new Map(sellData.map(i => [i.time, i])).values()]);
547
+ }}
548
+
549
+ if (data.depth_x.length) {{
550
+ const bids = [], asks = [], nets = [];
551
+ for(let i=0; i<data.depth_x.length; i++) {{
552
+ const t = data.depth_x[i];
553
+ bids.push({{ time: t, value: data.depth_bids[i] }});
554
+ asks.push({{ time: t, value: data.depth_asks[i] }});
555
+ nets.push({{ time: t, value: data.depth_net[i], color: data.depth_net[i] > 0 ? '#00ff9d' : '#ff3b3b' }});
556
  }}
557
+ bidSeries.setData(bids);
558
+ askSeries.setData(asks);
559
+ netSeries.setData(nets);
560
+ }}
561
+ }};
562
+ ws.onclose = () => setTimeout(connect, 2000);
563
+ }}
564
+ connect();
565
+ }});
566
+ </script>
567
  </body>
568
  </html>
569
  """
570
 
571
  async def kraken_worker():
572
  global market_state
 
573
  try:
574
  async with aiohttp.ClientSession() as session:
575
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"