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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +399 -176
app.py CHANGED
@@ -6,29 +6,24 @@ import bisect
6
  import math
7
  import statistics
8
  import aiohttp
 
9
  from aiohttp import web
10
  import websockets
11
 
12
- # --- CONFIGURATION ---
13
- SYMBOL_KRAKEN = "BTC/USD" # WebSocket Symbol
14
- API_PAIR = "XBTUSD" # REST API Pair
15
  PORT = 7860
16
  HISTORY_LENGTH = 300
17
  BROADCAST_RATE = 0.1
18
 
19
- # Quant Parameters
20
  DECAY_LAMBDA = 50.0
21
- IMPACT_SENSITIVITY = 1.5
22
  Z_SCORE_THRESHOLD = 3.0
23
  WALL_LOOKBACK = 200
24
- SQRT_IMPACT_K = 0.5
25
 
26
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
27
 
28
- # --- MATHEMATICAL CLASSES ---
29
-
30
  class KalmanFilter:
31
- def __init__(self, process_noise=1e-5, measurement_noise=1e-3):
32
  self.x = 0.0
33
  self.v = 0.0
34
  self.P = 1.0
@@ -38,7 +33,6 @@ class KalmanFilter:
38
  self.last_time = time.time()
39
 
40
  def update(self, measurement):
41
- if measurement == 0: return
42
  now = time.time()
43
  dt = now - self.last_time
44
  self.last_time = now
@@ -48,37 +42,34 @@ class KalmanFilter:
48
  self.first_run = False
49
  return
50
 
 
 
51
  pred_x = self.x + self.v * dt
52
  pred_v = self.v
53
- self.P = self.P + self.Q
54
 
55
- residual = measurement - pred_x
56
- S = self.P + self.R
57
- K = self.P / S
 
 
 
 
 
 
 
 
58
 
