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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +309 -792
app.py CHANGED
@@ -8,22 +8,26 @@ import statistics
8
  from aiohttp import web
9
  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
- 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
@@ -31,15 +35,12 @@ market_state = {
31
 
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)
@@ -47,30 +48,23 @@ def detect_anomalies(orders, scan_depth):
47
  except statistics.StatisticsError:
48
  return []
49
 
50
- if stdev_vol == 0:
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,
60
- "vol": qty,
61
- "z_score": z_score
62
- })
63
 
64
  walls.sort(key=lambda x: x['z_score'], reverse=True)
65
  return walls[:3]
66
 
67
  def analyze_structure(diff_x, diff_y, current_mid):
68
- if not diff_y or len(diff_y) < 5:
69
- return None
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]
@@ -81,24 +75,17 @@ def analyze_structure(diff_x, diff_y, current_mid):
81
 
82
  if weighted_imbalance != 0:
83
  impact = math.sqrt(abs(weighted_imbalance)) * IMPACT_SENSITIVITY
84
- if weighted_imbalance < 0:
85
- impact = -impact
86
  else:
87
  impact = 0.0
88
 
89
  projected_price = current_mid + impact
90
-
91
- return {
92
- "projected": projected_price,
93
- "net_score": weighted_imbalance
94
- }
95
 
96
  def process_market_data():
97
- if not market_state['ready']:
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
 
@@ -113,39 +100,36 @@ def process_market_data():
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):
@@ -162,790 +146,337 @@ def process_market_data():
162
  "depth_bids": chart_bids,
163
  "depth_asks": chart_asks,
164
  "analysis": analysis,
