Alvin3y1 commited on
Commit
6d1f4cf
·
verified ·
1 Parent(s): 6349f64

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +354 -625
app.py CHANGED
@@ -6,7 +6,7 @@ import bisect
6
  import math
7
  import statistics
8
  import aiohttp
9
- from datetime import datetime
10
  from aiohttp import web
11
  import websockets
12
 
@@ -15,647 +15,404 @@ PORT = 7860
15
  HISTORY_LENGTH = 300
16
  BROADCAST_RATE = 0.1
17
 
18
- DECAY_LAMBDA = 50.0
19
- IMPACT_SENSITIVITY = 2.0
20
- Z_SCORE_THRESHOLD = 3.0
21
- WALL_LOOKBACK = 200
 
22
 
23
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
24
 
25
- class KalmanFilter:
26
- def __init__(self, process_noise=1e-4, measurement_noise=1e-2):
27
- self.x = 0.0
28
- self.v = 0.0
29
- self.P = 1.0
30
- self.Q = process_noise
31
- self.R = measurement_noise
32
- self.first_run = True
33
- self.last_time = time.time()
34
-
35
- def update(self, measurement):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  now = time.time()
37
- dt = now - self.last_time
38
- self.last_time = now
39
-
40
- if self.first_run:
41
- self.x = measurement
42
- self.first_run = False
43
- return
44
-
45
- # 1. Predict
46
- # x = x + v*dt
47
- pred_x = self.x + self.v * dt
48
  pred_v = self.v
49
-
50
- # P = FPF' + Q
51
- # Simple scalar expansion for P (covariance)
52
- # F is [1 dt; 0 1]
53
- p_xx = self.P + dt * dt + self.Q
54
-
55
- # 2. Update
56
- # y = z - Hx (Residual)
57
- y = measurement - pred_x
58
-
59
- # S = HPH' + R
60
- S = p_xx + self.R
61
-
62
- # K = PH'S^-1
63
- K_x = p_xx / S
64
- K_v = dt / S # simplified gain for velocity
65
 
66
- # x = x + Ky
67
- self.x = pred_x + K_x * y
68
- self.v = pred_v + K_v * y
69
 
70
- # P = (I - KH)P
71
- self.P = (1 - K_x) * p_xx
 
 
 
 
72
 
73
  market_state = {
74
  "bids": {},
75
  "asks": {},
76
  "history": [],
77
- "pred_history": [],
78
- "trade_vol_history": [],
79
  "ohlc_history": [],
80
  "current_vol_window": {"buy": 0.0, "sell": 0.0, "start": time.time()},
81
  "current_mid": 0.0,
82
  "ready": False,
83
- "kalman": KalmanFilter(),
84
- "volatility_sq_sum": 0.0,
85
- "volatility_count": 0
 
86
  }
87
 
88
  connected_clients = set()
89
 
90
- def detect_anomalies(orders, scan_depth):
91
- if len(orders) < 10: return []
92
- relevant_orders = orders[:scan_depth]
93
- volumes = [q for p, q in relevant_orders]
94
- if not volumes: return []
95
 
96
- try:
97
- avg_vol = statistics.mean(volumes)
98
- stdev_vol = statistics.stdev(volumes)
99
- except statistics.StatisticsError:
100
- return []
101
-
102
- if stdev_vol == 0: return []
103
-
104
- walls = []
105
- for price, qty in relevant_orders:
106
- z_score = (qty - avg_vol) / stdev_vol
107
- if z_score > Z_SCORE_THRESHOLD:
108
- walls.append({"price": price, "vol": qty, "z_score": z_score})
109
 
110
- walls.sort(key=lambda x: x['z_score'], reverse=True)
111
- return walls[:3]
112
 
113
- def calculate_micro_price_structure(diff_x, diff_y_net, current_mid, best_bid, best_ask, walls):
114
- if not diff_x or len(diff_x) < 5: return None
115
-
116
- weighted_imbalance = 0.0
117
- total_weight = 0.0
118
 
