File size: 27,588 Bytes
3469441
 
 
 
c43af34
a0c3495
3a80860
8b9c571
30fe49c
a5396a6
3469441
df9cdf6
8b9c571
a5396a6
df9cdf6
58b7790
df9cdf6
c2b455a
f95b3cf
 
 
df9cdf6
 
3a80860
a5396a6
 
df9cdf6
a5396a6
 
 
df9cdf6
 
f95b3cf
 
c955262
2324f1b
a5396a6
 
669adc5
 
c2b455a
 
3a80860
df9cdf6
3a80860
 
df9cdf6
3a80860
 
 
 
 
 
 
df9cdf6
3a80860
 
 
 
 
df9cdf6
3a80860
 
 
 
f95b3cf
c2b455a
 
f95b3cf
bfed924
f95b3cf
df9cdf6
a0c3495
 
f95b3cf
bfed924
c2b455a
f95b3cf
c2b455a
f95b3cf
c2b455a
f95b3cf
c2b455a
 
 
bfed924
f95b3cf
c2b455a
 
f95b3cf
c2b455a
 
 
 
f95b3cf
c2b455a
 
 
 
 
f95b3cf
 
c2b455a
74c1714
669adc5
df9cdf6
669adc5
 
c2b455a
f95b3cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a80860
 
c2b455a
 
3a80860
c2b455a
 
3a80860
 
 
669adc5
 
c2b455a
669adc5
 
 
df9cdf6
669adc5
 
c2b455a
669adc5
 
 
df9cdf6
669adc5
c2b455a
58b7790
df9cdf6
669adc5
 
 
 
df9cdf6
669adc5
 
df9cdf6
669adc5
df9cdf6
 
669adc5
f95b3cf
58b7790
 
669adc5
c2b455a
f95b3cf
c2b455a
 
df9cdf6
bbe99a8
bfed924
 
 
 
669adc5
 
 
1530d0a
bfed924
f95b3cf
1530d0a
c2b455a
1530d0a
 
3a80860
df9cdf6
669adc5
 
c2b455a
df9cdf6
3469441
deb82b2
3469441
deb82b2
6eaed7c
7252530
6eaed7c
3469441
df9cdf6
6eaed7c
8b9c571
6eaed7c
 
 
 
 
 
f95b3cf
df9cdf6
 
 
8b9c571
df9cdf6
8b9c571
df9cdf6
 
 
 
 
 
c2b455a
df9cdf6
6eaed7c
 
df9cdf6
 
 
 
6eaed7c
8b9c571
 
df9cdf6
 
8b9c571
d6a6045
 
 
6eaed7c
8b9c571
6eaed7c
8b9c571
6eaed7c
 
df9cdf6
6eaed7c
 
 
 
df9cdf6
 
 
8b9c571
 
 
 
 
 
 
6eaed7c
df9cdf6
 
 
 
8b9c571
d6a6045
 
f95b3cf
6eaed7c
f95b3cf
6eaed7c
 
 
 
 
 
 
 
 
 
 
c2b455a
6eaed7c
 
df9cdf6
 
8b9c571
6eaed7c
 
 
 
8b9c571
 
 
 
 
 
f95b3cf
8b9c571
 
 
 
6eaed7c
 
df9cdf6
6eaed7c
 
f95b3cf
 
 
 
 
 
 
 
3469441
 
 
df9cdf6
 
8b9c571
 
 
6eaed7c
 
d6a6045
6eaed7c
df9cdf6
30fe49c
df9cdf6
f95b3cf
6eaed7c
df9cdf6
d6a6045
df9cdf6
8b9c571
f95b3cf
6eaed7c
d6a6045
8b9c571
f95b3cf
6eaed7c
1530d0a
df9cdf6
30fe49c
df9cdf6
 
8b9c571
f95b3cf
6eaed7c
8b9c571
6eaed7c
37ef4ee
df9cdf6
 
8b9c571
 
 
f95b3cf
8b9c571
df9cdf6
d6a6045
8b9c571
 
f95b3cf
 
8b9c571
f95b3cf
3a80860
66a96ba
f95b3cf
 
 
 
 
8b9c571
d702b3f
c43af34
3469441
 
8b9c571
 
 
 
 
df9cdf6
 
8b9c571
 
3a80860
df9cdf6
3a80860
df9cdf6
 
8b9c571
6eaed7c
 
 
 
 
df9cdf6
 