165
- "walls": {
166
- "bids": bid_walls,
167
- "asks": ask_walls
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>
941
  """
942
 
 
943
  async def kraken_worker():
944
  global market_state
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,17 +491,13 @@ async def kraken_worker():
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,7 +505,7 @@ async def kraken_worker():
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,7 +513,7 @@ async def kraken_worker():
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,21 +522,16 @@ 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):
1005
  ws = web.WebSocketResponse()
1006
  await ws.prepare(request)
1007
  connected_clients.add(ws)
1008
- try:
1009
- async for msg in ws:
1010
- pass
1011
- finally:
1012
- connected_clients.remove(ws)
1013
  return ws
1014
 
1015
  async def handle_index(request):
@@ -1022,11 +544,8 @@ async def start_background(app):
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,11 +557,9 @@ async def main():
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
 
8
  from aiohttp import web
9
  import websockets
10
 
11
+ # --- CONFIGURATION ---
12
  SYMBOL_KRAKEN = "BTC/USD"
13
  PORT = 7860
14
+ HISTORY_LENGTH = 300
15
  BROADCAST_RATE = 0.1
16
+ DECAY_LAMBDA = 100.0
17
+ IMPACT_SENSITIVITY = 0.5
18
+
19
+ # Wall Detection Parameters
20
+ Z_SCORE_THRESHOLD = 3.0
21
+ WALL_LOOKBACK = 200
22
 
23
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
24
 
25
+ # --- GLOBAL STATE ---
26
  market_state = {
27
  "bids": {},
28
  "asks": {},
29
+ "history": [],
30
+ "pred_history": [],
31
  "current_mid": 0.0,
32
  "prev_mid": 0.0,
33
  "ready": False
 
35
 
36
  connected_clients = set()
37
 
38
+ # --- BACKEND LOGIC ---
39
  def detect_anomalies(orders, scan_depth):
40
+ if len(orders) < 10: return []
 
 
41
  relevant_orders = orders[:scan_depth]
42
  volumes = [q for p, q in relevant_orders]
43
+ if not volumes: return []
 
 
44
 
45
  try:
46
  avg_vol = statistics.mean(volumes)
 
48
  except statistics.StatisticsError:
49
  return []
50
 
51
+ if stdev_vol == 0: return []
 
52
 
53
  walls = []
 
54
  for price, qty in relevant_orders:
55
  z_score = (qty - avg_vol) / stdev_vol
56
  if z_score > Z_SCORE_THRESHOLD:
57
+ walls.append({"price": price, "vol": qty, "z_score": z_score})
 
 
 
 
58
 
59
  walls.sort(key=lambda x: x['z_score'], reverse=True)
60
  return walls[:3]
61
 
62
  def analyze_structure(diff_x, diff_y, current_mid):
63
+ if not diff_y or len(diff_y) < 5: return None
 
64
 
65
  weighted_imbalance = 0.0
66
  prev_vol = 0.0
67
+
68
  for i in range(len(diff_x)):
69
  dist = diff_x[i]
70
  cum_vol = diff_y[i]
 
75
 
76
  if weighted_imbalance != 0:
77
  impact = math.sqrt(abs(weighted_imbalance)) * IMPACT_SENSITIVITY
78
+ if weighted_imbalance < 0: impact = -impact
 
79
  else:
80
  impact = 0.0
81
 
82
  projected_price = current_mid + impact
83
+ return {"projected": projected_price, "net_score": weighted_imbalance}
 
 
 
 
84
 
85
  def process_market_data():
86
+ if not market_state['ready']: return {"error": "Initializing..."}
 
87
 
88
  mid = market_state['current_mid']
 
89
  sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
90
  sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
91
 
 
100
  d = mid - p
101
  if d >= 0:
102
  cum += q
103
+ d_b_x.append(d); d_b_y.append(cum)
 
104
 
105
  d_a_x, d_a_y, cum = [], [], 0
106
  for p, q in raw_asks:
107
  d = p - mid
108
  if d >= 0:
109
  cum += q
110
+ d_a_x.append(d); d_a_y.append(cum)
 
111
 
112
  diff_x, diff_y = [], []
113
  chart_bids, chart_asks = [], []
114
+
115
  if d_b_x and d_a_x:
116
  max_dist = min(d_b_x[-1], d_a_x[-1])
117
  step_size = max_dist / 100
118
  steps = [i * step_size for i in range(1, 101)]
119
+
120
  for s in steps:
121
  idx_b = bisect.bisect_right(d_b_x, s)
122
+ vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
 
123
  idx_a = bisect.bisect_right(d_a_x, s)
124
+ vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
125
+
126
  diff_x.append(s)
127
  diff_y.append(vol_b - vol_a)
128
  chart_bids.append(vol_b)
129
  chart_asks.append(vol_a)
130
 
131
  analysis = analyze_structure(diff_x, diff_y, mid)
132
+
133
  now = time.time()
134
  if analysis:
135
  if not market_state['pred_history'] or (now - market_state['pred_history'][-1]['t'] > 0.5):
 
146
  "depth_bids": chart_bids,
147
  "depth_asks": chart_asks,
148
  "analysis": analysis,
149
+ "walls": {"bids": bid_walls, "asks": ask_walls}
 
 
 
150
  }
151
 
152
+ # --- FRONTEND (UPDATED UI) ---
153
+ HTML_PAGE = f"""
154
  <!DOCTYPE html>
155
  <html lang="en">
156
  <head>
157
  <meta charset="UTF-8">
158
+ <title>Liquidity Radar | {SYMBOL_KRAKEN}</title>
 
159
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
160
+ <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Roboto+Mono:wght@300;400;700&display=swap" rel="stylesheet">
161
  <style>
162
+ :root {{
163
+ --bg-dark: #050505;
164
+ --panel-bg: #0f1116;
165
+ --border: #1e222d;
166
+ --accent-blue: #2962FF;
167
+ --accent-green: #00E676;
168
+ --accent-red: #FF1744;
169
+ --text-main: #E0E0E0;
170
+ --text-dim: #757575;
171
+ --glass: rgba(15, 17, 22, 0.85);
172
+ }}
173
+ body {{
174
+ margin: 0; padding: 0;
175
+ background-color: var(--bg-dark);
176
+ color: var(--text-main);
177
+ font-family: 'Roboto Mono', monospace;
178
+ overflow: hidden;
179
+ height: 100vh; width: 100vw;
180
+ }}
181
+
182
+ /* LAYOUT GRID */
183
+ .layout {{
184
+ display: grid;
185
+ grid-template-rows: 50px 1fr 1fr;
186
+ grid-template-columns: 3fr 1fr;
187
+ gap: 5px;
188
+ height: 100vh;
189
+ padding: 5px;
190
+ box-sizing: border-box;
191
+ }}
192
+
193
+ /* TOP HEADER */
194
+ .header-bar {{
195
+ grid-column: 1 / 3;
196
+ grid-row: 1 / 2;
197
+ background: var(--panel-bg);
198
+ border: 1px solid var(--border);
199
+ border-radius: 4px;
 
 
 
 
200
  display: flex;
201
  align-items: center;
202
+ padding: 0 20px;
203
  justify-content: space-between;
204
+ }}
205
+ .brand {{ font-family: 'Orbitron', sans-serif; font-weight: 700; color: var(--accent-blue); font-size: 18px; letter-spacing: 1px; }}
206
+ .live-price-box {{ display: flex; gap: 10px; align-items: baseline; }}
207
+ #main-price {{ font-size: 20px; font-weight: bold; color: #fff; }}
208
+
209
+ /* PANELS */
210
+ .panel {{ background: var(--panel-bg); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; position: relative; display: flex; flex-direction: column; }}
211
+
212
+ #p-chart {{ grid-column: 1 / 2; grid-row: 2 / 3; }}
213
+ #p-depth {{ grid-column: 1 / 2; grid-row: 3 / 4; display: flex; flex-direction: row; gap: 5px; border: none; background: transparent; }}
214
+
215
+ .sub-chart {{ flex: 1; background: var(--panel-bg); border: 1px solid var(--border); display: flex; flex-direction: column; }}
216
+
217
+ /* STATS SIDEBAR */
218
+ #p-sidebar {{
219
+ grid-column: 2 / 3;
220
+ grid-row: 2 / 4;
221
+ padding: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  display: flex;
223
  flex-direction: column;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  gap: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  overflow-y: auto;
226
+ }}
227
+
228
+ .panel-header {{
229
+ padding: 8px 12px;
230
+ background: rgba(255,255,255,0.02);
231
+ border-bottom: 1px solid var(--border);
232
+ font-size: 10px;
233
+ font-weight: 700;
234
+ color: var(--text-dim);
235
+ text-transform: uppercase;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  letter-spacing: 0.5px;
237
+ }}
238
+
239
+ /* CARD STYLES */
240
+ .card {{
241
+ background: rgba(255,255,255,0.03);
242
+ border: 1px solid var(--border);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  border-radius: 4px;
244
+ padding: 15px;
245
+ }}
246
+ .card-title {{ font-size: 10px; color: var(--text-dim); margin-bottom: 5px; display: block; }}
247
+ .card-value {{ font-size: 24px; font-weight: 700; display: block; }}
248
+ .card-sub {{ font-size: 12px; margin-left: 5px; font-weight: 400; }}
249
+
250
+ /* TEXT COLORS */
251
+ .green {{ color: var(--accent-green); }}
252
+ .red {{ color: var(--accent-red); }}
253
+ .blue {{ color: var(--accent-blue); }}
254
+ .dim {{ color: var(--text-dim); }}
255
+
256
+ /* WALL LIST */
257
+ .wall-item {{
258
+ display: flex; justify-content: space-between;
259
+ padding: 8px 5px; border-bottom: 1px solid #1a1d26; font-size: 11px;
260
+ }}
261
+ .wall-item:last-child {{ border: none; }}
262
+
263
+ /* LOADER */
264
+ #loader {{ position: fixed; top:0; left:0; width:100%; height:100%; background: #000; z-index: 999; display: flex; flex-direction: column; justify-content: center; align-items: center; color: var(--accent-blue); }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  </style>
266
  </head>
