Alvin3y1 commited on
Commit
5cd90b2
Β·
verified Β·
1 Parent(s): 1533a4b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +50 -678
app.py CHANGED
@@ -2,329 +2,29 @@ import asyncio
2
  import json
3
  import logging
4
  import time
5
- import bisect
6
- import math
7
- import statistics
8
  import aiohttp
9
  from aiohttp import web
10
  import websockets
11
 
12
  SYMBOL_KRAKEN = "BTC/USD"
13
  PORT = 7860
14
- HISTORY_LENGTH = 300
15
- BROADCAST_RATE = 0.1
16
-
17
- DECAY_LAMBDA = 50.0
18
- IMPACT_SENSITIVITY = 2.0
19
- Z_SCORE_THRESHOLD = 3.0
20
- WALL_LOOKBACK = 200
21
-
22
- # ML Hyperparameters
23
- LEARNING_RATE = 0.01
24
- MOMENTUM = 0.9
25
 
26
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
27
 
28
- class OnlineScaler:
29
- def __init__(self):
30
- self.n = 0
31
- self.mean = 0.0
32
- self.M2 = 0.0
33
-
34
- def update(self, x):
35
- self.n += 1
36
- delta = x - self.mean
37
- self.mean += delta / self.n
38
- delta2 = x - self.mean
39
- self.M2 += delta * delta2
40
- return self.transform(x)
41
-
42
- def transform(self, x):
43
- if self.n < 2: return 0.0
44
- var = self.M2 / (self.n - 1)
45
- if var == 0: return 0.0
46
- std = math.sqrt(var)
47
- return (x - self.mean) / std
48
-
49
- class QuantModel:
50
- def __init__(self, num_features):
51
- self.weights = [0.0] * num_features
52
- self.bias = 0.0
53
- self.velocity = [0.0] * num_features
54
- self.bias_velocity = 0.0
55
- self.scalers = [OnlineScaler() for _ in range(num_features)]
56
- self.prev_features = None
57
- self.prev_price = None
58
-
59
- def predict(self, features):
60
- scaled = [s.transform(f) for s, f in zip(self.scalers, features)]
61
- dot = sum(w * x for w, x in zip(self.weights, scaled))
62
- return dot + self.bias
63
-
64
- def train(self, current_price, current_features):
65
- if self.prev_features is None or self.prev_price is None:
66
- self.prev_features = [s.update(f) for s, f in zip(self.scalers, current_features)]
67
- self.prev_price = current_price
68
- return
69
-
70
- # Target: Price Change (Delta)
71
- actual_delta = current_price - self.prev_price
72
-
73
- # Predict using PAST features
74
- pred_delta = sum(w * x for w, x in zip(self.weights, self.prev_features)) + self.bias
75
-
76
- # Error
77
- error = pred_delta - actual_delta
78
-
79
- # SGD with Momentum Update
80
- for i in range(len(self.weights)):
81
- grad = error * self.prev_features[i]
82
- self.velocity[i] = MOMENTUM * self.velocity[i] - LEARNING_RATE * grad
83
- self.weights[i] += self.velocity[i]
84
-
85
- self.bias_velocity = MOMENTUM * self.bias_velocity - LEARNING_RATE * error
86
- self.bias += self.bias_velocity
87
-
88
- # Store for next tick
89
- self.prev_features = [s.update(f) for s, f in zip(self.scalers, current_features)]
90
- self.prev_price = current_price
91
-
92
- def get_forecast(self, current_price, current_features):
93
- # Predict NEXT delta based on CURRENT features
94
- pred_delta = self.predict(current_features)
95
- return current_price + pred_delta
96
-
97
- # 4 Features: OFI, Depth Area, Best Imbalance, Velocity
98
- ml_model = QuantModel(4)
99
-
100
  market_state = {
101
- "bids": {},
102
- "asks": {},
103
- "history": [],
104
- "pred_history": [],
105
- "ml_history": [],
106
- "trade_vol_history": [],
107
  "ohlc_history": [],
108
- "current_vol_window": {"buy": 0.0, "sell": 0.0, "start": time.time()},
109
- "current_mid": 0.0,
110
- "prev_mid": 0.0,
111
  "ready": False
112
  }
113
 
114
  connected_clients = set()
115
 