f95b3cf
8b9c571
f95b3cf
 
d6a6045
f95b3cf
df9cdf6
f95b3cf
df9cdf6
6eaed7c
 
df9cdf6
 
f95b3cf
df9cdf6
6eaed7c
d6a6045
f95b3cf
 
 
 
 
 
 
 
 
 
 
 
8b9c571
3a80860
f95b3cf
6eaed7c
 
 
 
 
 
f95b3cf
6eaed7c
c2b455a
6eaed7c
f95b3cf
6eaed7c
f95b3cf
 
 
 
 
6eaed7c
 
df9cdf6
 
8b9c571
df9cdf6
 
 
30fe49c
b788fc7
df9cdf6
 
8b9c571
 
 
 
 
df9cdf6
 
 
c2b455a
8b9c571
df9cdf6
8b9c571
 
bfed924
d6a6045
8b9c571
 
 
 
 
 
df9cdf6
8b9c571
c2b455a
 
df9cdf6
 
d6a6045
f95b3cf
df9cdf6
8b9c571
 
df9cdf6
 
8b9c571
6eaed7c
8b9c571
 
 
6eaed7c
8b9c571
 
 
 
 
f95b3cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
df9cdf6
f95b3cf
 
df9cdf6
 
f95b3cf
df9cdf6
 
 
8b9c571
 
 
6eaed7c
df9cdf6
 
 
 
 
 
8b9c571
 
df9cdf6
30fe49c
df9cdf6
3469441
 
 
 
 
c2b455a
a5396a6
 
3469441
 
a6d8311
df9cdf6
f95b3cf
 
a6d8311
 
 
 
f95b3cf
 
 
 
a5396a6
 
a6d8311
 
30fe49c
a6d8311
 
30fe49c
a6d8311
 
df9cdf6
 
a6d8311
 
df9cdf6
 
 
c955262
 
 
 
deb82b2
c955262
 
df9cdf6
c955262
6dbbeb8
c955262
 
 
f95b3cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3469441
 
df9cdf6
a6d8311
3469441
669adc5
 
 
 
30fe49c
669adc5
df9cdf6
 
669adc5
 
 
 
 
 
6a86634
c2b455a
6a86634
 
669adc5
c43af34
669adc5
 
3469441
75f3b2f
 
669adc5
75f3b2f
 
 
669adc5
df9cdf6
 
75f3b2f
3469441
 
a5396a6
30fe49c
75f3b2f
 
3469441
 
a5396a6
3469441
c2b455a
3469441
 
a5396a6
df9cdf6
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
import asyncio
import json
import logging
import time
import bisect
import math
import statistics
from datetime import datetime
from aiohttp import web
import websockets

# --- CONFIGURATION ---
SYMBOL_KRAKEN = "BTC/USD"
PORT = 7860
HISTORY_LENGTH = 300 
BROADCAST_RATE = 0.1

# Mathematical Constants
DECAY_LAMBDA = 50.0      
IMPACT_SENSITIVITY = 2.0 
WALL_DAMPENING = 0.8     
Z_SCORE_THRESHOLD = 3.0  
WALL_LOOKBACK = 200      

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')

# --- GLOBAL STATE ---
market_state = {
    "bids": {},
    "asks": {},
    "history": [], 
    "pred_history": [], 
    "trade_vol_history": [], # New: Store trade volume history
    "current_vol_window": {"buy": 0.0, "sell": 0.0, "start": time.time()},
    "current_mid": 0.0,
    "ready": False
}

connected_clients = set()

# --- QUANTITATIVE METHODS ---

def detect_anomalies(orders, scan_depth):
    if len(orders) < 10: return []
    relevant_orders = orders[:scan_depth]
    volumes = [q for p, q in relevant_orders]
    if not volumes: return []

    try:
        avg_vol = statistics.mean(volumes)
        stdev_vol = statistics.stdev(volumes)
    except statistics.StatisticsError:
        return []

    if stdev_vol == 0: return []

    walls = []
    for price, qty in relevant_orders:
        z_score = (qty - avg_vol) / stdev_vol
        if z_score > Z_SCORE_THRESHOLD:
            walls.append({"price": price, "vol": qty, "z_score": z_score})

    walls.sort(key=lambda x: x['z_score'], reverse=True)
    return walls[:3]