267
  <body>
268
+
269
  <div id="loader">
270
+ <div style="font-family: 'Orbitron'; font-size: 24px;">SYSTEM INITIALIZING</div>
271
+ <div id="loading-status" style="margin-top: 10px; font-size: 12px; color: #666;">Connecting to Feed...</div>
 
272
  </div>
273
 
274
+ <div class="layout">
275
+ <div class="header-bar">
276
+ <div class="brand">LIQUIDITY RADAR <span style="font-size: 12px; color: #666;">// {SYMBOL_KRAKEN}</span></div>
277
+ <div class="live-price-box">
278
+ <span class="dim">CURRENT MID:</span>
279
+ <span id="main-price">---</span>
 
 
280
  </div>
281
+ </div>
282
 
283
+ <!-- MAIN PRICE CHART -->
284
+ <div id="p-chart" class="panel">
285
+ <div class="panel-header">Real-time Price Action & Anomaly Detection</div>
286
+ <div id="tv-price" style="flex: 1; width: 100%;"></div>
287
+ </div>
 
 
 
 
 
288
 
289
+ <!-- DEPTH CHARTS (SPLIT) -->
290
+ <div id="p-depth">
291
+ <div class="sub-chart">
292
+ <div class="panel-header">Liquidity Density (Raw)</div>
293
+ <div id="tv-raw" style="flex: 1; width: 100%;"></div>
294
  </div>