116
- def detect_anomalies(orders, scan_depth):
117
- if len(orders) < 10: return []
118
- relevant_orders = orders[:scan_depth]
119
- volumes = [q for p, q in relevant_orders]
120
- if not volumes: return []
121
-
122
- try:
123
- avg_vol = statistics.mean(volumes)
124
- stdev_vol = statistics.stdev(volumes)
125
- except statistics.StatisticsError:
126
- return []
127
-
128
- if stdev_vol == 0: return []
129
-
130
- walls = []
131
- for price, qty in relevant_orders:
132
- z_score = (qty - avg_vol) / stdev_vol
133
- if z_score > Z_SCORE_THRESHOLD:
134
- walls.append({"price": price, "vol": qty, "z_score": z_score})
135
-
136
- walls.sort(key=lambda x: x['z_score'], reverse=True)
137
- return walls[:3]
138
-
139
- def calculate_micro_price_structure(diff_x, diff_y_net, current_mid, best_bid, best_ask, walls):
140
- if not diff_x or len(diff_x) < 5: return None, 0
141
-
142
- weighted_imbalance = 0.0
143
- total_weight = 0.0
144
-
145
- for i in range(len(diff_x)):
146
- dist = diff_x[i]
147
- net_vol = diff_y_net[i]
148
- weight = math.exp(-dist / DECAY_LAMBDA)
149
- weighted_imbalance += net_vol * weight
150
- total_weight += weight
151
-
152
- rho = weighted_imbalance / total_weight if total_weight > 0 else 0
153
-
154
- spread = best_ask - best_bid
155
- theoretical_delta = (spread / 2) * rho * IMPACT_SENSITIVITY
156
- projected_price = current_mid + theoretical_delta
157
-
158
- final_delta = theoretical_delta
159
- if final_delta > 0 and walls['asks']:
160
- nearest_wall = walls['asks'][0]
161
- if projected_price >= nearest_wall['price']:
162
- damp_factor = 1.0 / (1.0 + (nearest_wall['z_score'] * 0.2))
163
- final_delta *= damp_factor
164
- elif final_delta < 0 and walls['bids']:
165
- nearest_wall = walls['bids'][0]
166
- if projected_price <= nearest_wall['price']:
167
- damp_factor = 1.0 / (1.0 + (nearest_wall['z_score'] * 0.2))
168
- final_delta *= damp_factor
169
-
170
- return {
171
- "projected": current_mid + final_delta,
172
- "rho": rho
173
- }, sum(diff_y_net)
174
-
175
- def calculate_polr(bids, asks, mid):
176
- if not bids or not asks: return []
177
-
178
- sorted_bids = sorted(bids.items(), key=lambda x: -x[0])
179
- sorted_asks = sorted(asks.items(), key=lambda x: x[0])
180
-
181
- path_points = []
182
- volume_steps = [i * 0.5 for i in range(1, 61)]
183
-
184
- for i, target_vol in enumerate(volume_steps):
185
- ask_cost_dist = 0
186
- cum_vol = 0
187
- target_ask_price = mid
188
- for p, q in sorted_asks:
189
- cum_vol += q
190
- if cum_vol >= target_vol:
191
- target_ask_price = p
192
- break
193
- ask_cost_dist = target_ask_price - mid
194
-
195
- bid_cost_dist = 0
196
- cum_vol = 0
197
- target_bid_price = mid
198
- for p, q in sorted_bids:
199
- cum_vol += q
200
- if cum_vol >= target_vol:
201
- target_bid_price = p
202
- break
203
- bid_cost_dist = mid - target_bid_price
204
-
205
- if bid_cost_dist <= 0: bid_cost_dist = 0.01
206
- if ask_cost_dist <= 0: ask_cost_dist = 0.01
207
-
208
- projected_p = mid
209
- if ask_cost_dist > bid_cost_dist:
210
- projected_p = target_ask_price
211
- else:
212
- projected_p = target_bid_price
213
-
214
- path_points.append({'index': i, 'p': projected_p})
215
-
216
- return path_points
217
-
218
  def process_market_data():
219
- if not market_state['ready']: return {"error": "Initializing..."}
220
-
221
- mid = market_state['current_mid']
222
-
223
- now = time.time()
224
- if now - market_state['current_vol_window']['start'] >= 1.0:
225
- market_state['trade_vol_history'].append({
226
- 't': now,
227
- 'buy': market_state['current_vol_window']['buy'],
228
- 'sell': market_state['current_vol_window']['sell']
229
- })
230
- if len(market_state['trade_vol_history']) > 60:
231
- market_state['trade_vol_history'].pop(0)
232
- market_state['current_vol_window'] = {"buy": 0.0, "sell": 0.0, "start": now}
233
-
234
- sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
235
- sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
236
-
237
- if not sorted_bids or not sorted_asks: return {"error": "Empty Book"}
238
-
239
- best_bid_p, best_bid_q = sorted_bids[0]
240
- best_ask_p, best_ask_q = sorted_asks[0]
241
-
242
- bid_walls = detect_anomalies(sorted_bids, WALL_LOOKBACK)
243
- ask_walls = detect_anomalies(sorted_asks, WALL_LOOKBACK)
244
-
245
- d_b_x, d_b_y, cum = [], [], 0
246
- for p, q in sorted_bids[:300]:
247
- d = mid - p
248
- if d >= 0:
249
- cum += q
250
- d_b_x.append(d); d_b_y.append(cum)
251
-
252
- d_a_x, d_a_y, cum = [], [], 0
253
- for p, q in sorted_asks[:300]:
254
- d = p - mid
255
- if d >= 0:
256
- cum += q
257
- d_a_x.append(d); d_a_y.append(cum)
258
-
259
- diff_x, diff_y_net = [], []
260
- chart_bids, chart_asks = [], []
261
-
262
- if d_b_x and d_a_x:
263
- max_dist = min(d_b_x[-1], d_a_x[-1])
264
- step_size = max_dist / 100
265
- steps = [i * step_size for i in range(1, 101)]
266
-
267
- for s in steps:
268
- idx_b = bisect.bisect_right(d_b_x, s)
269
- vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
270
- idx_a = bisect.bisect_right(d_a_x, s)
271
- vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
272
-
273
- diff_x.append(s)
274
- diff_y_net.append(vol_b - vol_a)
275
- chart_bids.append(vol_b)
276
- chart_asks.append(vol_a)
277
-
278
- analysis, depth_integral = calculate_micro_price_structure(
279
- diff_x, diff_y_net, mid, best_bid_p, best_ask_p,
280
- {"bids": bid_walls, "asks": ask_walls}
281
- )
282
-
283
- # --- MACHINE LEARNING FEATURE EXTRACTION ---
284
- # 1. OFI: Net Buy-Sell Vol in current window
285
- feat_ofi = market_state['current_vol_window']['buy'] - market_state['current_vol_window']['sell']
286
- # 2. Depth Difference: Area under the Net Liquidity Curve (Bids - Asks)
287
- feat_depth = depth_integral
288
- # 3. Orderbook Imbalance at L1
289
- feat_l1_imb = (best_bid_q - best_ask_q) / (best_bid_q + best_ask_q)
290
- # 4. Price Momentum (Current - Prev)
291
- feat_mom = mid - market_state['prev_mid']
292
-
293
- features = [feat_ofi, feat_depth, feat_l1_imb, feat_mom]
294
-
295
- # Train (Learn from last tick's prediction vs this tick's reality)
296
- ml_model.train(mid, features)
297
- # Predict (Forecast next tick)
298
- ml_prediction = ml_model.get_forecast(mid, features)
299
-
300
- if len(market_state['ml_history']) == 0 or (now - market_state['ml_history'][-1]['t'] > 0.5):
301
- market_state['ml_history'].append({'t': now, 'p': ml_prediction})
302
- if len(market_state['ml_history']) > HISTORY_LENGTH:
303
- market_state['ml_history'].pop(0)
304
- # -------------------------------------------
305
-
306
- polr_path = calculate_polr(market_state['bids'], market_state['asks'], mid)
307
-
308
- if analysis:
309
- if not market_state['pred_history'] or (now - market_state['pred_history'][-1]['t'] > 0.5):
310
- market_state['pred_history'].append({'t': now, 'p': analysis['projected']})
311
- if len(market_state['pred_history']) > HISTORY_LENGTH:
312
- market_state['pred_history'].pop(0)
313
 