def calculate_micro_price_structure(diff_x, diff_y_net, current_mid, best_bid, best_ask, walls):
    if not diff_x or len(diff_x) < 5: return None

    # Weighted Imbalance Calculation
    weighted_imbalance = 0.0
    total_weight = 0.0
    
    for i in range(len(diff_x)):
        dist = diff_x[i]
        net_vol = diff_y_net[i]
        weight = math.exp(-dist / DECAY_LAMBDA)
        weighted_imbalance += net_vol * weight
        total_weight += weight

    rho = weighted_imbalance / total_weight if total_weight > 0 else 0

    # Base Projection
    spread = best_ask - best_bid
    theoretical_delta = (spread / 2) * rho * IMPACT_SENSITIVITY
    projected_price = current_mid + theoretical_delta

    # Wall Friction
    final_delta = theoretical_delta
    if final_delta > 0 and walls['asks']:
        nearest_wall = walls['asks'][0]
        if projected_price >= nearest_wall['price']:
            damp_factor = 1.0 / (1.0 + (nearest_wall['z_score'] * 0.2))
            final_delta *= damp_factor
    elif final_delta < 0 and walls['bids']:
        nearest_wall = walls['bids'][0]
        if projected_price <= nearest_wall['price']:
            damp_factor = 1.0 / (1.0 + (nearest_wall['z_score'] * 0.2))
            final_delta *= damp_factor

    return {
        "projected": current_mid + final_delta,
        "rho": rho
    }

def process_market_data():
    if not market_state['ready']: return {"error": "Initializing..."}

    mid = market_state['current_mid']
    
    # Process Trade Volume Window (Reset every 1 second)
    now = time.time()
    if now - market_state['current_vol_window']['start'] >= 1.0:
        market_state['trade_vol_history'].append({
            't': now,
            'buy': market_state['current_vol_window']['buy'],
            'sell': market_state['current_vol_window']['sell']
        })
        if len(market_state['trade_vol_history']) > 60: # Keep last 60 seconds
            market_state['trade_vol_history'].pop(0)
        
        # Reset window
        market_state['current_vol_window'] = {"buy": 0.0, "sell": 0.0, "start": now}

    # Order Book Processing
    sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
    sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
    
    if not sorted_bids or not sorted_asks: return {"error": "Empty Book"}

    best_bid = sorted_bids[0][0]
    best_ask = sorted_asks[0][0]

    bid_walls = detect_anomalies(sorted_bids, WALL_LOOKBACK)
    ask_walls = detect_anomalies(sorted_asks, WALL_LOOKBACK)

    d_b_x, d_b_y, cum = [], [], 0
    for p, q in sorted_bids[:300]:
        d = mid - p
        if d >= 0:
            cum += q
            d_b_x.append(d); d_b_y.append(cum)

    d_a_x, d_a_y, cum = [], [], 0
    for p, q in sorted_asks[:300]:
        d = p - mid
        if d >= 0:
            cum += q
            d_a_x.append(d); d_a_y.append(cum)

    diff_x, diff_y_net = [], []
    chart_bids, chart_asks = [], []
    
    if d_b_x and d_a_x:
        max_dist = min(d_b_x[-1], d_a_x[-1])
        step_size = max_dist / 100
        steps = [i * step_size for i in range(1, 101)]
        
        for s in steps:
            idx_b = bisect.bisect_right(d_b_x, s)
            vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
            idx_a = bisect.bisect_right(d_a_x, s)
            vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
            
            diff_x.append(s)
            diff_y_net.append(vol_b - vol_a)
            chart_bids.append(vol_b)
            chart_asks.append(vol_a)

    analysis = calculate_micro_price_structure(
        diff_x, diff_y_net, mid, best_bid, best_ask, 
        {"bids": bid_walls, "asks": ask_walls}
    )
    
    if analysis:
        if not market_state['pred_history'] or (now - market_state['pred_history'][-1]['t'] > 0.5):
            market_state['pred_history'].append({'t': now, 'p': analysis['projected']})
            if len(market_state['pred_history']) > HISTORY_LENGTH:
                market_state['pred_history'].pop(0)

    return {
        "mid": mid,
        "history": market_state['history'],
        "pred_history": market_state['pred_history'],
        "trade_history": market_state['trade_vol_history'],
        "depth_x": diff_x,
        "depth_net": diff_y_net,
        "depth_bids": chart_bids,
        "depth_asks": chart_asks,
        "analysis": analysis,
        "walls": {"bids": bid_walls, "asks": ask_walls}
    }