295
+ <div class="sub-chart">
296
+ <div class="panel-header">Order Imbalance (Net)</div>
297
+ <div id="tv-net" style="flex: 1; width: 100%;"></div>
 
 
 
 
 
 
 
 
298
  </div>
299
+ </div>
300
 
301
+ <!-- SIDEBAR ANALYTICS -->
302
+ <div id="p-sidebar" class="panel">
303
+
304
+ <!-- IMPACT PROJECTION CARD -->
305
+ <div class="card" style="border-left: 3px solid var(--accent-blue);">
306
+ <span class="card-title">IMPACT PROJECTION (5s)</span>
307
+ <div style="display: flex; align-items: baseline;">
308
+ <span id="proj-val" class="card-value">---</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  </div>
310
+ <!-- PERCENTAGE CHANGE INDICATOR -->
311
+ <div style="margin-top: 5px;">
312
+ <span class="card-title" style="display:inline;">DELTA: </span>
313
+ <span id="proj-pct" style="font-weight: bold;">--%</span>
 
 
 
 
 
314
  </div>
315
+ </div>
316
+
317
+ <!-- IMBALANCE SCORE CARD -->
318
+ <div class="card">
319
+ <span class="card-title">WEIGHTED IMBALANCE</span>
320
+ <span id="score-val" class="card-value">0.00</span>
321
+ <span style="font-size: 10px; color: #555;">Exp Decay λ={DECAY_LAMBDA}</span>
322
+ </div>
323
 
324
+ <!-- WALL DETECTION -->
325
+ <div class="card" style="flex: 1; display: flex; flex-direction: column;">
326
+ <span class="card-title">DETECTED LIQUIDITY WALLS (Z > 3.0)</span>
327
+ <div id="wall-list" style="margin-top: 10px; overflow-y: auto;">
328
+ <div class="dim">Scanning depth...</div>
 
 
 
329
  </div>
330
  </div>
331
  </div>
332
  </div>
333
 
334
  <script>