314
  return {
315
- "mid": mid,
316
- "history": market_state['history'],
317
- "pred_history": market_state['pred_history'],
318
- "ml_history": market_state['ml_history'],
319
- "polr": polr_path,
320
- "trade_history": market_state['trade_vol_history'],
321
- "ohlc": market_state['ohlc_history'],
322
- "depth_x": diff_x,
323
- "depth_net": diff_y_net,
324
- "depth_bids": chart_bids,
325
- "depth_asks": chart_asks,
326
- "analysis": analysis,
327
- "walls": {"bids": bid_walls, "asks": ask_walls}
328
  }
329
 
330
  HTML_PAGE = f"""
@@ -332,7 +32,7 @@ HTML_PAGE = f"""
332
  <html lang="en">
333
  <head>
334
  <meta charset="UTF-8">
335
- <title>{SYMBOL_KRAKEN}</title>
336
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
337
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
338
  <style>
@@ -341,13 +41,8 @@ HTML_PAGE = f"""
341
  --bg-panel: #0a0a0a;
342
  --border: #252525;
343
  --text-main: #FFFFFF;
344
- --text-dim: #999999;
345
  --green: #00ff9d;
346
  --red: #ff3b3b;
347
- --blue: #2979ff;
348
- --yellow: #ffeb3b;
349
- --purple: #d500f9;
350
- --cyan: #00bcd4;
351
  }}
352
  body {{
353
  margin: 0; padding: 0;
@@ -356,181 +51,41 @@ HTML_PAGE = f"""
356
  font-family: 'Inter', sans-serif;
357
  overflow: hidden;
358
  height: 100vh; width: 100vw;
 
 
359
  }}
360
- .layout {{
361
- display: grid;
362
- grid-template-rows: 34px 1fr 1fr;
363
- grid-template-columns: 3fr 1fr;
364
- gap: 1px;
365
- background-color: var(--border);
366
- height: 100vh;
367
- box-sizing: border-box;
368
- }}
369
- .panel {{ background: var(--bg-panel); display: flex; flex-direction: column; overflow: hidden; }}
370
-
371
- .status-bar {{
372
- grid-column: 1 / 3;
373
- grid-row: 1 / 2;
374
  background: var(--bg-panel);
 
375
  display: flex;
376
  align-items: center;
377
  justify-content: space-between;
378
- padding: 0 12px;
379
  font-family: 'JetBrains Mono', monospace;
380
- font-size: 12px;
381
- text-transform: uppercase;
382
- border-bottom: 1px solid var(--border);
383
- z-index: 50;
384
  }}