59
- self.x = pred_x + K * residual
60
- self.v = pred_v + (K / dt) * residual * 0.1
61
- self.P = (1 - K) * self.P
62
-
63
- class VolatilityModel:
64
- def __init__(self, decay=0.94):
65
- self.variance = 0.0
66
- self.decay = decay
67
- self.initialized = False
68
-
69
- def update(self, high, low):
70
- if high == 0 or low == 0: return
71
- range_sq = (high - low) ** 2
72
- if not self.initialized:
73
- self.variance = range_sq
74
- self.initialized = True
75
- else:
76
- self.variance = (self.decay * self.variance) + ((1 - self.decay) * range_sq)
77
-
78
- def get_sigma(self):
79
- return math.sqrt(self.variance)
80
-
81
- # --- GLOBAL STATE ---
82
  market_state = {
83
  "bids": {},
84
  "asks": {},
@@ -86,18 +77,16 @@ market_state = {
86
  "pred_history": [],
87
  "trade_vol_history": [],
88
  "ohlc_history": [],
89
- "current_vol_window": {"buy": 0.0, "sell": 0.0, "start": time.time()},
90
  "current_mid": 0.0,
91
  "ready": False,
92
  "kalman": KalmanFilter(),
93
- "vol_model": VolatilityModel(),
94
- "avg_minute_volume": 1.0
95
  }
96
 
97
  connected_clients = set()
98
 
99
- # --- ANALYTICS ---
100
-
101
  def detect_anomalies(orders, scan_depth):
102
  if len(orders) < 10: return []
103
  relevant_orders = orders[:scan_depth]
@@ -121,89 +110,79 @@ def detect_anomalies(orders, scan_depth):
121
  walls.sort(key=lambda x: x['z_score'], reverse=True)
122
  return walls[:3]
123
 
124
- def calculate_book_imbalance(diff_x, diff_y_net):
125
- if not diff_x or len(diff_x) < 5: return 0.0
 
126
  weighted_imbalance = 0.0
127
  total_weight = 0.0
 
128
  for i in range(len(diff_x)):
129
  dist = diff_x[i]
130
  net_vol = diff_y_net[i]
131
  weight = math.exp(-dist / DECAY_LAMBDA)
132
  weighted_imbalance += net_vol * weight
133
  total_weight += weight
134
- if total_weight == 0: return 0.0
135
- return weighted_imbalance / total_weight
136
-
137
- def predict_next_candle(current_mid, kf_velocity, book_rho, net_trade_vol, avg_vol, sigma, walls):
138
- trend_impact = kf_velocity * 60.0
139
- book_impact = book_rho * 100 * IMPACT_SENSITIVITY
140
-
141
- denom = max(avg_vol, 0.1)
142
- trade_impact_raw = sigma * math.sqrt(abs(net_trade_vol) / denom)
143
- trade_impact = trade_impact_raw * (1 if net_trade_vol >= 0 else -1) * SQRT_IMPACT_K
144
-
145
- total_drift = trend_impact + book_impact + trade_impact
146
- pred_close = current_mid + total_drift
147
 
148
- range_expansion = sigma * 1.0
149
 
150
- pred_high = max(current_mid, pred_close) + (range_expansion * 0.5)
151
- pred_low = min(current_mid, pred_close) - (range_expansion * 0.5)
 
152
 
153
- if walls['asks']:
 
154
  nearest_wall = walls['asks'][0]
155
- if pred_high > nearest_wall['price']:
156
- pred_high -= (pred_high - nearest_wall['price']) * 0.8
157
- if pred_close > nearest_wall['price']:
158
- pred_close -= (pred_close - nearest_wall['price']) * 0.8
159
-
160
- if walls['bids']:
161
  nearest_wall = walls['bids'][0]
162
- if pred_low < nearest_wall['price']:
163
- pred_low += (nearest_wall['price'] - pred_low) * 0.8
164
- if pred_close < nearest_wall['price']:
165
- pred_close += (nearest_wall['price'] - pred_close) * 0.8
166
 
167
- now_ts = int(time.time())
168
  return {
169
- 'time': now_ts + 60,
170
- 'open': current_mid,
171
- 'high': pred_high,
172
- 'low': pred_low,
173
- 'close': pred_close
174
  }
175
 
176
  def process_market_data():
177
- if not market_state['ready'] or market_state['current_mid'] == 0:
178
- return {"error": "Initializing..."}
179
 
180
  mid = market_state['current_mid']
181
 
182
- # Update Models
183
  market_state['kalman'].update(mid)
184
- if market_state['ohlc_history']:
185
- last_candle = market_state['ohlc_history'][-1]
186
- market_state['vol_model'].update(last_candle['high'], last_candle['low'])
 
 
 
 
 
187
 
188
- # Volume Window
189
  now = time.time()
 
 
190
  if now - market_state['current_vol_window']['start'] >= 1.0:
191
- b_vol = market_state['current_vol_window']['buy']
192
- s_vol = market_state['current_vol_window']['sell']
193
- market_state['trade_vol_history'].append({'t': now, 'buy': b_vol, 'sell': s_vol})
 
 
194
  if len(market_state['trade_vol_history']) > 60:
195
  market_state['trade_vol_history'].pop(0)
196
-
197
- total_recent_vol = sum(x['buy'] + x['sell'] for x in market_state['trade_vol_history'])
198
- market_state['avg_minute_volume'] = max(total_recent_vol, 1.0)
199
  market_state['current_vol_window'] = {"buy": 0.0, "sell": 0.0, "start": now}
200
 
201
- # Order Book
202
  sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
203
  sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
204
 
205
  if not sorted_bids or not sorted_asks: return {"error": "Empty Book"}
206
 
 
 
 
207
  bid_walls = detect_anomalies(sorted_bids, WALL_LOOKBACK)
208
  ask_walls = detect_anomalies(sorted_asks, WALL_LOOKBACK)
209
 
@@ -228,39 +207,59 @@ def process_market_data():
228
  max_dist = min(d_b_x[-1], d_a_x[-1])
229
  step_size = max_dist / 100
230
  steps = [i * step_size for i in range(1, 101)]
 
231
  for s in steps:
232
  idx_b = bisect.bisect_right(d_b_x, s)
233
  vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
234
  idx_a = bisect.bisect_right(d_a_x, s)
235
  vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
 
236
  diff_x.append(s)
237
  diff_y_net.append(vol_b - vol_a)
238
- chart_bids.append(vol_b); chart_asks.append(vol_a)
239
-
240
- # Predictions
241
- rho = calculate_book_imbalance(diff_x, diff_y_net)
242
- recent_net_vol = sum(x['buy'] - x['sell'] for x in market_state['trade_vol_history'])
243
- sigma = market_state['vol_model'].get_sigma()
244
- if sigma == 0: sigma = 10.0
245
-
246
- pred_candle = predict_next_candle(
247
- current_mid=mid,
248
- kf_velocity=market_state['kalman'].v,
249
- book_rho=rho,
250
- net_trade_vol=recent_net_vol,
251
- avg_vol=market_state['avg_minute_volume'],
252
- sigma=sigma,
253
- walls={"bids": bid_walls, "asks": ask_walls}
254
- )
255
 
256
- analysis = { "projected": pred_candle['close'], "rho": rho }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  return {
259
  "mid": mid,
260
  "history": market_state['history'],
 
261
  "trade_history": market_state['trade_vol_history'],
262
  "ohlc": market_state['ohlc_history'],
263
- "pred_candle": pred_candle,
264
  "depth_x": diff_x,
265
  "depth_net": diff_y_net,
266
  "depth_bids": chart_bids,
@@ -274,7 +273,7 @@ HTML_PAGE = f"""
274
  <html lang="en">
275
  <head>
276
  <meta charset="UTF-8">
277
- <title>{SYMBOL_KRAKEN} | Quant Terminal</title>
278
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
279
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
280
  <style>
@@ -290,46 +289,135 @@ HTML_PAGE = f"""
290
  --yellow: #ffeb3b;
291
  --purple: #d500f9;
292
  }}