335
+ document.addEventListener('DOMContentLoaded', () => {{
336
+ const dom = {{
337
  loader: document.getElementById('loader'),
338
  status: document.getElementById('loading-status'),
339
+ mainPrice: document.getElementById('main-price'),
 
 
340
  scoreVal: document.getElementById('score-val'),
341
  projVal: document.getElementById('proj-val'),
342
+ projPct: document.getElementById('proj-pct'),
 
343
  wallList: document.getElementById('wall-list')
344
+ }};
345
+
346
+ // CHART CONFIGURATION
347
+ const chartCommon = {{
348
+ layout: {{ background: {{ type: 'solid', color: '#0f1116' }}, textColor: '#757575', fontFamily: 'Roboto Mono' }},
349
+ grid: {{ vertLines: {{ color: '#1e222d' }}, horzLines: {{ color: '#1e222d' }} }},
350
+ rightPriceScale: {{ borderColor: '#1e222d', scaleMargins: {{ top: 0.1, bottom: 0.1 }} }},
351
+ timeScale: {{ borderColor: '#1e222d', timeVisible: true, secondsVisible: true }},
352
+ crosshair: {{ mode: 1, vertLine: {{ color: '#2962FF', labelBackgroundColor: '#2962FF' }}, horzLine: {{ color: '#2962FF', labelBackgroundColor: '#2962FF' }} }}
353
+ }};
354
+
355
+ // 1. PRICE CHART
356
  const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartCommon);
357
+ const priceSeries = priceChart.addLineSeries({{ color: '#E0E0E0', lineWidth: 2, title: 'Price' }});
358
+ const predSeries = priceChart.addLineSeries({{ color: '#2962FF', lineWidth: 2, lineStyle: 2, title: 'Projected' }});
 
359
 
360
+ // 2. RAW DEPTH CHART
361
+ const rawChart = LightweightCharts.createChart(document.getElementById('tv-raw'), {{
362
  ...chartCommon,
363
+ timeScale: {{ tickMarkFormatter: t => t.toFixed(0) }},
364
+ localization: {{ timeFormatter: t => 'Dist: $' + t.toFixed(2) }}
365
+ }});
366
+ const bidSeries = rawChart.addAreaSeries({{ lineColor: '#00E676', topColor: 'rgba(0, 230, 118, 0.2)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
367
+ const askSeries = rawChart.addAreaSeries({{ lineColor: '#FF1744', topColor: 'rgba(255, 23, 68, 0.2)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
368
+
369
+ // 3. NET CHART
370
+ const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{
371
  ...chartCommon,
372
+ timeScale: {{ tickMarkFormatter: t => t.toFixed(0) }},
373
+ localization: {{ timeFormatter: t => 'Dist: $' + t.toFixed(2) }}
374
+ }});
375
+ const netSeries = netChart.addHistogramSeries({{ color: '#2962FF' }});
376
 
377
  let activePriceLines = [];
378
 
379
+ // RESIZE HANDLER
380
+ new ResizeObserver(entries => {{
381
+ entries.forEach(e => {{
382
+ if(e.target.id === 'tv-price') priceChart.applyOptions({{ width: e.contentRect.width, height: e.contentRect.height }});
383
+ if(e.target.id === 'tv-raw') rawChart.applyOptions({{ width: e.contentRect.width, height: e.contentRect.height }});
384
+ if(e.target.id === 'tv-net') netChart.applyOptions({{ width: e.contentRect.width, height: e.contentRect.height }});
385
+ }});
386
+ }}).observe(document.body);
387
+
388
+ function connect() {{
389
+ const ws = new WebSocket((window.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host + '/ws');
390
+
391
+ ws.onopen = () => dom.status.innerText = "Stream Connected. Buffering...";
392
+ ws.onclose = () => {{ dom.loader.style.display = 'flex'; dom.status.innerText = "Reconnecting..."; setTimeout(connect, 3000); }};
393
+
394
+ ws.onmessage = (e) => {{
395
+ const data = JSON.parse(e.data);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  if (data.error) return;
397
  dom.loader.style.display = 'none';
398
 
399
+ // UPDATE CHARTS
400
+ if (data.history.length) {{
401
+ const hist = data.history.map(d => ({{ time: Math.floor(d.t), value: d.p }}));
402
+ const uniqueHist = [...new Map(hist.map(item => [item.time, item])).values()];
403
+ priceSeries.setData(uniqueHist);
404
+
405
+ const lastPrice = uniqueHist[uniqueHist.length-1].value;
406
+ dom.mainPrice.innerText = lastPrice.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
407
+
408
+ // PREDICTION & PERCENTAGE LOGIC
409
+ if (data.analysis) {{
410
+ const proj = data.analysis.projected;
411
+ const score = data.analysis.net_score;
412
+
413
+ // 1. Draw projected line
414
+ predSeries.setData([
415
+ uniqueHist[uniqueHist.length-1],
416
+ {{ time: uniqueHist[uniqueHist.length-1].time + 60, value: proj }}
 
 
 
 
 
 
 
 
 
 
 
417
  ]);
418
 
419
+ // 2. Update UI Numbers
420
+ dom.projVal.innerText = proj.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
421
+ dom.projVal.className = proj > lastPrice ? "card-value green" : "card-value red";
422
+
423
+ dom.scoreVal.innerText = score.toFixed(2);
424
+ dom.scoreVal.style.color = score > 0 ? "var(--accent-green)" : "var(--accent-red)";
425
+
426
+ // 3. CALCULATE PERCENTAGE CHANGE
427
+ const pctChange = ((proj - lastPrice) / lastPrice) * 100;
428
+ const sign = pctChange >= 0 ? "+" : "";
429
+ dom.projPct.innerText = `${{sign}}${{pctChange.toFixed(3)}}%`;
430
+ dom.projPct.style.color = pctChange >= 0 ? "var(--accent-green)" : "var(--accent-red)";
431
+ }}
432
+ }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
 
434
+ // WALLS
435
+ if (data.walls) {{
436
+ activePriceLines.forEach(l => priceSeries.removePriceLine(l));
437
+ activePriceLines = [];
438
+ let html = "";
439
+
440
+ data.walls.bids.forEach(w => {{
441
+ activePriceLines.push(priceSeries.createPriceLine({{ price: w.price, color: '#00E676', lineWidth: 1, lineStyle: 2, axisLabelVisible: true, title: 'BUY WALL' }}));
442
+ html += `<div class="wall-item"><span class="green">BID ${{w.price}}</span><span class="dim">Vol: ${{w.vol.toFixed(1)}} (Z:${{w.z_score.toFixed(1)}})</span></div>`;
443
+ }});
444
+
445
+ data.walls.asks.forEach(w => {{
446
+ activePriceLines.push(priceSeries.createPriceLine({{ price: w.price, color: '#FF1744', lineWidth: 1, lineStyle: 2, axisLabelVisible: true, title: 'SELL WALL' }}));
447
+ html += `<div class="wall-item"><span class="red">ASK ${{w.price}}</span><span class="dim">Vol: ${{w.vol.toFixed(1)}} (Z:${{w.z_score.toFixed(1)}})</span></div>`;
448
+ }});
449
+ dom.wallList.innerHTML = html || '<div style="padding:10px; font-size:10px; color:#555;">No significant anomalies detected.</div>';
450
+ }}
451
+
452
+ // DEPTH
453
+ if (data.depth_x.length) {{
454
+ const bids = [], asks = [], nets = [];
455
+ for(let i=0; i<data.depth_x.length; i++) {{
456
+ bids.push({{ time: data.depth_x[i], value: data.depth_bids[i] }});
457
+ asks.push({{ time: data.depth_x[i], value: data.depth_asks[i] }});
458
+ nets.push({{ time: data.depth_x[i], value: data.depth_net[i], color: data.depth_net[i] > 0 ? '#00E676' : '#FF1744' }});
459
+ }}
460
+ bidSeries.setData(bids);
461
+ askSeries.setData(asks);
462
+ netSeries.setData(nets);
463
+ }}
464
+ }};
465
+ }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  connect();
467
+ }});
468
  </script>
469
  </body>
470
  </html>
471
  """
472
 
473
+ # --- SERVER BOILERPLATE ---
474
  async def kraken_worker():
475
  global market_state
476
  while True:
477
  try:
478
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
479
+ logging.info(f"🔌 Connected to Kraken ({SYMBOL_KRAKEN})")
480
  await ws.send(json.dumps({
481
  "method": "subscribe",
482
  "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}
 
491
  for item in data:
492
  for bid in item.get('bids', []):
493
  q, p = float(bid['qty']), float(bid['price'])
494
+ if q == 0: market_state['bids'].pop(p, None)
495
+ else: market_state['bids'][p] = q
 
 
496
  for ask in item.get('asks', []):
497
  q, p = float(ask['qty']), float(ask['price'])
498
+ if q == 0: market_state['asks'].pop(p, None)
499
+ else: market_state['asks'][p] = q
500
+
 
 
501
  if market_state['bids'] and market_state['asks']:
502
  best_bid = max(market_state['bids'].keys())
503
  best_ask = min(market_state['asks'].keys())
 
505
  market_state['prev_mid'] = market_state['current_mid']
506
  market_state['current_mid'] = mid
507
  market_state['ready'] = True
508
+
509
  now = time.time()
510
  if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
511
  market_state['history'].append({'t': now, 'p': mid})
 
513
  market_state['history'].pop(0)
514
 
515
  except Exception as e:
516
+ logging.warning(f"⚠️ Reconnecting: {e}")
517
  await asyncio.sleep(3)
518
 
519
  async def broadcast_worker():
 
522
  payload = process_market_data()
523
  msg = json.dumps(payload)
524
  for ws in list(connected_clients):
525
+ try: await ws.send_str(msg)
526
+ except: pass
 
 
527
  await asyncio.sleep(BROADCAST_RATE)
528
 
529
  async def websocket_handler(request):
530
  ws = web.WebSocketResponse()
531
  await ws.prepare(request)
532
  connected_clients.add(ws)
533
+ try: async for msg in ws: pass
534
+ finally: connected_clients.remove(ws)
 
 
 
535
  return ws
536
 
537
  async def handle_index(request):
 
544
  async def cleanup_background(app):
545
  app['kraken_task'].cancel()
546
  app['broadcast_task'].cancel()
547
+ try: await app['kraken_task']; await app['broadcast_task']
548
+ except: pass
 
 
 
549
 
550
  async def main():
551
  app = web.Application()
 
557
  await runner.setup()
558
  site = web.TCPSite(runner, '0.0.0.0', PORT)
559
  await site.start()
560
+ print(f"🚀 AI Liquidity Dashboard: http://localhost:{PORT}")
561
  await asyncio.Event().wait()
562
 
563
  if __name__ == "__main__":
564
+ try: asyncio.run(main())
565
+ except KeyboardInterrupt: pass