385
- .status-left {{ display: flex; gap: 20px; align-items: center; }}
386
- .live-dot {{ width: 8px; height: 8px; background-color: var(--green); border-radius: 50%; display: inline-block; box-shadow: 0 0 8px var(--green); }}
387
- .ticker-val {{ font-weight: 700; color: #fff; font-size: 13px; }}
388
-
389
- #p-chart {{ grid-column: 1 / 2; grid-row: 2 / 3; }}
390
 
391
- #p-bottom {{
392
- grid-column: 1 / 2; grid-row: 3 / 4;
393
- display: grid;
394
- grid-template-columns: 1fr 1fr;
395
- gap: 1px;
396
- background: var(--border);
397
- }}
398
- .bottom-sub {{ background: var(--bg-panel); display: flex; flex-direction: column; position: relative; }}
399
-
400
- #p-sidebar {{
401
- grid-column: 2 / 3;
402
- grid-row: 2 / 4;
403
- padding: 15px;
404
- display: flex;
405
- flex-direction: column;
406
- gap: 15px;
407
- border-left: 1px solid var(--border);
408
- overflow: hidden;
409
- }}
410
-
411
- .chart-header {{
412
- height: 24px;
413
- min-height: 24px;
414
- display: flex;
415
- align-items: center;
416
- padding-left: 12px;
417
- font-size: 10px;
418
- font-weight: 700;
419
- color: var(--text-dim);
420
- background: #050505;
421
- border-bottom: 1px solid #151515;
422
- letter-spacing: 0.5px;
423
- }}
424
-
425
- .data-group {{ display: flex; flex-direction: column; gap: 4px; }}
426
- .label {{ font-size: 10px; color: var(--text-dim); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }}
427
- .value {{ font-family: 'JetBrains Mono', monospace; font-size: 20px; font-weight: 700; color: #fff; }}
428
- .value-lg {{ font-size: 26px; }}
429
- .value-sub {{ font-family: 'JetBrains Mono', monospace; font-size: 11px; margin-top: 2px; color: #666; }}
430
-
431
- .divider {{ height: 1px; background: var(--border); width: 100%; }}
432
- .c-green {{ color: var(--green); }}
433
- .c-red {{ color: var(--red); }}
434
- .c-dim {{ color: var(--text-dim); }}
435
- .c-purp {{ color: var(--purple); }}
436
- .c-cyan {{ color: var(--cyan); }}
437
-
438
- .list-container {{ display: flex; flex-direction: column; gap: 8px; overflow-y: auto; height: 100px; }}
439
- .list-item {{
440
- display: flex; justify-content: space-between;
441
- font-family: 'JetBrains Mono', monospace;
442
- font-size: 11px;
443
- border-bottom: 1px solid #151515;
444
- padding-bottom: 4px;
445
- }}
446
- .list-item span:first-child {{ color: #e0e0e0; }}
447
- .list-item:last-child {{ border: none; }}
448
-
449
- .sidebar-chart-box {{
450
  flex: 1;
451
- display: flex;
452
- flex-direction: column;
453
- min-height: 0;
454
- }}
455
- .mini-chart {{
456
- flex: 1;
457
- background: rgba(255,255,255,0.02);
458
- border: 1px solid var(--border);
459
- border-radius: 4px;
460
  }}
461
  </style>
462
  </head>
463
  <body>
464
- <div class="layout">
465
- <div class="status-bar">
466
  <div class="status-left">
467
  <span class="live-dot"></span>
468
- <span style="font-weight:700; color:#fff;">{SYMBOL_KRAKEN}</span>
469
- <span id="price-ticker" class="ticker-val">---</span>
470
  </div>
471
- <div class="status-right" id="clock">00:00:00 UTC</div>
472
  </div>
473
 
474
- <div id="p-chart" class="panel">
475
- <div class="chart-header">
476
- PRICE (BLUE) // <span class="c-purp">POLR</span> // <span style="color:var(--yellow)">MICRO</span> // <span class="c-cyan">ML MODEL</span>
477
- </div>
478
- <div id="tv-price" style="flex: 1; width: 100%;"></div>
479
- </div>
480
-
481
- <div id="p-bottom">
482
- <div class="bottom-sub">
483
- <div class="chart-header">1M KLINE (KRAKEN OHLC)</div>
484
- <div id="tv-candles" style="flex: 1; width: 100%;"></div>
485
- </div>
486
- <div class="bottom-sub">
487
- <div class="chart-header">ORDER FLOW IMBALANCE</div>
488
- <div id="tv-net" style="flex: 1; width: 100%;"></div>
489
- </div>
490
- </div>
491
-
492
- <div id="p-sidebar" class="panel">
493
-
494
- <div class="data-group">
495
- <span class="label">ML Prediction</span>
496
- <span id="ml-val" class="value c-cyan">---</span>
497
- </div>
498
-
499
- <div class="data-group">
500
- <span class="label">Micro-Price Delta</span>
501
- <div style="display:flex; align-items: baseline; gap: 10px;">
502
- <span id="proj-pct" class="value value-lg">--%</span>
503
- <span id="proj-val" class="value-sub">---</span>
504
- </div>
505
- </div>
506
-
507
- <div class="divider"></div>
508
-
509
- <div class="data-group">
510
- <span class="label">OFI Imbalance Ratio</span>
511
- <span id="score-val" class="value">0.00</span>
512
- </div>
513
-
514
- <div class="divider"></div>
515
-
516
- <div class="data-group">
517
- <span class="label">Detected Walls (Z > 3.0)</span>
518
- <div id="wall-list" class="list-container">
519
- <span class="c-dim" style="font-size: 11px;">Scanning...</span>
520
- </div>
521
- </div>
522
-
523
- <div class="sidebar-chart-box">
524
- <span class="label" style="margin-bottom:4px;">Real-time Volume Ticks</span>
525
- <div id="sidebar-vol" class="mini-chart"></div>
526
- </div>
527
-
528
- <div class="sidebar-chart-box">
529
- <span class="label" style="margin-bottom:4px;">Liquidity Density</span>
530
- <div id="sidebar-density" class="mini-chart"></div>
531
- </div>
532
- </div>
533
- </div>
534
 
535
  <script>
536
  setInterval(() => {{
@@ -539,94 +94,26 @@ HTML_PAGE = f"""
539
  }}, 1000);
540
 
541
  document.addEventListener('DOMContentLoaded', () => {{
542
- const dom = {{
543
- ticker: document.getElementById('price-ticker'),
544
- score: document.getElementById('score-val'),
545
- projVal: document.getElementById('proj-val'),
546
- projPct: document.getElementById('proj-pct'),
547
- mlVal: document.getElementById('ml-val'),
548
- wallList: document.getElementById('wall-list')
549
- }};
550
 
551
- const chartOpts = {{
552
- layout: {{ background: {{ type: 'solid', color: '#0a0a0a' }}, textColor: '#888', fontFamily: 'JetBrains Mono' }},
553
  grid: {{ vertLines: {{ color: '#151515' }}, horzLines: {{ color: '#151515' }} }},
554
  rightPriceScale: {{ borderColor: '#222', scaleMargins: {{ top: 0.1, bottom: 0.1 }} }},
555
- timeScale: {{ borderColor: '#222', timeVisible: true, secondsVisible: true }},
556
  crosshair: {{ mode: 1, vertLine: {{ color: '#444', labelBackgroundColor: '#444' }}, horzLine: {{ color: '#444', labelBackgroundColor: '#444' }} }}
557
- }};
558
-
559
- const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts);
560
-
561
- const polrLines = [];
562
- const polrCount = 60;
563
-
564
- for(let i=0; i<polrCount; i++) {{
565
- const opacity = 1.0 - (i / (polrCount + 5));
566
- const color = `rgba(213, 0, 249, ${{opacity.toFixed(2)}})`;
567
-
568
- polrLines.push(
569
- priceChart.addLineSeries({{
570
- color: color,
571
- lineWidth: 1,
572
- crosshairMarkerVisible: false,
573
- lastValueVisible: false,
574
- priceLineVisible: false,
575
- title: ''
576
- }})
577
- );
578
- }}
579
-
580
- const priceSeries = priceChart.addLineSeries({{ color: '#2979ff', lineWidth: 2, title: 'Price' }});
581
- const predSeries = priceChart.addLineSeries({{ color: '#ffeb3b', lineWidth: 2, lineStyle: 2, title: 'Micro-Structure' }});
582
- const mlSeries = priceChart.addLineSeries({{ color: '#00bcd4', lineWidth: 2, lineStyle: 0, title: 'ML Forecast' }});
583
-
584
- const candleChart = LightweightCharts.createChart(document.getElementById('tv-candles'), {{
585
- ...chartOpts,
586
- timeScale: {{ timeVisible: true, secondsVisible: false }}
587
- }});
588
- const candleSeries = candleChart.addCandlestickSeries({{
589
- upColor: '#00ff9d', downColor: '#ff3b3b', borderVisible: false, wickUpColor: '#00ff9d', wickDownColor: '#ff3b3b'
590
  }});
591
 
592
- const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{
593
- ...chartOpts, localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
 
 
594
  }});
595
- const netSeries = netChart.addHistogramSeries({{ color: '#2979ff' }});
596
-
597
- const volChart = LightweightCharts.createChart(document.getElementById('sidebar-vol'), {{
598
- ...chartOpts,
599
- grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }},
600
- rightPriceScale: {{ visible: false }},
601
- timeScale: {{ visible: false }},
602
- handleScroll: false, handleScale: false
603
- }});
604
- const volBuySeries = volChart.addHistogramSeries({{ color: '#00ff9d' }});
605
- const volSellSeries = volChart.addHistogramSeries({{ color: '#ff3b3b' }});
606
-
607
- const denChart = LightweightCharts.createChart(document.getElementById('sidebar-density'), {{
608
- ...chartOpts,
609
- grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }},
610
- rightPriceScale: {{ visible: false }},
611
- timeScale: {{ visible: false }},
612
- handleScroll: false, handleScale: false
613
- }});
614
- const bidSeries = denChart.addAreaSeries({{ lineColor: '#00ff9d', topColor: 'rgba(0, 255, 157, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
615
- const askSeries = denChart.addAreaSeries({{ lineColor: '#ff3b3b', topColor: 'rgba(255, 59, 59, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
616
-
617
- let activeLines = [];
618
- let activeCandleLines = [];
619
 
620
  new ResizeObserver(entries => {{
621
- for(let entry of entries) {{
622
- const {{width, height}} = entry.contentRect;
623
- if(entry.target.id === 'tv-price') priceChart.applyOptions({{width, height}});
624
- if(entry.target.id === 'tv-candles') candleChart.applyOptions({{width, height}});
625
- if(entry.target.id === 'tv-net') netChart.applyOptions({{width, height}});
626
- if(entry.target.id === 'sidebar-vol') volChart.applyOptions({{width, height}});
627
- if(entry.target.id === 'sidebar-density') denChart.applyOptions({{width, height}});
628
- }}
629
- }}).observe(document.body);
630
 
631
  function connect() {{
632
  const ws = new WebSocket((location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/ws');
@@ -635,53 +122,6 @@ HTML_PAGE = f"""
635
  const data = JSON.parse(e.data);
636
  if (data.error) return;
637
 
638
- if (data.history.length) {{
639
- const hist = data.history.map(d => ({{ time: Math.floor(d.t), value: d.p }}));
640
- const cleanHist = [...new Map(hist.map(i => [i.time, i])).values()];
641
- priceSeries.setData(cleanHist);
642
-
643
- const lastP = cleanHist[cleanHist.length-1].value;
644
- const lastTime = cleanHist[cleanHist.length-1].time;
645
- dom.ticker.innerText = lastP.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
646
-
647
- if (data.analysis) {{
648
- const proj = data.analysis.projected;
649
- const rho = data.analysis.rho;
650
- predSeries.setData([
651
- cleanHist[cleanHist.length-1],
652
- {{ time: lastTime + 60, value: proj }}
653
- ]);
654
- const pct = ((proj - lastP) / lastP) * 100;
655
- const sign = pct >= 0 ? "+" : "";
656
- dom.projPct.innerText = `${{sign}}${{pct.toFixed(4)}}%`;
657
- dom.projPct.style.color = pct >= 0 ? "var(--green)" : "var(--red)";
658
- dom.projVal.innerText = proj.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
659
- dom.score.innerText = rho.toFixed(3);
660
- dom.score.style.color = rho > 0 ? "var(--green)" : (rho < 0 ? "var(--red)" : "var(--text-main)");
661
- }}
662
-
663
- if (data.ml_history && data.ml_history.length) {{
664
- const mlLast = data.ml_history[data.ml_history.length-1];
665
- dom.mlVal.innerText = mlLast.p.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
666
-
667
- mlSeries.setData([
668
- cleanHist[cleanHist.length-1],
669
- {{ time: lastTime + 30, value: mlLast.p }}
670
- ]);
671
- }}
672
-
673
- if (data.polr && data.polr.length) {{
674
- data.polr.forEach((point, index) => {{
675
- if (index < polrLines.length) {{
676
- polrLines[index].update({{
677
- time: lastTime,
678
- value: point.p
679
- }});
680
- }}
681
- }});
682
- }}
683
- }}
684
-
685
  if (data.ohlc && data.ohlc.length) {{
686
  const candles = data.ohlc.map(c => ({{
687
  time: c.time,
@@ -690,54 +130,11 @@ HTML_PAGE = f"""
690
  low: c.low,
691
  close: c.close
692
  }}));
 
 
693
  const uniqueCandles = [...new Map(candles.map(i => [i.time, i])).values()];
694
  candleSeries.setData(uniqueCandles);
695
  }}
696
-
697
- if (data.walls) {{
698
- activeLines.forEach(l => priceSeries.removePriceLine(l));
699
- activeLines = [];
700
- activeCandleLines.forEach(l => candleSeries.removePriceLine(l));
701
- activeCandleLines = [];
702
-
703
- let html = "";
704
- const addWall = (w, type) => {{
705
- const color = type === 'BID' ? '#00ff9d' : '#ff3b3b';
706
- const lineOpts = {{ price: w.price, color: color, lineWidth: 1, lineStyle: 2, axisLabelVisible: false }};
707
-
708
- activeLines.push(priceSeries.createPriceLine(lineOpts));
709
- activeCandleLines.push(candleSeries.createPriceLine(lineOpts));
710
-
711
- 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>`;
712
- }};
713
- data.walls.asks.forEach(w => addWall(w, 'ASK'));
714
- data.walls.bids.forEach(w => addWall(w, 'BID'));
715
- dom.wallList.innerHTML = html || '<span class="c-dim" style="font-size:11px">Scanning...</span>';
716
- }}
717
-
718
- if (data.trade_history && data.trade_history.length) {{
719
- const buyData = [], sellData = [];
720
- data.trade_history.forEach(t => {{
721
- const time = Math.floor(t.t);
722
- buyData.push({{ time: time, value: t.buy }});
723
- sellData.push({{ time: time, value: t.sell }});
724
- }});
725
- volBuySeries.setData([...new Map(buyData.map(i => [i.time, i])).values()]);
726
- volSellSeries.setData([...new Map(sellData.map(i => [i.time, i])).values()]);
727
- }}
728
-
729
- if (data.depth_x.length) {{
730
- const bids = [], asks = [], nets = [];
731
- for(let i=0; i<data.depth_x.length; i++) {{
732
- const t = data.depth_x[i];
733
- bids.push({{ time: t, value: data.depth_bids[i] }});
734
- asks.push({{ time: t, value: data.depth_asks[i] }});
735
- nets.push({{ time: t, value: data.depth_net[i], color: data.depth_net[i] > 0 ? '#00ff9d' : '#ff3b3b' }});
736
- }}
737
- bidSeries.setData(bids);
738
- askSeries.setData(asks);
739
- netSeries.setData(nets);
740
- }}
741
  }};
742
  ws.onclose = () => setTimeout(connect, 2000);
743
  }}
@@ -750,6 +147,8 @@ HTML_PAGE = f"""
750
 
751
  async def kraken_worker():
752
  global market_state
 
 
753
  try:
754
  async with aiohttp.ClientSession() as session:
755
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
@@ -768,21 +167,20 @@ async def kraken_worker():
768
  'low': float(c[3]),
769
  'close': float(c[4])
770
  }
771
- for c in raw_candles[-120:]
772
  ]
 
773
  break
774
  except Exception as e:
775
  logging.error(f"History fetch failed: {e}")
776
 
 
777
  while True:
778
  try:
779
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
780
  logging.info(f"πŸ”Œ Connected to Kraken ({SYMBOL_KRAKEN})")
781
 
782
- await ws.send(json.dumps({
783
- "method": "subscribe",
784
- "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}
785
- }))
786
  await ws.send(json.dumps({
787
  "method": "subscribe",
788
  "params": {"channel": "trade", "symbol": [SYMBOL_KRAKEN]}
@@ -797,51 +195,23 @@ async def kraken_worker():
797
  channel = payload.get("channel")
798
  data = payload.get("data", [])
799
 
800
- if channel == "book":
801
- for item in data:
802
- for bid in item.get('bids', []):
803
- q, p = float(bid['qty']), float(bid['price'])
804
- if q == 0: market_state['bids'].pop(p, None)
805
- else: market_state['bids'][p] = q
806
- for ask in item.get('asks', []):
807
- q, p = float(ask['qty']), float(ask['price'])
808
- if q == 0: market_state['asks'].pop(p, None)
809
- else: market_state['asks'][p] = q
810
-
811
- if market_state['bids'] and market_state['asks']:
812
- market_state['prev_mid'] = market_state['current_mid']
813
- best_bid = max(market_state['bids'].keys())
814
- best_ask = min(market_state['asks'].keys())
815
- mid = (best_bid + best_ask) / 2
816
- market_state['current_mid'] = mid
817
- market_state['ready'] = True
818
-
819
- now = time.time()
820
- if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
821
- market_state['history'].append({'t': now, 'p': mid})
822
- if len(market_state['history']) > HISTORY_LENGTH:
823
- market_state['history'].pop(0)
824
-
825
- elif channel == "trade":
826
  for trade in data:
827
  try:
828
- qty = float(trade['qty'])
829
  price = float(trade['price'])
830
- side = trade['side']
831
-
832
- if side == 'buy': market_state['current_vol_window']['buy'] += qty
833
- else: market_state['current_vol_window']['sell'] += qty
834
-
835
  current_minute_start = int(time.time()) // 60 * 60
836
 
837
  if market_state['ohlc_history']:
838
  last_candle = market_state['ohlc_history'][-1]
839
 
 
840
  if last_candle['time'] == current_minute_start:
841
  last_candle['close'] = price
842
  if price > last_candle['high']: last_candle['high'] = price
843
  if price < last_candle['low']: last_candle['low'] = price
844
 
 
845
  elif current_minute_start > last_candle['time']:
846
  new_candle = {
847
  'time': current_minute_start,
@@ -851,13 +221,15 @@ async def kraken_worker():
851
  'close': price
852
  }
853
  market_state['ohlc_history'].append(new_candle)
854
- if len(market_state['ohlc_history']) > 200:
855
  market_state['ohlc_history'].pop(0)
856
  except: pass
857
 
 
858
  elif channel == "ohlc":
859
  for candle in data:
860
  try:
 
861
  start_time = int(float(candle['endtime'])) - 60
862
  c_data = {
863
  'time': start_time,
@@ -872,7 +244,7 @@ async def kraken_worker():
872
  market_state['ohlc_history'][-1] = c_data
873
  elif market_state['ohlc_history'][-1]['time'] < start_time:
874
  market_state['ohlc_history'].append(c_data)
875
- if len(market_state['ohlc_history']) > 200:
876
  market_state['ohlc_history'].pop(0)
877
  except Exception as e:
878
  pass
@@ -925,7 +297,7 @@ async def main():
925
  await runner.setup()
926
  site = web.TCPSite(runner, '0.0.0.0', PORT)
927
  await site.start()
928
- print(f"πŸš€ Quant Dashboard: http://localhost:{PORT}")
929
  await asyncio.Event().wait()
930
 
931
  if __name__ == "__main__":
 
2
  import json
3
  import logging
4
  import time
 
 
 
5
  import aiohttp
6
  from aiohttp import web
7
  import websockets
8
 
9
  SYMBOL_KRAKEN = "BTC/USD"
10
  PORT = 7860
11
+ BROADCAST_RATE = 0.5 # Slower broadcast is fine for just candles
 
 
 
 
 
 
 
 
 
 
12
 
13
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  market_state = {
 
 
 
 
 
 
16
  "ohlc_history": [],
 
 
 
17
  "ready": False
18
  }
19
 
20
  connected_clients = set()
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  def process_market_data():
23
+ if not market_state['ready']:
24
+ return {"error": "Initializing..."}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  return {
27
+ "ohlc": market_state['ohlc_history']
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
29
 
30
  HTML_PAGE = f"""
 
32
  <html lang="en">
33
  <head>
34
  <meta charset="UTF-8">
35
+ <title>{SYMBOL_KRAKEN} Klines</title>
36
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
37
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
38
  <style>
 
41
  --bg-panel: #0a0a0a;
42
  --border: #252525;
43
  --text-main: #FFFFFF;
 
44
  --green: #00ff9d;
45
  --red: #ff3b3b;
 
 
 
 
46
  }}
47
  body {{
48
  margin: 0; padding: 0;
 
51
  font-family: 'Inter', sans-serif;
52
  overflow: hidden;
53
  height: 100vh; width: 100vw;
54
+ display: flex;
55
+ flex-direction: column;
56
  }}
57
+ .header {{
58
+ height: 40px;
 
 
 
 
 
 
 
 
 
 
 
 
59
  background: var(--bg-panel);
60
+ border-bottom: 1px solid var(--border);
61
  display: flex;
62
  align-items: center;
63
  justify-content: space-between;
64
+ padding: 0 15px;
65
  font-family: 'JetBrains Mono', monospace;
66
+ font-size: 13px;
 
 
 
67
  }}
68
+ .status-left {{ display: flex; gap: 15px; align-items: center; }}
69
+ .live-dot {{ width: 8px; height: 8px; background-color: var(--green); border-radius: 50%; box-shadow: 0 0 8px var(--green); }}
 
 
 
70
 
71
+ #chart-container {{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  flex: 1;
73
+ width: 100%;
74
+ position: relative;
 
 
 
 
 
 
 
75
  }}
76
  </style>
77
  </head>
78
  <body>
79
+ <div class="header">
 
80
  <div class="status-left">
81
  <span class="live-dot"></span>
82
+ <span style="font-weight:700;">{SYMBOL_KRAKEN}</span>
83
+ <span>1 Minute Candles</span>
84
  </div>
85
+ <div id="clock">00:00:00 UTC</div>
86
  </div>
87
 
88
+ <div id="chart-container"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  <script>
91
  setInterval(() => {{
 
94
  }}, 1000);
95
 
96
  document.addEventListener('DOMContentLoaded', () => {{
97
+ const chartContainer = document.getElementById('chart-container');
 
 
 
 
 
 
 
98
 
99
+ const chart = LightweightCharts.createChart(chartContainer, {{
100
+ layout: {{ background: {{ type: 'solid', color: '#000000' }}, textColor: '#888', fontFamily: 'JetBrains Mono' }},
101
  grid: {{ vertLines: {{ color: '#151515' }}, horzLines: {{ color: '#151515' }} }},
102
  rightPriceScale: {{ borderColor: '#222', scaleMargins: {{ top: 0.1, bottom: 0.1 }} }},
103
+ timeScale: {{ borderColor: '#222', timeVisible: true, secondsVisible: false }},
104
  crosshair: {{ mode: 1, vertLine: {{ color: '#444', labelBackgroundColor: '#444' }}, horzLine: {{ color: '#444', labelBackgroundColor: '#444' }} }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  }});
106
 
107
+ const candleSeries = chart.addCandlestickSeries({{
108
+ upColor: '#00ff9d', downColor: '#ff3b3b',
109
+ borderVisible: false,
110
+ wickUpColor: '#00ff9d', wickDownColor: '#ff3b3b'
111
  }});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
  new ResizeObserver(entries => {{
114
+ const {{width, height}} = entries[0].contentRect;
115
+ chart.applyOptions({{ width, height }});
116
+ }}).observe(chartContainer);
 
 
 
 
 
 
117
 
118
  function connect() {{
119
  const ws = new WebSocket((location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/ws');
 
122
  const data = JSON.parse(e.data);
123
  if (data.error) return;
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  if (data.ohlc && data.ohlc.length) {{
126
  const candles = data.ohlc.map(c => ({{
127
  time: c.time,
 
130
  low: c.low,
131
  close: c.close
132
  }}));
133
+
134
+ // Deduplicate based on time to prevent flickering
135
  const uniqueCandles = [...new Map(candles.map(i => [i.time, i])).values()];
136
  candleSeries.setData(uniqueCandles);
137
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  }};
139
  ws.onclose = () => setTimeout(connect, 2000);
140
  }}
 
147
 
148
  async def kraken_worker():
149
  global market_state
150
+
151
+ # 1. Fetch initial History via REST
152
  try:
153
  async with aiohttp.ClientSession() as session:
154
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
 
167
  'low': float(c[3]),
168
  'close': float(c[4])
169
  }
170
+ for c in raw_candles[-300:] # Keep last 300 candles
171
  ]
172
+ market_state['ready'] = True
173
  break
174
  except Exception as e:
175
  logging.error(f"History fetch failed: {e}")
176
 
177
+ # 2. Connect via WebSocket for Live Updates
178
  while True:
179
  try:
180
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
181
  logging.info(f"πŸ”Œ Connected to Kraken ({SYMBOL_KRAKEN})")
182
 
183
+ # Subscribe to Trade (for real-time ticks) and OHLC (for official close)
 
 
 
184
  await ws.send(json.dumps({
185
  "method": "subscribe",
186
  "params": {"channel": "trade", "symbol": [SYMBOL_KRAKEN]}
 
195
  channel = payload.get("channel")
196
  data = payload.get("data", [])
197
 
198
+ # A. Real-time Tick Updates (Simulating live candle)
199
+ if channel == "trade":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  for trade in data:
201
  try:
 
202
  price = float(trade['price'])
 
 
 
 
 
203
  current_minute_start = int(time.time()) // 60 * 60
204
 
205
  if market_state['ohlc_history']:
206
  last_candle = market_state['ohlc_history'][-1]
207
 
208
+ # Update current candle
209
  if last_candle['time'] == current_minute_start:
210
  last_candle['close'] = price
211
  if price > last_candle['high']: last_candle['high'] = price
212
  if price < last_candle['low']: last_candle['low'] = price
213
 
214
+ # Create new candle
215
  elif current_minute_start > last_candle['time']:
216
  new_candle = {
217
  'time': current_minute_start,
 
221
  'close': price
222
  }
223
  market_state['ohlc_history'].append(new_candle)
224
+ if len(market_state['ohlc_history']) > 300:
225
  market_state['ohlc_history'].pop(0)
226
  except: pass
227
 
228
+ # B. Confirmed Candle Updates
229
  elif channel == "ohlc":
230
  for candle in data:
231
  try:
232
+ # Kraken sends 'endtime', convert to 'starttime' for charts
233
  start_time = int(float(candle['endtime'])) - 60
234
  c_data = {
235
  'time': start_time,
 
244
  market_state['ohlc_history'][-1] = c_data
245
  elif market_state['ohlc_history'][-1]['time'] < start_time:
246
  market_state['ohlc_history'].append(c_data)
247
+ if len(market_state['ohlc_history']) > 300:
248
  market_state['ohlc_history'].pop(0)
249
  except Exception as e:
250
  pass
 
297
  await runner.setup()
298
  site = web.TCPSite(runner, '0.0.0.0', PORT)
299
  await site.start()
300
+ print(f"πŸš€ Kline Chart: http://localhost:{PORT}")
301
  await asyncio.Event().wait()
302
 
303
  if __name__ == "__main__":