# --- FRONTEND ---
HTML_PAGE = f"""
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{SYMBOL_KRAKEN}</title>
    <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
    <style>
        :root {{
            --bg-base: #000000;
            --bg-panel: #0a0a0a;
            --border: #252525;
            --text-main: #FFFFFF;
            --text-dim: #999999;
            --green: #00ff9d;
            --red: #ff3b3b;
            --blue: #2979ff;
            --yellow: #ffeb3b; 
        }}
        body {{ 
            margin: 0; padding: 0; 
            background-color: var(--bg-base); 
            color: var(--text-main); 
            font-family: 'Inter', sans-serif; 
            overflow: hidden; 
            height: 100vh; width: 100vw;
        }}
        
        .layout {{ 
            display: grid; 
            grid-template-rows: 34px 1fr 1fr; 
            grid-template-columns: 3fr 1fr; 
            gap: 1px;
            background-color: var(--border); 
            height: 100vh; 
            box-sizing: border-box;
        }}

        .panel {{ background: var(--bg-panel); display: flex; flex-direction: column; overflow: hidden; }}
        
        .status-bar {{
            grid-column: 1 / 3;
            grid-row: 1 / 2;
            background: var(--bg-panel);
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 0 12px;
            font-family: 'JetBrains Mono', monospace;
            font-size: 12px;
            text-transform: uppercase;
            border-bottom: 1px solid var(--border);
            z-index: 50;
        }}
        .status-left {{ display: flex; gap: 20px; align-items: center; }}
        .status-right {{ color: var(--text-dim); letter-spacing: 1px; }}
        .live-dot {{ width: 8px; height: 8px; background-color: var(--green); border-radius: 50%; display: inline-block; box-shadow: 0 0 8px var(--green); }}
        .ticker-val {{ font-weight: 700; color: #fff; font-size: 13px; }}

        #p-chart {{ grid-column: 1 / 2; grid-row: 2 / 3; }}
        
        #p-depth {{ 
            grid-column: 1 / 2; grid-row: 3 / 4; 
            display: grid; 
            grid-template-columns: 1fr 1fr; 
            gap: 1px; 
            background: var(--border); 
        }}
        .depth-sub {{ background: var(--bg-panel); display: flex; flex-direction: column; position: relative; }}

        #p-sidebar {{ 
            grid-column: 2 / 3; 
            grid-row: 2 / 4; 
            padding: 20px;
            display: flex;
            flex-direction: column;
            gap: 15px; /* Tighter gap to fit the new chart */
            border-left: 1px solid var(--border);
            overflow-y: hidden;
        }}

        .chart-header {{
            height: 24px;
            min-height: 24px;
            display: flex;
            align-items: center;
            padding-left: 12px;
            font-size: 10px;
            font-weight: 700;
            color: var(--text-dim);
            background: #050505; 
            border-bottom: 1px solid #151515;
            letter-spacing: 0.5px;
        }}

        .data-group {{ display: flex; flex-direction: column; gap: 4px; }}
        .label {{ font-size: 10px; color: var(--text-dim); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }}
        .value {{ font-family: 'JetBrains Mono', monospace; font-size: 20px; font-weight: 700; color: #fff; }}
        .value-lg {{ font-size: 26px; }}
        .value-sub {{ font-family: 'JetBrains Mono', monospace; font-size: 11px; margin-top: 2px; color: #666; }}

        .divider {{ height: 1px; background: var(--border); width: 100%; }}
        .c-green {{ color: var(--green); }}
        .c-red {{ color: var(--red); }}
        .c-dim {{ color: var(--text-dim); }}

        .list-container {{ display: flex; flex-direction: column; gap: 8px; overflow-y: auto; max-height: 120px; }}
        .list-item {{ 
            display: flex; justify-content: space-between; 
            font-family: 'JetBrains Mono', monospace; 
            font-size: 11px; 
            border-bottom: 1px solid #151515;
            padding-bottom: 4px;
        }}
        .list-item span:first-child {{ color: #e0e0e0; }}
        .list-item:last-child {{ border: none; }}
        
        #sidebar-chart {{
            flex: 1;
            background: rgba(255,255,255,0.02);
            border: 1px solid var(--border);
            border-radius: 4px;
            min-height: 100px;
        }}
    </style>
</head>
<body>

    <div class="layout">
        <div class="status-bar">
            <div class="status-left">
                <span class="live-dot"></span>
                <span style="font-weight:700; color:#fff;">{SYMBOL_KRAKEN}</span>
                <span id="price-ticker" class="ticker-val">---</span>
            </div>
            <div class="status-right" id="clock">00:00:00 UTC</div>
        </div>

        <div id="p-chart" class="panel">
            <div class="chart-header">PRICE ACTION (BLUE) // PREDICTION (YELLOW)</div>
            <div id="tv-price" style="flex: 1; width: 100%;"></div>
        </div>

        <div id="p-depth">
            <div class="depth-sub">
                <div class="chart-header">LIQUIDITY DENSITY</div>
                <div id="tv-raw" style="flex: 1; width: 100%;"></div>
            </div>
            <div class="depth-sub">
                <div class="chart-header">ORDER FLOW IMBALANCE</div>
                <div id="tv-net" style="flex: 1; width: 100%;"></div>
            </div>
        </div>

        <div id="p-sidebar" class="panel">
            
            <div class="data-group">
                <span class="label">Micro-Price Delta</span>
                <div style="display:flex; align-items: baseline; gap: 10px;">
                    <span id="proj-pct" class="value value-lg">--%</span>
                    <span id="proj-val" class="value-sub">---</span>
                </div>
            </div>

            <div class="divider"></div>

            <div class="data-group">
                <span class="label">OFI Imbalance Ratio</span>
                <span id="score-val" class="value">0.00</span>
            </div>

            <div class="divider"></div>

            <div class="data-group">
                <span class="label">Detected Walls (Z > 3.0)</span>
                <div id="wall-list" class="list-container">
                    <span class="c-dim" style="font-size: 11px;">Scanning...</span>
                </div>
            </div>

            <!-- NEW CHART UNDER WALLS -->
            <div class="data-group" style="flex: 1; display:flex; flex-direction:column;">
                <span class="label">Real-time Volume (Ticks)</span>
                <div id="sidebar-chart"></div>
            </div>
        </div>
    </div>

    <script>
        setInterval(() => {{
            const now = new Date();
            document.getElementById('clock').innerText = now.toISOString().split('T')[1].split('.')[0] + ' UTC';
        }}, 1000);

        document.addEventListener('DOMContentLoaded', () => {{
            const dom = {{
                ticker: document.getElementById('price-ticker'),
                score: document.getElementById('score-val'),
                projVal: document.getElementById('proj-val'),
                projPct: document.getElementById('proj-pct'),
                wallList: document.getElementById('wall-list')
            }};

            const chartOpts = {{
                layout: {{ background: {{ type: 'solid', color: '#0a0a0a' }}, textColor: '#888', fontFamily: 'JetBrains Mono' }},
                grid: {{ vertLines: {{ color: '#151515' }}, horzLines: {{ color: '#151515' }} }},
                rightPriceScale: {{ borderColor: '#222', scaleMargins: {{ top: 0.1, bottom: 0.1 }} }},
                timeScale: {{ borderColor: '#222', timeVisible: true, secondsVisible: true }},
                crosshair: {{ mode: 1, vertLine: {{ color: '#444', labelBackgroundColor: '#444' }}, horzLine: {{ color: '#444', labelBackgroundColor: '#444' }} }}
            }};

            // 1. MAIN PRICE CHART
            const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts);
            const priceSeries = priceChart.addLineSeries({{ color: '#2979ff', lineWidth: 2, title: 'Price' }}); // BLUE
            const predSeries = priceChart.addLineSeries({{ color: '#ffeb3b', lineWidth: 2, lineStyle: 2, title: 'Forecast' }}); // YELLOW

            // 2. DEPTH CHARTS
            const rawChart = LightweightCharts.createChart(document.getElementById('tv-raw'), {{
                ...chartOpts, localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
            }});
            const bidSeries = rawChart.addAreaSeries({{ lineColor: '#00ff9d', topColor: 'rgba(0, 255, 157, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
            const askSeries = rawChart.addAreaSeries({{ lineColor: '#ff3b3b', topColor: 'rgba(255, 59, 59, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});

            const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{
                ...chartOpts, localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
            }});
            const netSeries = netChart.addHistogramSeries({{ color: '#2979ff' }});

            // 3. SIDEBAR VOLUME CHART
            const volChart = LightweightCharts.createChart(document.getElementById('sidebar-chart'), {{
                ...chartOpts,
                grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }},
                rightPriceScale: {{ visible: false }},
                timeScale: {{ visible: false }},
                handleScroll: false,
                handleScale: false
            }});
            const volBuySeries = volChart.addHistogramSeries({{ color: '#00ff9d' }});
            const volSellSeries = volChart.addHistogramSeries({{ color: '#ff3b3b' }});

            let activeLines = [];

            // RESIZE OBSERVER
            new ResizeObserver(entries => {{
                for(let entry of entries) {{
                    const {{width, height}} = entry.contentRect;
                    if(entry.target.id === 'tv-price') priceChart.applyOptions({{width, height}});
                    if(entry.target.id === 'tv-raw') rawChart.applyOptions({{width, height}});
                    if(entry.target.id === 'tv-net') netChart.applyOptions({{width, height}});
                    if(entry.target.id === 'sidebar-chart') volChart.applyOptions({{width, height}});
                }}
            }}).observe(document.body);

            ['tv-price', 'tv-raw', 'tv-net', 'sidebar-chart'].forEach(id => {{
                new ResizeObserver(e => {{
                     const t = document.getElementById(id);
                     if(id === 'tv-price') priceChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
                     if(id === 'tv-raw') rawChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
                     if(id === 'tv-net') netChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
                     if(id === 'sidebar-chart') volChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
                }}).observe(document.getElementById(id));
            }});

            function connect() {{
                const ws = new WebSocket((location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/ws');
                
                ws.onmessage = (e) => {{
                    const data = JSON.parse(e.data);
                    if (data.error) return;

                    if (data.history.length) {{
                        const hist = data.history.map(d => ({{ time: Math.floor(d.t), value: d.p }}));
                        const cleanHist = [...new Map(hist.map(i => [i.time, i])).values()];
                        priceSeries.setData(cleanHist);

                        const lastP = cleanHist[cleanHist.length-1].value;
                        dom.ticker.innerText = lastP.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});

                        if (data.analysis) {{
                            const proj = data.analysis.projected;
                            const rho = data.analysis.rho; 

                            predSeries.setData([
                                cleanHist[cleanHist.length-1],
                                {{ time: cleanHist[cleanHist.length-1].time + 60, value: proj }}
                            ]);

                            const pct = ((proj - lastP) / lastP) * 100;
                            const sign = pct >= 0 ? "+" : "";
                            
                            dom.projPct.innerText = `${{sign}}${{pct.toFixed(4)}}%`;
                            dom.projPct.style.color = pct >= 0 ? "var(--green)" : "var(--red)";
                            
                            dom.projVal.innerText = proj.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
                            
                            dom.score.innerText = rho.toFixed(3);
                            dom.score.style.color = rho > 0 ? "var(--green)" : (rho < 0 ? "var(--red)" : "var(--text-main)");
                        }}
                    }}

                    // WALLS
                    if (data.walls) {{
                        activeLines.forEach(l => priceSeries.removePriceLine(l));
                        activeLines = [];
                        let html = "";

                        const addWall = (w, type) => {{
                            const color = type === 'BID' ? '#00ff9d' : '#ff3b3b';
                            activeLines.push(priceSeries.createPriceLine({{ price: w.price, color: color, lineWidth: 1, lineStyle: 2, axisLabelVisible: false }}));
                            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>`;
                        }};

                        data.walls.asks.forEach(w => addWall(w, 'ASK'));
                        data.walls.bids.forEach(w => addWall(w, 'BID'));
                        dom.wallList.innerHTML = html || '<span class="c-dim" style="font-size:11px">Scanning...</span>';
                    }}

                    // VOLUME CHART IN SIDEBAR
                    if (data.trade_history && data.trade_history.length) {{
                        const buyData = [];
                        const sellData = [];
                        data.trade_history.forEach(t => {{
                            const time = Math.floor(t.t);
                            buyData.push({{ time: time, value: t.buy }});
                            sellData.push({{ time: time, value: t.sell }});
                        }});
                        // Ensure unique time points for LW Charts
                        const uniqueBuys = [...new Map(buyData.map(i => [i.time, i])).values()];
                        const uniqueSells = [...new Map(sellData.map(i => [i.time, i])).values()];
                        
                        volBuySeries.setData(uniqueBuys);
                        volSellSeries.setData(uniqueSells);
                    }}

                    // DEPTH
                    if (data.depth_x.length) {{
                        const bids = [], asks = [], nets = [];
                        for(let i=0; i<data.depth_x.length; i++) {{
                            const t = data.depth_x[i];
                            bids.push({{ time: t, value: data.depth_bids[i] }});
                            asks.push({{ time: t, value: data.depth_asks[i] }});
                            nets.push({{ time: t, value: data.depth_net[i], color: data.depth_net[i] > 0 ? '#00ff9d' : '#ff3b3b' }});
                        }}
                        bidSeries.setData(bids);
                        askSeries.setData(asks);
                        netSeries.setData(nets);
                    }}
                }};
                
                ws.onclose = () => setTimeout(connect, 2000);
            }}
            connect();
        }});
    </script>
</body>
</html>
"""