119
- for i in range(len(diff_x)):
120
- dist = diff_x[i]
121
- net_vol = diff_y_net[i]
122
- weight = math.exp(-dist / DECAY_LAMBDA)
123
- weighted_imbalance += net_vol * weight
124
- total_weight += weight
125
-
126
- rho = weighted_imbalance / total_weight if total_weight > 0 else 0
127
-
128
- spread = best_ask - best_bid
129
- theoretical_delta = (spread / 2) * rho * IMPACT_SENSITIVITY
130
- projected_price = current_mid + theoretical_delta
131
-
132
- final_delta = theoretical_delta
133
- if final_delta > 0 and walls['asks']:
134
- nearest_wall = walls['asks'][0]
135
- if projected_price >= nearest_wall['price']:
136
- damp_factor = 1.0 / (1.0 + (nearest_wall['z_score'] * 0.2))
137
- final_delta *= damp_factor
138
- elif final_delta < 0 and walls['bids']:
139
- nearest_wall = walls['bids'][0]
140
- if projected_price <= nearest_wall['price']:
141
- damp_factor = 1.0 / (1.0 + (nearest_wall['z_score'] * 0.2))
142
- final_delta *= damp_factor
143
-
144
- return {
145
- "projected": current_mid + final_delta,
146
- "rho": rho
147
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
  def process_market_data():
150
  if not market_state['ready']: return {"error": "Initializing..."}
151
 
152
  mid = market_state['current_mid']
153
-
154
- # 1. Update Kalman Filter
155
- market_state['kalman'].update(mid)
156
-
157
- # 2. Update Volatility Estimate (Welford's online algorithm approx)
158
- if market_state['history']:
159
- prev_p = market_state['history'][-1]['p']
160
- ret = math.log(mid / prev_p) if prev_p > 0 else 0
161
- market_state['volatility_sq_sum'] = 0.95 * market_state['volatility_sq_sum'] + 0.05 * (ret ** 2)
162
-
163
- current_volatility = math.sqrt(market_state['volatility_sq_sum']) * mid
164
-
165
  now = time.time()
 
 
 
 
 
 
 
 
166
 
167
- # Volume Window Logic
168
- if now - market_state['current_vol_window']['start'] >= 1.0:
169
- market_state['trade_vol_history'].append({
170
- 't': now,
171
- 'buy': market_state['current_vol_window']['buy'],
172
- 'sell': market_state['current_vol_window']['sell']
173
- })
174
- if len(market_state['trade_vol_history']) > 60:
175
- market_state['trade_vol_history'].pop(0)
176
- market_state['current_vol_window'] = {"buy": 0.0, "sell": 0.0, "start": now}
177
-
178
- sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
179
- sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
180
 
181
- if not sorted_bids or not sorted_asks: return {"error": "Empty Book"}
182
-
183
- best_bid = sorted_bids[0][0]
184
- best_ask = sorted_asks[0][0]
185
-
186
- bid_walls = detect_anomalies(sorted_bids, WALL_LOOKBACK)
187
- ask_walls = detect_anomalies(sorted_asks, WALL_LOOKBACK)
188
-
189
- d_b_x, d_b_y, cum = [], [], 0
190
- for p, q in sorted_bids[:300]:
191
- d = mid - p
192
- if d >= 0:
193
- cum += q
194
- d_b_x.append(d); d_b_y.append(cum)
195
-
196
- d_a_x, d_a_y, cum = [], [], 0
197
- for p, q in sorted_asks[:300]:
198
- d = p - mid
199
- if d >= 0:
200
- cum += q
201
- d_a_x.append(d); d_a_y.append(cum)
202
-
203
- diff_x, diff_y_net = [], []
204
- chart_bids, chart_asks = [], []
205
 
206
- if d_b_x and d_a_x:
207
- max_dist = min(d_b_x[-1], d_a_x[-1])
208
- step_size = max_dist / 100
209
- steps = [i * step_size for i in range(1, 101)]
210
 
211
- for s in steps:
212
- idx_b = bisect.bisect_right(d_b_x, s)
213
- vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
214
- idx_a = bisect.bisect_right(d_a_x, s)
215
- vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
216
-
217
- diff_x.append(s)
218
- diff_y_net.append(vol_b - vol_a)
219
- chart_bids.append(vol_b)
220
- chart_asks.append(vol_a)
221
-
222
- analysis = calculate_micro_price_structure(
223
- diff_x, diff_y_net, mid, best_bid, best_ask,
224
- {"bids": bid_walls, "asks": ask_walls}
225
- )
226
 
227
- # --- PREDICT NEXT CANDLE ---
228
- # Formula: Price(t+60) = Price(t) + (Kalman_Velocity * 60) + (OFI_Impact)
229
- kf_velocity = market_state['kalman'].v
230
- ofi_impact = 0
231
- if analysis:
232
- # We use the Micro-Structure Delta as the immediate force
233
- ofi_impact = (analysis['projected'] - mid)
234
-
235
- # Forecast 60 seconds out
236
- pred_close = mid + (kf_velocity * 60.0) + ofi_impact
 
 
 
 
 
 
 
 
 
 
 
 
 
237
 
238
- # Estimate High/Low based on Volatility
239
- # We assume High/Low expand from Open/Close by sigma * sqrt(t)
240
- range_expansion = current_volatility * math.sqrt(60) * 2 # 2 Sigma
 
 
241
 
242
  pred_candle = {
243
- 'time': int(now) + 60, # Future time
244
  'open': mid,
245
  'close': pred_close,
246
- 'high': max(mid, pred_close) + range_expansion,
247
- 'low': min(mid, pred_close) - range_expansion
248
  }
249
 
250
- if analysis:
251
- if not market_state['pred_history'] or (now - market_state['pred_history'][-1]['t'] > 0.5):
252
- market_state['pred_history'].append({'t': now, 'p': analysis['projected']})
253
- if len(market_state['pred_history']) > HISTORY_LENGTH:
254
- market_state['pred_history'].pop(0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
  return {
257
  "mid": mid,
258
  "history": market_state['history'],
259
- "pred_history": market_state['pred_history'],
260
- "trade_history": market_state['trade_vol_history'],
261
  "ohlc": market_state['ohlc_history'],
262
- "pred_candle": pred_candle, # NEW
263
- "depth_x": diff_x,
264
- "depth_net": diff_y_net,
265
- "depth_bids": chart_bids,
266
- "depth_asks": chart_asks,
 
267
  "analysis": analysis,
268
- "walls": {"bids": bid_walls, "asks": ask_walls}
269
  }
270
 
 
271
  HTML_PAGE = f"""
272
  <!DOCTYPE html>
273
  <html lang="en">
274
  <head>
275
  <meta charset="UTF-8">
276
- <title>{SYMBOL_KRAKEN}</title>
277
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
278
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
279
  <style>
280
- :root {{
281
- --bg-base: #000000;
282
- --bg-panel: #0a0a0a;
283
- --border: #252525;
284
- --text-main: #FFFFFF;
285
- --text-dim: #999999;
286
- --green: #00ff9d;
287
- --red: #ff3b3b;
288
- --blue: #2979ff;
289
- --yellow: #ffeb3b;
290
- --purple: #d500f9;
291
- }}
292
- body {{
293
- margin: 0; padding: 0;
294
- background-color: var(--bg-base);
295
- color: var(--text-main);
296
- font-family: 'Inter', sans-serif;
297
- overflow: hidden;
298
- height: 100vh; width: 100vw;
299
- }}
300
-
301
- .layout {{
302
- display: grid;
303
- grid-template-rows: 34px 1fr 1fr;
304
- grid-template-columns: 3fr 1fr;
305
- gap: 1px;
306
- background-color: var(--border);
307
- height: 100vh;
308
- box-sizing: border-box;
309
- }}
310
-
311
  .panel {{ background: var(--bg-panel); display: flex; flex-direction: column; overflow: hidden; }}
312
-
313
- .status-bar {{
314
- grid-column: 1 / 3;
315
- grid-row: 1 / 2;
316
- background: var(--bg-panel);
317
- display: flex;
318
- align-items: center;
319
- justify-content: space-between;
320
- padding: 0 12px;
321
- font-family: 'JetBrains Mono', monospace;
322
- font-size: 12px;
323
- text-transform: uppercase;
324
- border-bottom: 1px solid var(--border);
325
- z-index: 50;
326
- }}
327
- .status-left {{ display: flex; gap: 20px; align-items: center; }}
328
- .live-dot {{ width: 8px; height: 8px; background-color: var(--green); border-radius: 50%; display: inline-block; box-shadow: 0 0 8px var(--green); }}
329
- .ticker-val {{ font-weight: 700; color: #fff; font-size: 13px; }}
330
-
331
  #p-chart {{ grid-column: 1 / 2; grid-row: 2 / 3; }}
332
-
333
- #p-bottom {{
334
- grid-column: 1 / 2; grid-row: 3 / 4;
335
- display: grid;
336
- grid-template-columns: 1fr 1fr;
337
- gap: 1px;
338
- background: var(--border);
339
- }}
340
- .bottom-sub {{ background: var(--bg-panel); display: flex; flex-direction: column; position: relative; }}
341
-
342
- #p-sidebar {{
343
- grid-column: 2 / 3;
344
- grid-row: 2 / 4;
345
- padding: 15px;
346
- display: flex;
347
- flex-direction: column;
348
- gap: 15px;
349
- border-left: 1px solid var(--border);
350
- overflow: hidden;
351
- }}
352
-
353
- .chart-header {{
354
- height: 24px;
355
- min-height: 24px;
356
- display: flex;
357
- align-items: center;
358
- padding-left: 12px;
359
- font-size: 10px;
360
- font-weight: 700;
361
- color: var(--text-dim);
362
- background: #050505;
363
- border-bottom: 1px solid #151515;
364
- letter-spacing: 0.5px;
365
- }}
366
-
367
  .data-group {{ display: flex; flex-direction: column; gap: 4px; }}
368
- .label {{ font-size: 10px; color: var(--text-dim); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }}
369
  .value {{ font-family: 'JetBrains Mono', monospace; font-size: 20px; font-weight: 700; color: #fff; }}
370
  .value-lg {{ font-size: 26px; }}
371
  .value-sub {{ font-family: 'JetBrains Mono', monospace; font-size: 11px; margin-top: 2px; color: #666; }}
372
-
373
  .divider {{ height: 1px; background: var(--border); width: 100%; }}
374
- .c-green {{ color: var(--green); }}
375
- .c-red {{ color: var(--red); }}
376
- .c-dim {{ color: var(--text-dim); }}
377
  .c-purple {{ color: var(--purple); }}
378
-
379
- .list-container {{ display: flex; flex-direction: column; gap: 8px; overflow-y: auto; height: 100px; }}
380
- .list-item {{
381
- display: flex; justify-content: space-between;
382
- font-family: 'JetBrains Mono', monospace;
383
- font-size: 11px;
384
- border-bottom: 1px solid #151515;
385
- padding-bottom: 4px;
386
- }}
387
- .list-item span:first-child {{ color: #e0e0e0; }}
388
- .list-item:last-child {{ border: none; }}
389
-
390
- .sidebar-chart-box {{
391
- flex: 1;
392
- display: flex;
393
- flex-direction: column;
394
- min-height: 0;
395
- }}
396
- .mini-chart {{
397
- flex: 1;
398
- background: rgba(255,255,255,0.02);
399
- border: 1px solid var(--border);
400
- border-radius: 4px;
401
- }}
402
  </style>
403
  </head>
404
  <body>
405
-
406
  <div class="layout">
407
  <div class="status-bar">
408
- <div class="status-left">
409
- <span class="live-dot"></span>
410
- <span style="font-weight:700; color:#fff;">{SYMBOL_KRAKEN}</span>
411
- <span id="price-ticker" class="ticker-val">---</span>
412
- </div>
413
- <div class="status-right" id="clock">00:00:00 UTC</div>
414
  </div>
415
-
416
  <div id="p-chart" class="panel">
417
- <div class="chart-header">PRICE ACTION (BLUE) // PREDICTION (YELLOW)</div>
418
- <div id="tv-price" style="flex: 1; width: 100%;"></div>
419
  </div>
420
-
421
- <div id="p-bottom">
422
- <div class="bottom-sub">
423
- <div class="chart-header">1M KLINE + GHOST PREDICTION (PURPLE)</div>
424
- <div id="tv-candles" style="flex: 1; width: 100%;"></div>
425
- </div>
426
- <div class="bottom-sub">
427
- <div class="chart-header">ORDER FLOW IMBALANCE</div>
428
- <div id="tv-net" style="flex: 1; width: 100%;"></div>
429
- </div>
430
  </div>
431
-
432
  <div id="p-sidebar" class="panel">
433
-
434
  <div class="data-group">
435
- <span class="label">Next 1M Candle Close</span>
436
  <div style="display:flex; align-items: baseline; gap: 10px;">
437
  <span id="proj-pct" class="value value-lg">--%</span>
438
  <span id="proj-val" class="value-sub c-purple">---</span>
439
  </div>
440
- <span class="label" style="margin-top:4px;">Kalman Trend + OFI</span>
441
  </div>
442
-
443
  <div class="divider"></div>
444
-
445
  <div class="data-group">
446
- <span class="label">OFI Imbalance Ratio</span>
447
- <span id="score-val" class="value">0.00</span>
448
  </div>
449
-
450
  <div class="divider"></div>
451
-
452
  <div class="data-group">
453
- <span class="label">Detected Walls (Z > 3.0)</span>
454
- <div id="wall-list" class="list-container">
455
- <span class="c-dim" style="font-size: 11px;">Scanning...</span>
456
- </div>
457
- </div>
458
-
459
- <div class="sidebar-chart-box">
460
- <span class="label" style="margin-bottom:4px;">Real-time Volume Ticks</span>
461
- <div id="sidebar-vol" class="mini-chart"></div>
462
- </div>
463
-
464
- <div class="sidebar-chart-box">
465
- <span class="label" style="margin-bottom:4px;">Liquidity Density</span>
466
- <div id="sidebar-density" class="mini-chart"></div>
467
  </div>
468
  </div>
469
  </div>
470
-
471
  <script>
472
- setInterval(() => {{
473
- const now = new Date();
474
- document.getElementById('clock').innerText = now.toISOString().split('T')[1].split('.')[0] + ' UTC';
475
- }}, 1000);
476
-
477
- document.addEventListener('DOMContentLoaded', () => {{
478
- const dom = {{
479
- ticker: document.getElementById('price-ticker'),
480
- score: document.getElementById('score-val'),
481
- projVal: document.getElementById('proj-val'),
482
- projPct: document.getElementById('proj-pct'),
483
- wallList: document.getElementById('wall-list')
484
- }};
485
-
486
- const chartOpts = {{
487
- layout: {{ background: {{ type: 'solid', color: '#0a0a0a' }}, textColor: '#888', fontFamily: 'JetBrains Mono' }},
488
- grid: {{ vertLines: {{ color: '#151515' }}, horzLines: {{ color: '#151515' }} }},
489
- rightPriceScale: {{ borderColor: '#222', scaleMargins: {{ top: 0.1, bottom: 0.1 }} }},
490
- timeScale: {{ borderColor: '#222', timeVisible: true, secondsVisible: true }},
491
- crosshair: {{ mode: 1, vertLine: {{ color: '#444', labelBackgroundColor: '#444' }}, horzLine: {{ color: '#444', labelBackgroundColor: '#444' }} }}
492
- }};
493
-
494
- const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts);
495
- const priceSeries = priceChart.addLineSeries({{ color: '#2979ff', lineWidth: 2, title: 'Price' }});
496
- const predSeries = priceChart.addLineSeries({{ color: '#ffeb3b', lineWidth: 2, lineStyle: 2, title: 'Forecast' }});
497
-
498
- const candleChart = LightweightCharts.createChart(document.getElementById('tv-candles'), {{
499
- ...chartOpts,
500
- timeScale: {{ timeVisible: true, secondsVisible: false }}
501
- }});
502
- const candleSeries = candleChart.addCandlestickSeries({{
503
- upColor: '#00ff9d', downColor: '#ff3b3b', borderVisible: false, wickUpColor: '#00ff9d', wickDownColor: '#ff3b3b'
504
- }});
505
- // Ghost Candle Series (Prediction)
506
- const ghostSeries = candleChart.addCandlestickSeries({{
507
- upColor: 'rgba(213, 0, 249, 0.5)',
508
- downColor: 'rgba(213, 0, 249, 0.5)',
509
- borderVisible: true,
510
- borderColor: '#d500f9',
511
- wickUpColor: '#d500f9',
512
- wickDownColor: '#d500f9'
513
- }});
514
-
515
- const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{
516
- ...chartOpts, localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
517
- }});
518
- const netSeries = netChart.addHistogramSeries({{ color: '#2979ff' }});
519
-
520
- const volChart = LightweightCharts.createChart(document.getElementById('sidebar-vol'), {{
521
- ...chartOpts,
522
- grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }},
523
- rightPriceScale: {{ visible: false }},
524
- timeScale: {{ visible: false }},
525
- handleScroll: false, handleScale: false
526
- }});
527
- const volBuySeries = volChart.addHistogramSeries({{ color: '#00ff9d' }});
528
- const volSellSeries = volChart.addHistogramSeries({{ color: '#ff3b3b' }});
529
-
530
- const denChart = LightweightCharts.createChart(document.getElementById('sidebar-density'), {{
531
- ...chartOpts,
532
- grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }},
533
- rightPriceScale: {{ visible: false }},
534
- timeScale: {{ visible: false }},
535
- handleScroll: false, handleScale: false
536
- }});
537
- const bidSeries = denChart.addAreaSeries({{ lineColor: '#00ff9d', topColor: 'rgba(0, 255, 157, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
538
- const askSeries = denChart.addAreaSeries({{ lineColor: '#ff3b3b', topColor: 'rgba(255, 59, 59, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
539
-
540
- let activeLines = [];
541
-
542
- new ResizeObserver(entries => {{
543
- for(let entry of entries) {{
544
- const {{width, height}} = entry.contentRect;
545
- if(entry.target.id === 'tv-price') priceChart.applyOptions({{width, height}});
546
- if(entry.target.id === 'tv-candles') candleChart.applyOptions({{width, height}});
547
- if(entry.target.id === 'tv-net') netChart.applyOptions({{width, height}});
548
- if(entry.target.id === 'sidebar-vol') volChart.applyOptions({{width, height}});
549
- if(entry.target.id === 'sidebar-density') denChart.applyOptions({{width, height}});
550
- }}
551
- }}).observe(document.body);
552
-
553
- ['tv-price', 'tv-candles', 'tv-net', 'sidebar-vol', 'sidebar-density'].forEach(id => {{
554
- new ResizeObserver(e => {{
555
- const t = document.getElementById(id);
556
- if (t.clientWidth && t.clientHeight) {{
557
- if(id === 'tv-price') priceChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
558
- if(id === 'tv-candles') candleChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
559
- if(id === 'tv-net') netChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
560
- if(id === 'sidebar-vol') volChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
561
- if(id === 'sidebar-density') denChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
562
- }}
563
- }}).observe(document.getElementById(id));
564
- }});
565
-
566
- function connect() {{
567
- const ws = new WebSocket((location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/ws');
568
-
569
- ws.onmessage = (e) => {{
570
- const data = JSON.parse(e.data);
571
- if (data.error) return;
572
-
573
- if (data.history.length) {{
574
- const hist = data.history.map(d => ({{ time: Math.floor(d.t), value: d.p }}));
575
- const cleanHist = [...new Map(hist.map(i => [i.time, i])).values()];
576
- priceSeries.setData(cleanHist);
577
-
578
- const lastP = cleanHist[cleanHist.length-1].value;
579
- dom.ticker.innerText = lastP.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
580
-
581
- if (data.analysis) {{
582
- const proj = data.analysis.projected;
583
- const rho = data.analysis.rho;
584
-
585
- predSeries.setData([
586
- cleanHist[cleanHist.length-1],
587
- {{ time: cleanHist[cleanHist.length-1].time + 60, value: proj }}
588
- ]);
589
-
590
- dom.score.innerText = rho.toFixed(3);
591
- dom.score.style.color = rho > 0 ? "var(--green)" : (rho < 0 ? "var(--red)" : "var(--text-main)");
592
- }}
593
- }}
594
-
595
- if (data.ohlc && data.ohlc.length) {{
596
- const candles = data.ohlc.map(c => ({{
597
- time: c.time, open: c.open, high: c.high, low: c.low, close: c.close
598
- }}));
599
- const uniqueCandles = [...new Map(candles.map(i => [i.time, i])).values()];
600
- candleSeries.setData(uniqueCandles);
601
- }}
602
-
603
- // RENDER GHOST CANDLE
604
- if (data.pred_candle) {{
605
- ghostSeries.setData([data.pred_candle]);
606
- const pClose = data.pred_candle.close;
607
- dom.projVal.innerText = pClose.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
608
-
609
- // Calculate pct from current open
610
- const currentP = parseFloat(dom.ticker.innerText.replace(/,/g, ''));
611
- const pct = ((pClose - currentP) / currentP) * 100;
612
- const sign = pct >= 0 ? "+" : "";
613
- dom.projPct.innerText = `${{sign}}${{pct.toFixed(4)}}%`;
614
- dom.projPct.style.color = pct >= 0 ? "var(--green)" : "var(--red)";
615
- }}
616
-
617
- if (data.walls) {{
618
- activeLines.forEach(l => priceSeries.removePriceLine(l));
619
- activeLines = [];
620
- let html = "";
621
- const addWall = (w, type) => {{
622
- const color = type === 'BID' ? '#00ff9d' : '#ff3b3b';
623
- activeLines.push(priceSeries.createPriceLine({{ price: w.price, color: color, lineWidth: 1, lineStyle: 2, axisLabelVisible: false }}));
624
- 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>`;
625
- }};
626
- data.walls.asks.forEach(w => addWall(w, 'ASK'));
627
- data.walls.bids.forEach(w => addWall(w, 'BID'));
628
- dom.wallList.innerHTML = html || '<span class="c-dim" style="font-size:11px">Scanning...</span>';
629
  }}
 
630
 
631
- if (data.trade_history && data.trade_history.length) {{
632
- const buyData = [], sellData = [];
633
- data.trade_history.forEach(t => {{
634
- const time = Math.floor(t.t);
635
- buyData.push({{ time: time, value: t.buy }});
636
- sellData.push({{ time: time, value: t.sell }});
637
- }});
638
- volBuySeries.setData([...new Map(buyData.map(i => [i.time, i])).values()]);
639
- volSellSeries.setData([...new Map(sellData.map(i => [i.time, i])).values()]);
640
- }}
641
 
642
- if (data.depth_x.length) {{
643
- const bids = [], asks = [], nets = [];
644
- for(let i=0; i<data.depth_x.length; i++) {{
645
- const t = data.depth_x[i];
646
- bids.push({{ time: t, value: data.depth_bids[i] }});
647
- asks.push({{ time: t, value: data.depth_asks[i] }});
648
- nets.push({{ time: t, value: data.depth_net[i], color: data.depth_net[i] > 0 ? '#00ff9d' : '#ff3b3b' }});
649
- }}
650
- bidSeries.setData(bids);
651
- askSeries.setData(asks);
652
- netSeries.setData(nets);
653
- }}
654
- }};
655
- ws.onclose = () => setTimeout(connect, 2000);
656
- }}
657
- connect();
658
- }});
659
  </script>
660
  </body>
661
  </html>
@@ -664,6 +421,7 @@ HTML_PAGE = f"""
664
  async def kraken_worker():
665
  global market_state
666
 
 
667
  try:
668
  async with aiohttp.ClientSession() as session:
669
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
@@ -671,40 +429,23 @@ async def kraken_worker():
671
  if response.status == 200:
672
  data = await response.json()
673
  if 'result' in data:
674
- for key in data['result']:
675
- if key != 'last':
676
- raw_candles = data['result'][key]
677
- market_state['ohlc_history'] = [
678
- {
679
- 'time': int(c[0]),
680
- 'open': float(c[1]),
681
- 'high': float(c[2]),
682
- 'low': float(c[3]),
683
- 'close': float(c[4])
684
- }
685
- for c in raw_candles[-120:]
686
- ]
687
- break
688
  except Exception as e:
689
- logging.error(f"History fetch failed: {e}")
690
 
691
  while True:
692
  try:
693
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
694
  logging.info(f"🔌 Connected to Kraken ({SYMBOL_KRAKEN})")
695
 
696
- await ws.send(json.dumps({
697
- "method": "subscribe",
698
- "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}
699
- }))
700
- await ws.send(json.dumps({
701
- "method": "subscribe",
702
- "params": {"channel": "trade", "symbol": [SYMBOL_KRAKEN]}
703
- }))
704
- await ws.send(json.dumps({
705
- "method": "subscribe",
706
- "params": {"channel": "ohlc", "symbol": [SYMBOL_KRAKEN], "interval": 1}
707
- }))
708
 
709
  async for message in ws:
710
  payload = json.loads(message)
@@ -714,79 +455,69 @@ async def kraken_worker():
714
  if channel == "book":
715
  for item in data:
716
  for bid in item.get('bids', []):
717
- q, p = float(bid['qty']), float(bid['price'])
718
- if q == 0: market_state['bids'].pop(p, None)
719
- else: market_state['bids'][p] = q
720
  for ask in item.get('asks', []):
721
- q, p = float(ask['qty']), float(ask['price'])
722
- if q == 0: market_state['asks'].pop(p, None)
723
- else: market_state['asks'][p] = q
724
 
 
 
 
 
725
  if market_state['bids'] and market_state['asks']:
726
  best_bid = max(market_state['bids'].keys())
727
  best_ask = min(market_state['asks'].keys())
728
- mid = (best_bid + best_ask) / 2
729
- market_state['prev_mid'] = market_state['current_mid']
730
- market_state['current_mid'] = mid
731
  market_state['ready'] = True
732
-
733
- now = time.time()
734
- if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
735
- market_state['history'].append({'t': now, 'p': mid})
736
- if len(market_state['history']) > HISTORY_LENGTH:
737
- market_state['history'].pop(0)
738
-
739
  elif channel == "trade":
740
  for trade in data:
741
  try:
742
- qty = float(trade['qty'])
743
- side = trade['side']
744
- if side == 'buy': market_state['current_vol_window']['buy'] += qty
745
- else: market_state['current_vol_window']['sell'] += qty
 
 
 
746
  except: pass
747
 
748
  elif channel == "ohlc":
749
- for candle in data:
750
- try:
751
- c_data = {
752
- 'time': int(float(candle['endtime'])),
753
- 'open': float(candle['open']),
754
- 'high': float(candle['high']),
755
- 'low': float(candle['low']),
756
- 'close': float(candle['close'])
757
- }
758
- if market_state['ohlc_history'] and market_state['ohlc_history'][-1]['time'] == c_data['time']:
759
- market_state['ohlc_history'][-1] = c_data
760
- else:
761
- market_state['ohlc_history'].append(c_data)
762
- if len(market_state['ohlc_history']) > 100:
763
- market_state['ohlc_history'].pop(0)
764
- except Exception as e:
765
- pass
766
 
767
  except Exception as e:
768
- logging.warning(f"⚠️ Reconnecting: {e}")
769
- await asyncio.sleep(3)
770
 
771
  async def broadcast_worker():
772
  while True:
773
  if connected_clients and market_state['ready']:
774
  payload = process_market_data()
775
- msg = json.dumps(payload)
776
- for ws in list(connected_clients):
777
- try: await ws.send_str(msg)
778
- except: pass
 
779
  await asyncio.sleep(BROADCAST_RATE)
780
 
781
  async def websocket_handler(request):
782
  ws = web.WebSocketResponse()
783
  await ws.prepare(request)
784
  connected_clients.add(ws)
785
- try:
786
- async for msg in ws:
787
- pass
788
- finally:
789
- connected_clients.remove(ws)
790
  return ws
791
 
792
  async def handle_index(request):
@@ -799,8 +530,6 @@ async def start_background(app):
799
  async def cleanup_background(app):
800
  app['kraken_task'].cancel()
801
  app['broadcast_task'].cancel()
802
- try: await app['kraken_task']; await app['broadcast_task']
803
- except: pass
804
 
805
  async def main():
806
  app = web.Application()
 
6
  import math
7
  import statistics
8
  import aiohttp
9
+ from collections import deque
10
  from aiohttp import web
11
  import websockets
12
 
 
15
  HISTORY_LENGTH = 300
16
  BROADCAST_RATE = 0.1
17
 
18
+ # --- MATH CONSTANTS ---
19
+ # DECAY for Micro-Price weights (focus on near-price liquidity)
20
+ MICROPRICE_DECAY = 0.05
21
+ # Lookback for VWAP and Volatility
22
+ WINDOW_SIZE = 60
23
 
24
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
25
 
26
+ # --- MATHEMATICAL HELPERS ---
27
+
28
+ class OnlineStats:
29
+ """
30
+ Welford's Online Algorithm for calculating Mean and Variance
31
+ in a single pass (O(1) complexity).
32
+ """
33
+ def __init__(self):
34
+ self.count = 0
35
+ self.mean = 0.0
36
+ self.M2 = 0.0
37
+
38
+ def update(self, value):
39
+ self.count += 1
40
+ delta = value - self.mean
41
+ self.mean += delta / self.count
42
+ delta2 = value - self.mean
43
+ self.M2 += delta * delta2
44
+
45
+ @property
46
+ def variance(self):
47
+ if self.count < 2: return 0.0
48
+ return self.M2 / self.count
49
+
50
+ @property
51
+ def std_dev(self):
52
+ return math.sqrt(self.variance)
53
+
54
+ class KalmanVelocity:
55
+ """
56
+ Kalman Filter specifically tuned for tracking Velocity (Trend)
57
+ rather than Position.
58
+ Model: Constant Velocity
59
+ """
60
+ def __init__(self, R=0.001, Q=0.0001):
61
+ self.z = 0.0 # Position
62
+ self.v = 0.0 # Velocity
63
+ self.P = 1.0 # Covariance
64
+ self.R = R # Measurement Noise
65
+ self.Q = Q # Process Noise
66
+ self.last_ts = time.time()
67
+
68
+ def update(self, price):
69
  now = time.time()
70
+ dt = now - self.last_ts
71
+ self.last_ts = now
72
+ if dt <= 0: return
73
+
74
+ # Predict
75
+ pred_z = self.z + self.v * dt
 
 
 
 
 
76
  pred_v = self.v
77
+ p_cov = self.P + self.Q
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
+ # Update
80
+ y = price - pred_z # Residual
81
+ K = p_cov / (p_cov + self.R) # Kalman Gain
82
 
83
+ self.z = pred_z + K * y
84
+ # Velocity update derived from residual
85
+ self.v = pred_v + (K / dt) * y
86
+ self.P = (1 - K) * p_cov
87
+
88
+ # --- STATE MANAGEMENT ---
89
 
90
  market_state = {
91
  "bids": {},
92
  "asks": {},
93
  "history": [],
94
+ "trade_history": deque(maxlen=2000), # Store recent trades for VWAP
 
95
  "ohlc_history": [],
96
  "current_vol_window": {"buy": 0.0, "sell": 0.0, "start": time.time()},
97
  "current_mid": 0.0,
98
  "ready": False,
99
+ "kalman": KalmanVelocity(),
100
+ "stats": OnlineStats(), # Online Volatility Tracker
101
+ "vwap_numerator": 0.0,
102
+ "vwap_denominator": 0.0
103
  }
104
 
105
  connected_clients = set()
106
 
107
+ # --- CORE MATH LOGIC ---
 
 
 
 
108
 
109
+ def calculate_weighted_micro_price(mid_price):
110
+ """
111
+ Calculates the 'Micro-Price' (Stoikov).
112
+ The mid-price is adjusted by the imbalance of liquidity
113
+ weighted by distance from the mid.
114
+ """
115
+ bids = sorted(market_state['bids'].items(), reverse=True)[:50] # Top 50 levels
116
+ asks = sorted(market_state['asks'].items())[:50]
 
 
 
 
 
117
 
118
+ if not bids or not asks: return mid_price
 
119
 
120
+ sum_wb = 0.0
121
+ sum_wa = 0.0
 
 
 
122
 
123
+ # Calculate Volume-Weighted Imbalance
124
+ for p, q in bids:
125
+ # Weight decays exponentially as distance from mid increases
126
+ distance = abs(mid_price - p)
127
+ weight = q * math.exp(-MICROPRICE_DECAY * distance)
128
+ sum_wb += weight
129
+
130
+ for p, q in asks:
131
+ distance = abs(p - mid_price)
132
+ weight = q * math.exp(-MICROPRICE_DECAY * distance)
133
+ sum_wa += weight
134
+
135
+ total_w = sum_wb + sum_wa
136
+ if total_w == 0: return mid_price
137
+
138
+ # Imbalance Ratio
139
+ imbalance = (sum_wb - sum_wa) / total_w
140
+
141
+ # Adjust spread based on imbalance
142
+ spread = asks[0][0] - bids[0][0]
143
+
144
+ # Formula: MicroPrice = Mid + (Imbalance * (Spread / 2))
145
+ micro_price = mid_price + (imbalance * (spread / 2))
146
+ return micro_price
147
+
148
+ def calculate_vwap_1m():
149
+ """Calculates Volume Weighted Average Price over the last 60 seconds."""
150
+ cutoff = time.time() - 60
151
+ v_sum = 0.0
152
+ pv_sum = 0.0
153
+
154
+ # Iterate trades in reverse (newest first)
155
+ for trade in reversed(market_state['trade_history']):
156
+ if trade['t'] < cutoff: break
157
+ pv_sum += trade['p'] * trade['q']
158
+ v_sum += trade['q']
159
+
160
+ return pv_sum / v_sum if v_sum > 0 else market_state['current_mid']
161
+
162
+ def calculate_kyle_lambda(volatility, volume_window):
163
+ """
164
+ Kyle's Lambda: A measure of market impact (Liquidity Cost).
165
+ Lambda ~ sigma / Volume
166
+ Quantifies how much price moves per $1 of order flow.
167
+ """
168
+ if volume_window <= 0: return 0
169
+ # Scaling factor (heuristic normalization for BTC/USD typical volumes)
170
+ return (volatility * 1000) / (math.sqrt(volume_window) + 1)
171
 
172
  def process_market_data():
173
  if not market_state['ready']: return {"error": "Initializing..."}
174
 
175
  mid = market_state['current_mid']
 
 
 
 
 
 
 
 
 
 
 
 
176
  now = time.time()
177
+
178
+ # 1. Update Volatility Stats (Welford)
179
+ market_state['stats'].update(mid)
180
+ volatility = market_state['stats'].std_dev
181
+ if volatility == 0: volatility = 1.0 # fallback
182
+
183
+ # 2. Update Kalman Filter (Velocity Trend)
184
+ market_state['kalman'].update(mid)
185
 
186
+ # 3. Calculate Micro-Structure Features
187
+ micro_price = calculate_weighted_micro_price(mid)
188
+ vwap = calculate_vwap_1m()
 
 
 
 
 
 
 
 
 
 
189
 
190
+ # 4. Calculate Order Flow Imbalance (OFI) - Last 10 seconds
191
+ ofi_buy = 0.0
192
+ ofi_sell = 0.0
193
+ ofi_window = 10.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
+ for t in reversed(market_state['trade_history']):
196
+ if t['t'] < (now - ofi_window): break
197
+ if t['side'] == 'buy': ofi_buy += t['q']
198
+ else: ofi_sell += t['q']
199
 
200
+ net_ofi = ofi_buy - ofi_sell
201
+ total_vol_10s = ofi_buy + ofi_sell
202
+
203
+ # 5. PREDICTION ENGINE (The Math Part)
 
 
 
 
 
 
 
 
 
 
 
204
 
205
+ # A. Kyle's Impact Term
206
+ # How much should the price move based on recent net buying/selling?
207
+ # Impact = Net_Volume * Lambda
208
+ k_lambda = calculate_kyle_lambda(volatility, total_vol_10s)
209
+ impact_term = net_ofi * k_lambda
210
+
211
+ # B. Mean Reversion Term (Ornstein-Uhlenbeck proxy)
212
+ # Prices tend to revert to VWAP in the short term.
213
+ # Pull = (VWAP - Price) * alpha
214
+ mean_reversion_alpha = 0.1 # Reversion strength coefficient
215
+ reversion_term = (vwap - mid) * mean_reversion_alpha
216
+
217
+ # C. Micro-Price Alpha
218
+ # The divergence between MicroPrice and MidPrice predicts immediate tick direction.
219
+ micro_alpha = (micro_price - mid) * 0.8 # 0.8 is a sensitivity weight
220
+
221
+ # D. Trend Term (Kalman)
222
+ # Project current velocity 60 seconds out
223
+ trend_term = market_state['kalman'].v * 60.0
224
+
225
+ # TOTAL PREDICTED CHANGE (Delta)
226
+ # We combine Market Impact (OFI), Micro-structure pressure (MP), Trend, and Mean Reversion.
227
+ predicted_delta = impact_term + micro_alpha + trend_term + reversion_term
228
 
229
+ pred_close = mid + predicted_delta
230
+
231
+ # Calculate Confidence Interval (2 Sigma) for the ghost candle
232
+ # Volatility scales with square root of time
233
+ sigma_1m = volatility * math.sqrt(60)
234
 
235
  pred_candle = {
236
+ 'time': int(now) + 60,
237
  'open': mid,
238
  'close': pred_close,
239
+ 'high': max(mid, pred_close) + (2 * sigma_1m),
240
+ 'low': min(mid, pred_close) - (2 * sigma_1m)
241
  }
242
 
243
+ # Data management for charts
244
+ if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
245
+ market_state['history'].append({'t': now, 'p': mid})
246
+ if len(market_state['history']) > HISTORY_LENGTH:
247
+ market_state['history'].pop(0)
248
+
249
+ # Calculate Depth Chart Arrays
250
+ bids = sorted(market_state['bids'].items(), reverse=True)[:100]
251
+ asks = sorted(market_state['asks'].items())[:100]
252
+ depth_x, depth_net, depth_bids, depth_asks = [], [], [], []
253
+
254
+ if bids and asks:
255
+ center_price = mid
256
+ for i in range(min(len(bids), len(asks))):
257
+ dist = (asks[i][0] - bids[i][0]) / 2
258
+ p_level = dist
259
+ depth_x.append(p_level)
260
+ depth_bids.append(bids[i][1])
261
+ depth_asks.append(asks[i][1])
262
+ depth_net.append(bids[i][1] - asks[i][1])
263
+
264
+ # Analysis Object for Frontend
265
+ analysis = {
266
+ "projected": pred_close,
267
+ "rho": (micro_price - mid), # Storing MP divergence as 'rho'
268
+ "vwap": vwap,
269
+ "lambda": k_lambda
270
+ }
271
 
272
  return {
273
  "mid": mid,
274
  "history": market_state['history'],
 
 
275
  "ohlc": market_state['ohlc_history'],
276
+ "trade_history": [], # Reduced payload, handled by client cumulative logic
277
+ "pred_candle": pred_candle,
278
+ "depth_x": depth_x,
279
+ "depth_net": depth_net,
280
+ "depth_bids": depth_bids,
281
+ "depth_asks": depth_asks,
282
  "analysis": analysis,
283
+ "walls": {"bids": [], "asks": []} # Removed legacy walls for cleaner math view
284
  }
285
 
286
+ # --- HTML FRONTEND (Unchanged visual structure, updated data mapping) ---
287
  HTML_PAGE = f"""
288
  <!DOCTYPE html>
289
  <html lang="en">
290
  <head>
291
  <meta charset="UTF-8">
292
+ <title>{SYMBOL_KRAKEN} Quant</title>
293
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
294
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
295
  <style>
296
+ :root {{ --bg-base: #000000; --bg-panel: #0a0a0a; --border: #252525; --text-main: #FFFFFF; --text-dim: #999999; --green: #00ff9d; --red: #ff3b3b; --purple: #d500f9; }}
297
+ 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; }}
298
+ .layout {{ display: grid; grid-template-rows: 34px 1fr 1fr; grid-template-columns: 3fr 1fr; gap: 1px; background-color: var(--border); height: 100vh; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  .panel {{ background: var(--bg-panel); display: flex; flex-direction: column; overflow: hidden; }}
300
+ .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; border-bottom: 1px solid var(--border); }}
301
+ .live-dot {{ width: 8px; height: 8px; background-color: var(--green); border-radius: 50%; display: inline-block; margin-right: 8px; box-shadow: 0 0 8px var(--green); }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  #p-chart {{ grid-column: 1 / 2; grid-row: 2 / 3; }}
303
+ #p-bottom {{ grid-column: 1 / 2; grid-row: 3 / 4; display: grid; grid-template-columns: 1fr; gap: 1px; background: var(--border); }}
304
+ #p-sidebar {{ grid-column: 2 / 3; grid-row: 2 / 4; padding: 15px; display: flex; flex-direction: column; gap: 20px; border-left: 1px solid var(--border); }}
305
+ .chart-header {{ 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; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  .data-group {{ display: flex; flex-direction: column; gap: 4px; }}
307
+ .label {{ font-size: 10px; color: var(--text-dim); font-weight: 600; text-transform: uppercase; }}
308
  .value {{ font-family: 'JetBrains Mono', monospace; font-size: 20px; font-weight: 700; color: #fff; }}
309
  .value-lg {{ font-size: 26px; }}
310
  .value-sub {{ font-family: 'JetBrains Mono', monospace; font-size: 11px; margin-top: 2px; color: #666; }}
 
311
  .divider {{ height: 1px; background: var(--border); width: 100%; }}
 
 
 
312
  .c-purple {{ color: var(--purple); }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  </style>
314
  </head>
315
  <body>
 
316
  <div class="layout">
317
  <div class="status-bar">
318
+ <div><span class="live-dot"></span><span style="font-weight:700;">{SYMBOL_KRAKEN} MATH MODEL</span></div>
319
+ <div id="price-ticker">---</div>
 
 
 
 
320
  </div>
 
321
  <div id="p-chart" class="panel">
322
+ <div class="chart-header">PRICE (BLUE) vs PREDICTION (YELLOW) vs VWAP (WHITE)</div>
323
+ <div id="tv-price" style="flex: 1;"></div>
324
  </div>
325
+ <div id="p-bottom" class="panel">
326
+ <div class="chart-header">1M KLINE + GHOST PREDICTION (PURPLE)</div>
327
+ <div id="tv-candles" style="flex: 1;"></div>
 
 
 
 
 
 
 
328
  </div>
 
329
  <div id="p-sidebar" class="panel">
 
330
  <div class="data-group">
331
+ <span class="label">Predicted Close (1m)</span>
332
  <div style="display:flex; align-items: baseline; gap: 10px;">
333
  <span id="proj-pct" class="value value-lg">--%</span>
334
  <span id="proj-val" class="value-sub c-purple">---</span>
335
  </div>
336
+ <span class="label" style="margin-top:4px;">MicroPrice + OFI Impact</span>
337
  </div>
 
338
  <div class="divider"></div>
 
339
  <div class="data-group">
340
+ <span class="label">VWAP Divergence</span>
341
+ <span id="vwap-div" class="value">0.00</span>
342
  </div>
 
343
  <div class="divider"></div>
 
344
  <div class="data-group">
345
+ <span class="label">Kyle's Lambda (Liq. Cost)</span>
346
+ <span id="lambda-val" class="value">0.00</span>
347
+ <span class="value-sub">Impact per Volume Unit</span>
 
 
 
 
 
 
 
 
 
 
 
348
  </div>
349
  </div>
350
  </div>
 
351
  <script>
352
+ const dom = {{ ticker: document.getElementById('price-ticker'), projVal: document.getElementById('proj-val'), projPct: document.getElementById('proj-pct'), vwapDiv: document.getElementById('vwap-div'), lambdaVal: document.getElementById('lambda-val') }};
353
+ const chartOpts = {{ layout: {{ background: {{ type: 'solid', color: '#0a0a0a' }}, textColor: '#888', fontFamily: 'JetBrains Mono' }}, grid: {{ vertLines: {{ color: '#151515' }}, horzLines: {{ color: '#151515' }} }}, crosshair: {{ mode: 1 }} }};
354
+
355
+ const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts);
356
+ const priceSeries = priceChart.addLineSeries({{ color: '#2979ff', lineWidth: 2 }});
357
+ const predSeries = priceChart.addLineSeries({{ color: '#ffeb3b', lineWidth: 2, lineStyle: 2 }});
358
+ const vwapSeries = priceChart.addLineSeries({{ color: 'rgba(255,255,255,0.3)', lineWidth: 1, lineStyle: 0 }});
359
+
360
+ const candleChart = LightweightCharts.createChart(document.getElementById('tv-candles'), chartOpts);
361
+ const candleSeries = candleChart.addCandlestickSeries({{ upColor: '#00ff9d', downColor: '#ff3b3b', borderVisible: false }});
362
+ const ghostSeries = candleChart.addCandlestickSeries({{ upColor: 'rgba(213, 0, 249, 0.5)', downColor: 'rgba(213, 0, 249, 0.5)', borderVisible: true, borderColor: '#d500f9' }});
363
+
364
+ new ResizeObserver(e => {{ priceChart.timeScale().fitContent(); }}).observe(document.body);
365
+
366
+ function connect() {{
367
+ const ws = new WebSocket((location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/ws');
368
+ ws.onmessage = (e) => {{
369
+ const data = JSON.parse(e.data);
370
+ if (data.error) return;
371
+
372
+ if (data.history.length) {{
373
+ const hist = data.history.map(d => ({{ time: Math.floor(d.t), value: d.p }}));
374
+ const cleanHist = [...new Map(hist.map(i => [i.time, i])).values()];
375
+ priceSeries.setData(cleanHist);
376
+ dom.ticker.innerText = cleanHist[cleanHist.length-1].value.toFixed(2);
377
+
378
+ if(data.analysis) {{
379
+ // Plot Prediction Line
380
+ predSeries.setData([
381
+ cleanHist[cleanHist.length-1],
382
+ {{ time: cleanHist[cleanHist.length-1].time + 60, value: data.analysis.projected }}
383
+ ]);
384
+
385
+ // Plot VWAP Line (simple point for now)
386
+ vwapSeries.setData([
387
+ {{ time: cleanHist[0].time, value: data.analysis.vwap }},
388
+ {{ time: cleanHist[cleanHist.length-1].time, value: data.analysis.vwap }}
389
+ ]);
390
+
391
+ // Update Sidebar
392
+ dom.lambdaVal.innerText = (data.analysis.lambda * 1000).toFixed(4); // Scaled for display
393
+ const vwapDiff = cleanHist[cleanHist.length-1].value - data.analysis.vwap;
394
+ dom.vwapDiv.innerText = vwapDiff.toFixed(2);
395
+ dom.vwapDiv.style.color = vwapDiff > 0 ? '#ff3b3b' : '#00ff9d'; // Red if price > vwap (overbought)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  }}
397
+ }}
398
 
399
+ if (data.ohlc && data.ohlc.length) {{
400
+ candleSeries.setData(data.ohlc.map(c => ({{ time: c.time, open: c.open, high: c.high, low: c.low, close: c.close }})));
401
+ }}
 
 
 
 
 
 
 
402
 
403
+ if (data.pred_candle) {{
404
+ ghostSeries.setData([data.pred_candle]);
405
+ const currentP = parseFloat(dom.ticker.innerText);
406
+ const pClose = data.pred_candle.close;
407
+ dom.projVal.innerText = pClose.toFixed(2);
408
+ const pct = ((pClose - currentP) / currentP) * 100;
409
+ dom.projPct.innerText = (pct >= 0 ? "+" : "") + pct.toFixed(3) + "%";
410
+ dom.projPct.style.color = pct >= 0 ? "var(--green)" : "var(--red)";
411
+ }}
412
+ }};
413
+ ws.onclose = () => setTimeout(connect, 2000);
414
+ }}
415
+ connect();
 
 
 
 
416
  </script>
417
  </body>
418
  </html>
 
421
  async def kraken_worker():
422
  global market_state
423
 
424
+ # Initial fetch for OHLC
425
  try:
426
  async with aiohttp.ClientSession() as session:
427
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
 
429
  if response.status == 200:
430
  data = await response.json()
431
  if 'result' in data:
432
+ # Extract OHLC
433
+ raw = list(data['result'].values())[0]
434
+ market_state['ohlc_history'] = [
435
+ {'time': int(c[0]), 'open': float(c[1]), 'high': float(c[2]), 'low': float(c[3]), 'close': float(c[4])}
436
+ for c in raw[-120:]
437
+ ]
 
 
 
 
 
 
 
 
438
  except Exception as e:
439
+ logging.error(f"Init Error: {e}")
440
 
441
  while True:
442
  try:
443
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
444
  logging.info(f"🔌 Connected to Kraken ({SYMBOL_KRAKEN})")
445
 
446
+ await ws.send(json.dumps({"method": "subscribe", "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 100}}))
447
+ await ws.send(json.dumps({"method": "subscribe", "params": {"channel": "trade", "symbol": [SYMBOL_KRAKEN]}}))
448
+ await ws.send(json.dumps({"method": "subscribe", "params": {"channel": "ohlc", "symbol": [SYMBOL_KRAKEN], "interval": 1}}))
 
 
 
 
 
 
 
 
 
449
 
450
  async for message in ws:
451
  payload = json.loads(message)
 
455
  if channel == "book":
456
  for item in data:
457
  for bid in item.get('bids', []):
458
+ market_state['bids'][float(bid['price'])] = float(bid['qty'])
 
 
459
  for ask in item.get('asks', []):
460
+ market_state['asks'][float(ask['price'])] = float(ask['qty'])
 
 
461
 
462
+ # Cleanup zero qty
463
+ market_state['bids'] = {k: v for k, v in market_state['bids'].items() if v > 0}
464
+ market_state['asks'] = {k: v for k, v in market_state['asks'].items() if v > 0}
465
+
466
  if market_state['bids'] and market_state['asks']:
467
  best_bid = max(market_state['bids'].keys())
468
  best_ask = min(market_state['asks'].keys())
469
+ market_state['current_mid'] = (best_bid + best_ask) / 2
 
 
470
  market_state['ready'] = True
471
+
 
 
 
 
 
 
472
  elif channel == "trade":
473
  for trade in data:
474
  try:
475
+ t_obj = {
476
+ 't': time.time(),
477
+ 'p': float(trade['price']),
478
+ 'q': float(trade['qty']),
479
+ 'side': trade['side']
480
+ }
481
+ market_state['trade_history'].append(t_obj)
482
  except: pass
483
 
484
  elif channel == "ohlc":
485
+ # Update OHLC array
486
+ for c in data:
487
+ c_data = {
488
+ 'time': int(float(c['endtime'])),
489
+ 'open': float(c['open']),
490
+ 'high': float(c['high']),
491
+ 'low': float(c['low']),
492
+ 'close': float(c['close'])
493
+ }
494
+ if market_state['ohlc_history'] and market_state['ohlc_history'][-1]['time'] == c_data['time']:
495
+ market_state['ohlc_history'][-1] = c_data
496
+ else:
497
+ market_state['ohlc_history'].append(c_data)
498
+ if len(market_state['ohlc_history']) > 100: market_state['ohlc_history'].pop(0)
 
 
 
499
 
500
  except Exception as e:
501
+ logging.warning(f"Reconnecting: {e}")
502
+ await asyncio.sleep(2)
503
 
504
  async def broadcast_worker():
505
  while True:
506
  if connected_clients and market_state['ready']:
507
  payload = process_market_data()
508
+ if "error" not in payload:
509
+ msg = json.dumps(payload)
510
+ for ws in list(connected_clients):
511
+ try: await ws.send_str(msg)
512
+ except: pass
513
  await asyncio.sleep(BROADCAST_RATE)
514
 
515
  async def websocket_handler(request):
516
  ws = web.WebSocketResponse()
517
  await ws.prepare(request)
518
  connected_clients.add(ws)
519
+ try: async for msg in ws: pass
520
+ finally: connected_clients.remove(ws)
 
 
 
521
  return ws
522
 
523
  async def handle_index(request):
 
530
  async def cleanup_background(app):
531
  app['kraken_task'].cancel()
532
  app['broadcast_task'].cancel()
 
 
533
 
534
  async def main():
535
  app = web.Application()