293
- 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; }}
294
- .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; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  .panel {{ background: var(--bg-panel); display: flex; flex-direction: column; overflow: hidden; }}
296
- .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; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  .status-left {{ display: flex; gap: 20px; align-items: center; }}
298
  .live-dot {{ width: 8px; height: 8px; background-color: var(--green); border-radius: 50%; display: inline-block; box-shadow: 0 0 8px var(--green); }}
299
  .ticker-val {{ font-weight: 700; color: #fff; font-size: 13px; }}
 
300
  #p-chart {{ grid-column: 1 / 2; grid-row: 2 / 3; }}
301
- #p-bottom {{ grid-column: 1 / 2; grid-row: 3 / 4; display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); }}
 
 
 
 
 
 
 
302
  .bottom-sub {{ background: var(--bg-panel); display: flex; flex-direction: column; position: relative; }}
303
- #p-sidebar {{ grid-column: 2 / 3; grid-row: 2 / 4; padding: 15px; display: flex; flex-direction: column; gap: 15px; border-left: 1px solid var(--border); overflow: hidden; }}
304
- .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; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  .data-group {{ display: flex; flex-direction: column; gap: 4px; }}
306
  .label {{ font-size: 10px; color: var(--text-dim); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }}
307
  .value {{ font-family: 'JetBrains Mono', monospace; font-size: 20px; font-weight: 700; color: #fff; }}
308
  .value-lg {{ font-size: 26px; }}
309
  .value-sub {{ font-family: 'JetBrains Mono', monospace; font-size: 11px; margin-top: 2px; color: #666; }}
 
310
  .divider {{ height: 1px; background: var(--border); width: 100%; }}
311
  .c-green {{ color: var(--green); }}
312
  .c-red {{ color: var(--red); }}
313
  .c-dim {{ color: var(--text-dim); }}
314
  .c-purple {{ color: var(--purple); }}
 
315
  .list-container {{ display: flex; flex-direction: column; gap: 8px; overflow-y: auto; height: 100px; }}
316
- .list-item {{ display: flex; justify-content: space-between; font-family: 'JetBrains Mono', monospace; font-size: 11px; border-bottom: 1px solid #151515; padding-bottom: 4px; }}
 
 
 
 
 
 
317
  .list-item span:first-child {{ color: #e0e0e0; }}
318
  .list-item:last-child {{ border: none; }}
319
- .sidebar-chart-box {{ flex: 1; display: flex; flex-direction: column; min-height: 0; }}
320
- .mini-chart {{ flex: 1; background: rgba(255,255,255,0.02); border: 1px solid var(--border); border-radius: 4px; }}
 
 
 
 
 
 
 
 
 
 
 
321
  </style>
322
  </head>
323
  <body>
 
324
  <div class="layout">
325
  <div class="status-bar">
326
- <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">CONNECTING...</span></div>
 
 
 
 
327
  <div class="status-right" id="clock">00:00:00 UTC</div>
328
  </div>
 
329
  <div id="p-chart" class="panel">
330
- <div class="chart-header">PRICE ACTION (BLUE)</div>
331
  <div id="tv-price" style="flex: 1; width: 100%;"></div>
332
  </div>
 
333
  <div id="p-bottom">
334
  <div class="bottom-sub">
335
  <div class="chart-header">1M KLINE + GHOST PREDICTION (PURPLE)</div>
@@ -340,100 +428,218 @@ HTML_PAGE = f"""
340
  <div id="tv-net" style="flex: 1; width: 100%;"></div>
341
  </div>
342
  </div>
 
343
  <div id="p-sidebar" class="panel">
 
344
  <div class="data-group">
345
- <span class="label">Next 1M Candle Prediction</span>
346
  <div style="display:flex; align-items: baseline; gap: 10px;">
347
  <span id="proj-pct" class="value value-lg">--%</span>
348
  <span id="proj-val" class="value-sub c-purple">---</span>
349
  </div>
350
- <span class="label" style="margin-top:4px;">Kalman + SqRoot Impact + OFI</span>
351
  </div>
 
352
  <div class="divider"></div>
353
- <div class="data-group"><span class="label">OFI Imbalance Ratio</span><span id="score-val" class="value">0.00</span></div>
 
 
 
 
 
354
  <div class="divider"></div>
355
- <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>
356
- <div class="sidebar-chart-box"><span class="label" style="margin-bottom:4px;">Real-time Volume Ticks</span><div id="sidebar-vol" class="mini-chart"></div></div>
357
- <div class="sidebar-chart-box"><span class="label" style="margin-bottom:4px;">Liquidity Density</span><div id="sidebar-density" class="mini-chart"></div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  </div>
359
  </div>
 
360
  <script>
361
- setInterval(() => {{ const now = new Date(); document.getElementById('clock').innerText = now.toISOString().split('T')[1].split('.')[0] + ' UTC'; }}, 1000);
 
 
 
 
362
  document.addEventListener('DOMContentLoaded', () => {{
363
- 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') }};
364
- 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' }} }} }};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts);
366
- const priceSeries = priceChart.addLineSeries({{ color: '#2979ff', lineWidth: 2, title: 'Price' }});
367
-
368
- const candleChart = LightweightCharts.createChart(document.getElementById('tv-candles'), {{ ...chartOpts, timeScale: {{ timeVisible: true, secondsVisible: false }} }});
369
- const candleSeries = candleChart.addCandlestickSeries({{ upColor: '#00ff9d', downColor: '#ff3b3b', borderVisible: false, wickUpColor: '#00ff9d', wickDownColor: '#ff3b3b' }});
370
- const ghostSeries = candleChart.addCandlestickSeries({{ upColor: 'rgba(213, 0, 249, 0.5)', downColor: 'rgba(213, 0, 249, 0.5)', borderVisible: true, borderColor: '#d500f9', wickUpColor: '#d500f9', wickDownColor: '#d500f9' }});
371
-
372
- const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{ ...chartOpts, localization: {{ timeFormatter: t => '$' + t.toFixed(2) }} }});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  const netSeries = netChart.addHistogramSeries({{ color: '#2979ff' }});
374
-
375
- const volChart = LightweightCharts.createChart(document.getElementById('sidebar-vol'), {{ ...chartOpts, grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }}, rightPriceScale: {{ visible: false }}, timeScale: {{ visible: false }} }});
 
 
 
 
 
 
376
  const volBuySeries = volChart.addHistogramSeries({{ color: '#00ff9d' }});
377
  const volSellSeries = volChart.addHistogramSeries({{ color: '#ff3b3b' }});
378
-
379
- const denChart = LightweightCharts.createChart(document.getElementById('sidebar-density'), {{ ...chartOpts, grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }}, rightPriceScale: {{ visible: false }}, timeScale: {{ visible: false }} }});
 
 
 
 
 
 
380
  const bidSeries = denChart.addAreaSeries({{ lineColor: '#00ff9d', topColor: 'rgba(0, 255, 157, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
381
  const askSeries = denChart.addAreaSeries({{ lineColor: '#ff3b3b', topColor: 'rgba(255, 59, 59, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
382
-
383
  let activeLines = [];
384
- new ResizeObserver(entries => {{ entries.forEach(e => {{ const id = e.target.id; const {{width, height}} = e.contentRect; if(id==='tv-price') priceChart.applyOptions({{width, height}}); if(id==='tv-candles') candleChart.applyOptions({{width, height}}); if(id==='tv-net') netChart.applyOptions({{width, height}}); if(id==='sidebar-vol') volChart.applyOptions({{width, height}}); if(id==='sidebar-density') denChart.applyOptions({{width, height}}); }}); }}).observe(document.body);
385
- ['tv-price', 'tv-candles', 'tv-net', 'sidebar-vol', 'sidebar-density'].forEach(id => {{ new ResizeObserver(e => {{ const t = document.getElementById(id); if (t.clientWidth) {{ if(id==='tv-price') priceChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }}); if(id==='tv-candles') candleChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }}); if(id==='tv-net') netChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }}); if(id==='sidebar-vol') volChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }}); if(id==='sidebar-density') denChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }}); }} }}).observe(document.getElementById(id)); }});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
 
387
  function connect() {{
388
  const ws = new WebSocket((location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/ws');
 
389
  ws.onmessage = (e) => {{
390
  const data = JSON.parse(e.data);
391
  if (data.error) return;
392
- if (data.history && data.history.length) {{
 
393
  const hist = data.history.map(d => ({{ time: Math.floor(d.t), value: d.p }}));
394
  const cleanHist = [...new Map(hist.map(i => [i.time, i])).values()];
395
  priceSeries.setData(cleanHist);
 
396
  const lastP = cleanHist[cleanHist.length-1].value;
397
  dom.ticker.innerText = lastP.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
 
398
  if (data.analysis) {{
399
- const rho = data.analysis.rho;
 
 
 
 
 
 
 
400
  dom.score.innerText = rho.toFixed(3);
401
  dom.score.style.color = rho > 0 ? "var(--green)" : (rho < 0 ? "var(--red)" : "var(--text-main)");
402
  }}
403
  }}
 
404
  if (data.ohlc && data.ohlc.length) {{
405
- const candles = data.ohlc.map(c => ({{ time: c.time, open: c.open, high: c.high, low: c.low, close: c.close }}));
406
- candleSeries.setData(candles);
 
 
 
407
  }}
 
 
408
  if (data.pred_candle) {{
409
  ghostSeries.setData([data.pred_candle]);
410
  const pClose = data.pred_candle.close;
411
  dom.projVal.innerText = pClose.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
412
- const currentP = parseFloat(dom.ticker.innerText.replace(/,/g, '') || 0);
413
- if(currentP > 0) {{
414
- const pct = ((pClose - currentP) / currentP) * 100;
415
- dom.projPct.innerText = (pct >= 0 ? "+" : "") + pct.toFixed(4) + "%";
416
- dom.projPct.style.color = pct >= 0 ? "var(--green)" : "var(--red)";
417
- }}
 
418
  }}
 
419
  if (data.walls) {{
420
- activeLines.forEach(l => priceSeries.removePriceLine(l)); activeLines = [];
 
421
  let html = "";
422
  const addWall = (w, type) => {{
423
  const color = type === 'BID' ? '#00ff9d' : '#ff3b3b';
424
  activeLines.push(priceSeries.createPriceLine({{ price: w.price, color: color, lineWidth: 1, lineStyle: 2, axisLabelVisible: false }}));
425
  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>`;
426
  }};
427
- data.walls.asks.forEach(w => addWall(w, 'ASK')); data.walls.bids.forEach(w => addWall(w, 'BID'));
 
428
  dom.wallList.innerHTML = html || '<span class="c-dim" style="font-size:11px">Scanning...</span>';
429
  }}
430
- if (data.trade_history) {{
 
431
  const buyData = [], sellData = [];
432
- data.trade_history.forEach(t => {{ const tm = Math.floor(t.t); buyData.push({{ time: tm, value: t.buy }}); sellData.push({{ time: tm, value: t.sell }}); }});
 
 
 
 
433
  volBuySeries.setData([...new Map(buyData.map(i => [i.time, i])).values()]);
434
  volSellSeries.setData([...new Map(sellData.map(i => [i.time, i])).values()]);
435
  }}
436
- if (data.depth_x) {{
 
437
  const bids = [], asks = [], nets = [];
438
  for(let i=0; i<data.depth_x.length; i++) {{
439
  const t = data.depth_x[i];
@@ -441,7 +647,9 @@ HTML_PAGE = f"""
441
  asks.push({{ time: t, value: data.depth_asks[i] }});
442
  nets.push({{ time: t, value: data.depth_net[i], color: data.depth_net[i] > 0 ? '#00ff9d' : '#ff3b3b' }});
443
  }}
444
- bidSeries.setData(bids); askSeries.setData(asks); netSeries.setData(nets);
 
 
445
  }}
446
  }};
447
  ws.onclose = () => setTimeout(connect, 2000);
@@ -456,12 +664,10 @@ HTML_PAGE = f"""
456
  async def kraken_worker():
457
  global market_state
458
 
459
- # 1. Fetch History with Timeout to prevent hanging
460
  try:
461
  async with aiohttp.ClientSession() as session:
462
- url = f"https://api.kraken.com/0/public/OHLC?pair={API_PAIR}&interval=1"
463
- # 5 second timeout for history fetch
464
- async with session.get(url, timeout=5) as response:
465
  if response.status == 200:
466
  data = await response.json()
467
  if 'result' in data:
@@ -469,23 +675,36 @@ async def kraken_worker():
469
  if key != 'last':
470
  raw_candles = data['result'][key]
471
  market_state['ohlc_history'] = [
472
- { 'time': int(c[0]), 'open': float(c[1]), 'high': float(c[2]), 'low': float(c[3]), 'close': float(c[4]) }
 
 
 
 
 
 
473
  for c in raw_candles[-120:]
474
  ]
475
- logging.info(f"Loaded {len(market_state['ohlc_history'])} historical candles")
476
  break
477
  except Exception as e:
478
- logging.error(f"History fetch failed (Continuing anyway): {e}")
479
 
480
- # 2. Main WebSocket Loop
481
  while True:
482
  try:
483
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
484
  logging.info(f"🔌 Connected to Kraken ({SYMBOL_KRAKEN})")
485
 
486
- await ws.send(json.dumps({"method": "subscribe", "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}}))
487
- await ws.send(json.dumps({"method": "subscribe", "params": {"channel": "trade", "symbol": [SYMBOL_KRAKEN]}}))
488
- await ws.send(json.dumps({"method": "subscribe", "params": {"channel": "ohlc", "symbol": [SYMBOL_KRAKEN], "interval": 1}}))
 
 
 
 
 
 
 
 
 
489
 
490
  async for message in ws:
491
  payload = json.loads(message)
@@ -531,8 +750,10 @@ async def kraken_worker():
531
  try:
532
  c_data = {
533
  'time': int(float(candle['endtime'])),
534
- 'open': float(candle['open']), 'high': float(candle['high']),
535
- 'low': float(candle['low']), 'close': float(candle['close'])
 
 
536
  }
537
  if market_state['ohlc_history'] and market_state['ohlc_history'][-1]['time'] == c_data['time']:
538
  market_state['ohlc_history'][-1] = c_data
@@ -540,7 +761,8 @@ async def kraken_worker():
540
  market_state['ohlc_history'].append(c_data)
541
  if len(market_state['ohlc_history']) > 100:
542
  market_state['ohlc_history'].pop(0)
543
- except: pass
 
544
 
545
  except Exception as e:
546
  logging.warning(f"⚠️ Reconnecting: {e}")
@@ -561,7 +783,8 @@ async def websocket_handler(request):
561
  await ws.prepare(request)
562
  connected_clients.add(ws)
563
  try:
564
- async for msg in ws: pass
 
565
  finally:
566
  connected_clients.remove(ws)
567
  return ws
 
6
  import math
7
  import statistics
8
  import aiohttp
9
+ from datetime import datetime
10
  from aiohttp import web
11
  import websockets
12
 
13
+ SYMBOL_KRAKEN = "BTC/USD"
 
 
14
  PORT = 7860
15
  HISTORY_LENGTH = 300
16
  BROADCAST_RATE = 0.1
17
 
 
18
  DECAY_LAMBDA = 50.0
19
+ IMPACT_SENSITIVITY = 2.0
20
  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
 
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
 
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": {},
 
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]
 
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
 
 
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,
 
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>
 
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>
 
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];
 
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);
 
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"
670
+ async with session.get(url) as response:
 
671
  if response.status == 200:
672
  data = await response.json()
673
  if 'result' in data:
 
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)
 
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
 
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}")
 
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