# --- SERVER ---
async def kraken_worker():
    global market_state
    while True:
        try:
            async with websockets.connect("wss://ws.kraken.com/v2") as ws:
                logging.info(f"πŸ”Œ Connected to Kraken ({SYMBOL_KRAKEN})")
                
                # SUBSCRIBE TO BOOK AND TRADES
                await ws.send(json.dumps({
                    "method": "subscribe",
                    "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}
                }))
                await ws.send(json.dumps({
                    "method": "subscribe",
                    "params": {"channel": "trade", "symbol": [SYMBOL_KRAKEN]}
                }))

                async for message in ws:
                    payload = json.loads(message)
                    channel = payload.get("channel")
                    data = payload.get("data", [])

                    if channel == "book":
                        for item in data:
                            for bid in item.get('bids', []):
                                q, p = float(bid['qty']), float(bid['price'])
                                if q == 0: market_state['bids'].pop(p, None)
                                else: market_state['bids'][p] = q
                            for ask in item.get('asks', []):
                                q, p = float(ask['qty']), float(ask['price'])
                                if q == 0: market_state['asks'].pop(p, None)
                                else: market_state['asks'][p] = q
                        
                        if market_state['bids'] and market_state['asks']:
                            best_bid = max(market_state['bids'].keys())
                            best_ask = min(market_state['asks'].keys())
                            mid = (best_bid + best_ask) / 2
                            market_state['prev_mid'] = market_state['current_mid']
                            market_state['current_mid'] = mid
                            market_state['ready'] = True
                            
                            now = time.time()
                            if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
                                market_state['history'].append({'t': now, 'p': mid})
                                if len(market_state['history']) > HISTORY_LENGTH:
                                    market_state['history'].pop(0)
                    
                    elif channel == "trade":
                        # Process trades for volume history
                        for trade in data:
                            # Kraken Trade format: [price, qty, time, side, order_type, misc]
                            # side: 'buy' or 'sell'
                            try:
                                qty = float(trade['qty'])
                                side = trade['side'] # 'buy' or 'sell'
                                if side == 'buy':
                                    market_state['current_vol_window']['buy'] += qty
                                else:
                                    market_state['current_vol_window']['sell'] += qty
                            except:
                                pass

        except Exception as e:
            logging.warning(f"⚠️ Reconnecting: {e}")
            await asyncio.sleep(3)

async def broadcast_worker():
    while True:
        if connected_clients and market_state['ready']:
            payload = process_market_data()
            msg = json.dumps(payload)
            for ws in list(connected_clients):
                try: await ws.send_str(msg)
                except: pass
        await asyncio.sleep(BROADCAST_RATE)

async def websocket_handler(request):
    ws = web.WebSocketResponse()
    await ws.prepare(request)
    connected_clients.add(ws)
    try:
        async for msg in ws: pass
    finally:
        connected_clients.remove(ws)
    return ws

async def handle_index(request):
    return web.Response(text=HTML_PAGE, content_type='text/html')

async def start_background(app):
    app['kraken_task'] = asyncio.create_task(kraken_worker())
    app['broadcast_task'] = asyncio.create_task(broadcast_worker())

async def cleanup_background(app):
    app['kraken_task'].cancel()
    app['broadcast_task'].cancel()
    try: await app['kraken_task']; await app['broadcast_task']
    except: pass

async def main():
    app = web.Application()
    app.router.add_get('/', handle_index)
    app.router.add_get('/ws', websocket_handler)
    app.on_startup.append(start_background)
    app.on_cleanup.append(cleanup_background)
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, '0.0.0.0', PORT)
    await site.start()
    print(f"πŸš€ Quant Dashboard: http://localhost:{PORT}")
    await asyncio.Event().wait()

if __name__ == "__main__":
    try: asyncio.run(main())
    except KeyboardInterrupt: pass