Alvin3y1 commited on
Commit
f95b3cf
·
verified ·
1 Parent(s): c2b455a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +126 -117
app.py CHANGED
@@ -16,9 +16,9 @@ HISTORY_LENGTH = 300
16
  BROADCAST_RATE = 0.1
17
 
18
  # Mathematical Constants
19
- DECAY_LAMBDA = 50.0 # Decay factor for liquidity weighting (closer orders matter more)
20
- IMPACT_SENSITIVITY = 2.0 # Multiplier for Micro-Price deviation
21
- WALL_DAMPENING = 0.8 # How much a Z-Score > 3 wall reduces momentum (0.0-1.0)
22
  Z_SCORE_THRESHOLD = 3.0
23
  WALL_LOOKBACK = 200
24
 
@@ -30,8 +30,9 @@ market_state = {
30
  "asks": {},
31
  "history": [],
32
  "pred_history": [],
 
 
33
  "current_mid": 0.0,
34
- "prev_mid": 0.0,
35
  "ready": False
36
  }
37
 
@@ -39,31 +40,7 @@ connected_clients = set()
39
 
40
  # --- QUANTITATIVE METHODS ---
41
 
42
- def calculate_ols_slope(x_values, y_values):
43
- """
44
- Calculates the slope (m) of the liquidity density using Ordinary Least Squares (OLS).
45
- y = mx + c
46
- Steep slope = High Liquidity Density (Hard to move price).
47
- Flat slope = Low Liquidity Density (Price slips easily).
48
- """
49
- n = len(x_values)
50
- if n < 2: return 0.0
51
-
52
- sum_x = sum(x_values)
53
- sum_y = sum(y_values)
54
- sum_xy = sum(x*y for x, y in zip(x_values, y_values))
55
- sum_xx = sum(x*x for x in x_values)
56
-
57
- denominator = (n * sum_xx - sum_x * sum_x)
58
- if denominator == 0: return 0.0
59
-
60
- slope = (n * sum_xy - sum_x * sum_y) / denominator
61
- return slope
62
-
63
  def detect_anomalies(orders, scan_depth):
64
- """
65
- Standard Z-Score Outlier Detection.
66
- """
67
  if len(orders) < 10: return []
68
  relevant_orders = orders[:scan_depth]
69
  volumes = [q for p, q in relevant_orders]
@@ -83,78 +60,46 @@ def detect_anomalies(orders, scan_depth):
83
  if z_score > Z_SCORE_THRESHOLD:
84
  walls.append({"price": price, "vol": qty, "z_score": z_score})
85
 
86
- # Sort by Z-Score (Strongest first)
87
  walls.sort(key=lambda x: x['z_score'], reverse=True)
88
  return walls[:3]
89
 
90
- def calculate_micro_price_structure(diff_x, diff_y_raw, current_mid, best_bid, best_ask, walls):
91
- """
92
- Advanced Prediction Engine using:
93
- 1. Weighted Imbalance (Exponential Decay)
94
- 2. Liquidity Slope Elasticity
95
- 3. Wall Friction Dampening
96
- """
97
  if not diff_x or len(diff_x) < 5: return None
98
 
99
- # 1. Calculate Weighted Volume Imbalance (VOI)
100
- # Using exponential decay to value liquidity near the spread higher than deep liquidity
101
- sum_weighted_bid = 0.0
102
- sum_weighted_ask = 0.0
103
-
104
- # Reconstruct raw bid/ask curves from the net diff arrays for calculation
105
- # (This is an approximation based on the diffs passed in, ideally we use raw state)
106
- # For efficiency, we calculate 'imbalance' directly from the net curve
107
  weighted_imbalance = 0.0
 
108
 
109
  for i in range(len(diff_x)):
110
  dist = diff_x[i]
111
- net_vol = diff_y_raw[i] # This is BidVol - AskVol at this depth
112
-
113
- # Decay function: e^(-x / lambda)
114
  weight = math.exp(-dist / DECAY_LAMBDA)
115
  weighted_imbalance += net_vol * weight
 
116
 
117
- # Normalize Imbalance (-1 to 1 range roughly)
118
- # We divide by the total weighted volume estimate to get a ratio (rho)
119
- # Estimate total volume based on abs(net_vol) as a proxy
120
- total_weighted_vol = sum(abs(v) * math.exp(-d/DECAY_LAMBDA) for d, v in zip(diff_x, diff_y_raw))
121
- if total_weighted_vol == 0: rho = 0
122
- else: rho = weighted_imbalance / total_weighted_vol
123
 
124
- # 2. Base Micro-Price Projection
125
- # P_micro = P_mid + (Spread * ImbalanceRatio * Sensitivity)
126
  spread = best_ask - best_bid
127
  theoretical_delta = (spread / 2) * rho * IMPACT_SENSITIVITY
128
-
129
  projected_price = current_mid + theoretical_delta
130
 
131
- # 3. Wall Friction Logic
132
- # If the projection tries to cross a wall, the Z-Score of that wall reduces the delta.
133
-
134
  final_delta = theoretical_delta
135
-
136
- # Check Ask Walls (Resistance)
137
  if final_delta > 0 and walls['asks']:
138
- nearest_wall = walls['asks'][0] # Strongest wall
139
  if projected_price >= nearest_wall['price']:
140
- # Dampen based on Z-Score. Higher Z = More damping.
141
- # Factor: 1 / (1 + (Z * 0.1))
142
  damp_factor = 1.0 / (1.0 + (nearest_wall['z_score'] * 0.2))
143
  final_delta *= damp_factor
144
-
145
- # Check Bid Walls (Support)
146
  elif final_delta < 0 and walls['bids']:
147
- nearest_wall = walls['bids'][0] # Strongest wall
148
  if projected_price <= nearest_wall['price']:
149
  damp_factor = 1.0 / (1.0 + (nearest_wall['z_score'] * 0.2))
150
  final_delta *= damp_factor
151
 
152
- final_projected = current_mid + final_delta
153
-
154
  return {
155
- "projected": final_projected,
156
- "net_score": weighted_imbalance, # Raw score for the UI meter
157
- "rho": rho # Imbalance ratio
158
  }
159
 
160
  def process_market_data():
@@ -162,7 +107,21 @@ def process_market_data():
162
 
163
  mid = market_state['current_mid']
164
 
165
- # Get raw sorted orders
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
167
  sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
168
 
@@ -171,11 +130,9 @@ def process_market_data():
171
  best_bid = sorted_bids[0][0]
172
  best_ask = sorted_asks[0][0]
173
 
174
- # --- WALL DETECTION ---
175
  bid_walls = detect_anomalies(sorted_bids, WALL_LOOKBACK)
176
  ask_walls = detect_anomalies(sorted_asks, WALL_LOOKBACK)
177
 
178
- # --- DEPTH AGGREGATION ---
179
  d_b_x, d_b_y, cum = [], [], 0
180
  for p, q in sorted_bids[:300]:
181
  d = mid - p
@@ -190,44 +147,31 @@ def process_market_data():
190
  cum += q
191
  d_a_x.append(d); d_a_y.append(cum)
192
 
193
- # --- UNIFIED GRID FOR ANALYSIS ---
194
  diff_x, diff_y_net = [], []
195
  chart_bids, chart_asks = [], []
196
 
197
  if d_b_x and d_a_x:
198
  max_dist = min(d_b_x[-1], d_a_x[-1])
199
- # Create 100 buckets up to the max common depth
200
  step_size = max_dist / 100
201
  steps = [i * step_size for i in range(1, 101)]
202
 
203
  for s in steps:
204
- # Interpolate Bid Volume at step s
205
  idx_b = bisect.bisect_right(d_b_x, s)
206
  vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
207
-
208
- # Interpolate Ask Volume at step s
209
  idx_a = bisect.bisect_right(d_a_x, s)
210
  vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
211
 
212
  diff_x.append(s)
213
- diff_y_net.append(vol_b - vol_a) # Net Imbalance at this depth
214
-
215
  chart_bids.append(vol_b)
216
  chart_asks.append(vol_a)
217
 
218
- # --- MATHEMATICAL ANALYSIS ---
219
  analysis = calculate_micro_price_structure(
220
- diff_x,
221
- diff_y_net,
222
- mid,
223
- best_bid,
224
- best_ask,
225
  {"bids": bid_walls, "asks": ask_walls}
226
  )
227
 
228
- now = time.time()
229
  if analysis:
230
- # Rate limit prediction history update
231
  if not market_state['pred_history'] or (now - market_state['pred_history'][-1]['t'] > 0.5):
232
  market_state['pred_history'].append({'t': now, 'p': analysis['projected']})
233
  if len(market_state['pred_history']) > HISTORY_LENGTH:
@@ -237,6 +181,7 @@ def process_market_data():
237
  "mid": mid,
238
  "history": market_state['history'],
239
  "pred_history": market_state['pred_history'],
 
240
  "depth_x": diff_x,
241
  "depth_net": diff_y_net,
242
  "depth_bids": chart_bids,
@@ -264,6 +209,7 @@ HTML_PAGE = f"""
264
  --green: #00ff9d;
265
  --red: #ff3b3b;
266
  --blue: #2979ff;
 
267
  }}
268
  body {{
269
  margin: 0; padding: 0;
@@ -322,8 +268,9 @@ HTML_PAGE = f"""
322
  padding: 20px;
323
  display: flex;
324
  flex-direction: column;
325
- gap: 25px;
326
  border-left: 1px solid var(--border);
 
327
  }}
328
 
329
  .chart-header {{
@@ -351,7 +298,7 @@ HTML_PAGE = f"""
351
  .c-red {{ color: var(--red); }}
352
  .c-dim {{ color: var(--text-dim); }}
353
 
354
- .list-container {{ display: flex; flex-direction: column; gap: 8px; overflow-y: hidden; }}
355
  .list-item {{
356
  display: flex; justify-content: space-between;
357
  font-family: 'JetBrains Mono', monospace;
@@ -361,6 +308,14 @@ HTML_PAGE = f"""
361
  }}
362
  .list-item span:first-child {{ color: #e0e0e0; }}
363
  .list-item:last-child {{ border: none; }}
 
 
 
 
 
 
 
 
364
  </style>
365
  </head>
366
  <body>
@@ -376,17 +331,17 @@ HTML_PAGE = f"""
376
  </div>
377
 
378
  <div id="p-chart" class="panel">
379
- <div class="chart-header">PRICE ACTION // MICRO-STRUCTURE FORECAST</div>
380
  <div id="tv-price" style="flex: 1; width: 100%;"></div>
381
  </div>
382
 
383
  <div id="p-depth">
384
  <div class="depth-sub">
385
- <div class="chart-header">LIQUIDITY DENSITY PROFILE</div>
386
  <div id="tv-raw" style="flex: 1; width: 100%;"></div>
387
  </div>
388
  <div class="depth-sub">
389
- <div class="chart-header">ORDER FLOW IMBALANCE (OFI)</div>
390
  <div id="tv-net" style="flex: 1; width: 100%;"></div>
391
  </div>
392
  </div>
@@ -394,7 +349,7 @@ HTML_PAGE = f"""
394
  <div id="p-sidebar" class="panel">
395
 
396
  <div class="data-group">
397
- <span class="label">Micro-Price Delta (5s)</span>
398
  <div style="display:flex; align-items: baseline; gap: 10px;">
399
  <span id="proj-pct" class="value value-lg">--%</span>
400
  <span id="proj-val" class="value-sub">---</span>
@@ -404,23 +359,23 @@ HTML_PAGE = f"""
404
  <div class="divider"></div>
405
 
406
  <div class="data-group">
407
- <span class="label">OFI Imbalance Ratio (ρ)</span>
408
  <span id="score-val" class="value">0.00</span>
409
- <span class="value-sub">Range: -1.0 (Bear) to 1.0 (Bull)</span>
410
  </div>
411
 
412
  <div class="divider"></div>
413
 
414
- <div class="data-group" style="flex: 1;">
415
- <span class="label" style="margin-bottom: 10px;">High Sigma Walls (Z > 3.0)</span>
416
  <div id="wall-list" class="list-container">
417
- <span class="c-dim" style="font-size: 11px;">Initializing Quantitative Scan...</span>
418
  </div>
419
  </div>
420
-
421
- <div style="margin-top: auto;">
422
- <span class="label">Model Latency</span>
423
- <div class="value-sub c-green">Optimized</div>
 
424
  </div>
425
  </div>
426
  </div>
@@ -448,39 +403,55 @@ HTML_PAGE = f"""
448
  crosshair: {{ mode: 1, vertLine: {{ color: '#444', labelBackgroundColor: '#444' }}, horzLine: {{ color: '#444', labelBackgroundColor: '#444' }} }}
449
  }};
450
 
 
451
  const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts);
452
- const priceSeries = priceChart.addLineSeries({{ color: '#FFFFFF', lineWidth: 1, title: 'Price' }});
453
- const predSeries = priceChart.addLineSeries({{ color: '#2979ff', lineWidth: 1, lineStyle: 2, title: 'Forecast' }});
454
 
 
455
  const rawChart = LightweightCharts.createChart(document.getElementById('tv-raw'), {{
456
- ...chartOpts,
457
- localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
458
  }});
459
  const bidSeries = rawChart.addAreaSeries({{ lineColor: '#00ff9d', topColor: 'rgba(0, 255, 157, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
460
  const askSeries = rawChart.addAreaSeries({{ lineColor: '#ff3b3b', topColor: 'rgba(255, 59, 59, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
461
 
462
  const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{
463
- ...chartOpts,
464
- localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
465
  }});
466
  const netSeries = netChart.addHistogramSeries({{ color: '#2979ff' }});
467
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  let activeLines = [];
469
 
 
470
  new ResizeObserver(entries => {{
471
  for(let entry of entries) {{
472
  const {{width, height}} = entry.contentRect;
473
  if(entry.target.id === 'tv-price') priceChart.applyOptions({{width, height}});
474
  if(entry.target.id === 'tv-raw') rawChart.applyOptions({{width, height}});
475
  if(entry.target.id === 'tv-net') netChart.applyOptions({{width, height}});
 
476
  }}
477
  }}).observe(document.body);
478
 
479
- ['tv-price', 'tv-raw', 'tv-net'].forEach(id => {{
480
  new ResizeObserver(e => {{
481
- if(id === 'tv-price') priceChart.applyOptions({{ width: e[0].contentRect.width, height: e[0].contentRect.height }});
482
- if(id === 'tv-raw') rawChart.applyOptions({{ width: e[0].contentRect.width, height: e[0].contentRect.height }});
483
- if(id === 'tv-net') netChart.applyOptions({{ width: e[0].contentRect.width, height: e[0].contentRect.height }});
 
 
484
  }}).observe(document.getElementById(id));
485
  }});
486
 
@@ -501,7 +472,6 @@ HTML_PAGE = f"""
501
 
502
  if (data.analysis) {{
503
  const proj = data.analysis.projected;
504
- // Using the Imbalance Ratio (rho) for the score display
505
  const rho = data.analysis.rho;
506
 
507
  predSeries.setData([
@@ -522,6 +492,7 @@ HTML_PAGE = f"""
522
  }}
523
  }}
524
 
 
525
  if (data.walls) {{
526
  activeLines.forEach(l => priceSeries.removePriceLine(l));
527
  activeLines = [];
@@ -538,10 +509,27 @@ HTML_PAGE = f"""
538
 
539
  data.walls.asks.forEach(w => addWall(w, 'ASK'));
540
  data.walls.bids.forEach(w => addWall(w, 'BID'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
541
 
542
- dom.wallList.innerHTML = html || '<span class="c-dim" style="font-size:11px">Scanning liquidity surface...</span>';
 
543
  }}
544
 
 
545
  if (data.depth_x.length) {{
546
  const bids = [], asks = [], nets = [];
547
  for(let i=0; i<data.depth_x.length; i++) {{
@@ -572,10 +560,16 @@ async def kraken_worker():
572
  try:
573
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
574
  logging.info(f"🔌 Connected to Kraken ({SYMBOL_KRAKEN})")
 
 
575
  await ws.send(json.dumps({
576
  "method": "subscribe",
577
  "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}
578
  }))
 
 
 
 
579
 
580
  async for message in ws:
581
  payload = json.loads(message)
@@ -606,6 +600,21 @@ async def kraken_worker():
606
  market_state['history'].append({'t': now, 'p': mid})
607
  if len(market_state['history']) > HISTORY_LENGTH:
608
  market_state['history'].pop(0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
 
610
  except Exception as e:
611
  logging.warning(f"⚠️ Reconnecting: {e}")
 
16
  BROADCAST_RATE = 0.1
17
 
18
  # Mathematical Constants
19
+ DECAY_LAMBDA = 50.0
20
+ IMPACT_SENSITIVITY = 2.0
21
+ WALL_DAMPENING = 0.8
22
  Z_SCORE_THRESHOLD = 3.0
23
  WALL_LOOKBACK = 200
24
 
 
30
  "asks": {},
31
  "history": [],
32
  "pred_history": [],
33
+ "trade_vol_history": [], # New: Store trade volume history
34
+ "current_vol_window": {"buy": 0.0, "sell": 0.0, "start": time.time()},
35
  "current_mid": 0.0,
 
36
  "ready": False
37
  }
38
 
 
40
 
41
  # --- QUANTITATIVE METHODS ---
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  def detect_anomalies(orders, scan_depth):
 
 
 
44
  if len(orders) < 10: return []
45
  relevant_orders = orders[:scan_depth]
46
  volumes = [q for p, q in relevant_orders]
 
60
  if z_score > Z_SCORE_THRESHOLD:
61
  walls.append({"price": price, "vol": qty, "z_score": z_score})
62
 
 
63
  walls.sort(key=lambda x: x['z_score'], reverse=True)
64
  return walls[:3]
65
 
66
+ def calculate_micro_price_structure(diff_x, diff_y_net, current_mid, best_bid, best_ask, walls):
 
 
 
 
 
 
67
  if not diff_x or len(diff_x) < 5: return None
68
 
69
+ # Weighted Imbalance Calculation
 
 
 
 
 
 
 
70
  weighted_imbalance = 0.0
71
+ total_weight = 0.0
72
 
73
  for i in range(len(diff_x)):
74
  dist = diff_x[i]
75
+ net_vol = diff_y_net[i]
 
 
76
  weight = math.exp(-dist / DECAY_LAMBDA)
77
  weighted_imbalance += net_vol * weight
78
+ total_weight += weight
79
 
80
+ rho = weighted_imbalance / total_weight if total_weight > 0 else 0
 
 
 
 
 
81
 
82
+ # Base Projection
 
83
  spread = best_ask - best_bid
84
  theoretical_delta = (spread / 2) * rho * IMPACT_SENSITIVITY
 
85
  projected_price = current_mid + theoretical_delta
86
 
87
+ # Wall Friction
 
 
88
  final_delta = theoretical_delta
 
 
89
  if final_delta > 0 and walls['asks']:
90
+ nearest_wall = walls['asks'][0]
91
  if projected_price >= nearest_wall['price']:
 
 
92
  damp_factor = 1.0 / (1.0 + (nearest_wall['z_score'] * 0.2))
93
  final_delta *= damp_factor
 
 
94
  elif final_delta < 0 and walls['bids']:
95
+ nearest_wall = walls['bids'][0]
96
  if projected_price <= nearest_wall['price']:
97
  damp_factor = 1.0 / (1.0 + (nearest_wall['z_score'] * 0.2))
98
  final_delta *= damp_factor
99
 
 
 
100
  return {
101
+ "projected": current_mid + final_delta,
102
+ "rho": rho
 
103
  }
104
 
105
  def process_market_data():
 
107
 
108
  mid = market_state['current_mid']
109
 
110
+ # Process Trade Volume Window (Reset every 1 second)
111
+ now = time.time()
112
+ if now - market_state['current_vol_window']['start'] >= 1.0:
113
+ market_state['trade_vol_history'].append({
114
+ 't': now,
115
+ 'buy': market_state['current_vol_window']['buy'],
116
+ 'sell': market_state['current_vol_window']['sell']
117
+ })
118
+ if len(market_state['trade_vol_history']) > 60: # Keep last 60 seconds
119
+ market_state['trade_vol_history'].pop(0)
120
+
121
+ # Reset window
122
+ market_state['current_vol_window'] = {"buy": 0.0, "sell": 0.0, "start": now}
123
+
124
+ # Order Book Processing
125
  sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
126
  sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
127
 
 
130
  best_bid = sorted_bids[0][0]
131
  best_ask = sorted_asks[0][0]
132
 
 
133
  bid_walls = detect_anomalies(sorted_bids, WALL_LOOKBACK)
134
  ask_walls = detect_anomalies(sorted_asks, WALL_LOOKBACK)
135
 
 
136
  d_b_x, d_b_y, cum = [], [], 0
137
  for p, q in sorted_bids[:300]:
138
  d = mid - p
 
147
  cum += q
148
  d_a_x.append(d); d_a_y.append(cum)
149
 
 
150
  diff_x, diff_y_net = [], []
151
  chart_bids, chart_asks = [], []
152
 
153
  if d_b_x and d_a_x:
154
  max_dist = min(d_b_x[-1], d_a_x[-1])
 
155
  step_size = max_dist / 100
156
  steps = [i * step_size for i in range(1, 101)]
157
 
158
  for s in steps:
 
159
  idx_b = bisect.bisect_right(d_b_x, s)
160
  vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
 
 
161
  idx_a = bisect.bisect_right(d_a_x, s)
162
  vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
163
 
164
  diff_x.append(s)
165
+ diff_y_net.append(vol_b - vol_a)
 
166
  chart_bids.append(vol_b)
167
  chart_asks.append(vol_a)
168
 
 
169
  analysis = calculate_micro_price_structure(
170
+ diff_x, diff_y_net, mid, best_bid, best_ask,
 
 
 
 
171
  {"bids": bid_walls, "asks": ask_walls}
172
  )
173
 
 
174
  if analysis:
 
175
  if not market_state['pred_history'] or (now - market_state['pred_history'][-1]['t'] > 0.5):
176
  market_state['pred_history'].append({'t': now, 'p': analysis['projected']})
177
  if len(market_state['pred_history']) > HISTORY_LENGTH:
 
181
  "mid": mid,
182
  "history": market_state['history'],
183
  "pred_history": market_state['pred_history'],
184
+ "trade_history": market_state['trade_vol_history'],
185
  "depth_x": diff_x,
186
  "depth_net": diff_y_net,
187
  "depth_bids": chart_bids,
 
209
  --green: #00ff9d;
210
  --red: #ff3b3b;
211
  --blue: #2979ff;
212
+ --yellow: #ffeb3b;
213
  }}
214
  body {{
215
  margin: 0; padding: 0;
 
268
  padding: 20px;
269
  display: flex;
270
  flex-direction: column;
271
+ gap: 15px; /* Tighter gap to fit the new chart */
272
  border-left: 1px solid var(--border);
273
+ overflow-y: hidden;
274
  }}
275
 
276
  .chart-header {{
 
298
  .c-red {{ color: var(--red); }}
299
  .c-dim {{ color: var(--text-dim); }}
300
 
301
+ .list-container {{ display: flex; flex-direction: column; gap: 8px; overflow-y: auto; max-height: 120px; }}
302
  .list-item {{
303
  display: flex; justify-content: space-between;
304
  font-family: 'JetBrains Mono', monospace;
 
308
  }}
309
  .list-item span:first-child {{ color: #e0e0e0; }}
310
  .list-item:last-child {{ border: none; }}
311
+
312
+ #sidebar-chart {{
313
+ flex: 1;
314
+ background: rgba(255,255,255,0.02);
315
+ border: 1px solid var(--border);
316
+ border-radius: 4px;
317
+ min-height: 100px;
318
+ }}
319
  </style>
320
  </head>
321
  <body>
 
331
  </div>
332
 
333
  <div id="p-chart" class="panel">
334
+ <div class="chart-header">PRICE ACTION (BLUE) // PREDICTION (YELLOW)</div>
335
  <div id="tv-price" style="flex: 1; width: 100%;"></div>
336
  </div>
337
 
338
  <div id="p-depth">
339
  <div class="depth-sub">
340
+ <div class="chart-header">LIQUIDITY DENSITY</div>
341
  <div id="tv-raw" style="flex: 1; width: 100%;"></div>
342
  </div>
343
  <div class="depth-sub">
344
+ <div class="chart-header">ORDER FLOW IMBALANCE</div>
345
  <div id="tv-net" style="flex: 1; width: 100%;"></div>
346
  </div>
347
  </div>
 
349
  <div id="p-sidebar" class="panel">
350
 
351
  <div class="data-group">
352
+ <span class="label">Micro-Price Delta</span>
353
  <div style="display:flex; align-items: baseline; gap: 10px;">
354
  <span id="proj-pct" class="value value-lg">--%</span>
355
  <span id="proj-val" class="value-sub">---</span>
 
359
  <div class="divider"></div>
360
 
361
  <div class="data-group">
362
+ <span class="label">OFI Imbalance Ratio</span>
363
  <span id="score-val" class="value">0.00</span>
 
364
  </div>
365
 
366
  <div class="divider"></div>
367
 
368
+ <div class="data-group">
369
+ <span class="label">Detected Walls (Z > 3.0)</span>
370
  <div id="wall-list" class="list-container">
371
+ <span class="c-dim" style="font-size: 11px;">Scanning...</span>
372
  </div>
373
  </div>
374
+
375
+ <!-- NEW CHART UNDER WALLS -->
376
+ <div class="data-group" style="flex: 1; display:flex; flex-direction:column;">
377
+ <span class="label">Real-time Volume (Ticks)</span>
378
+ <div id="sidebar-chart"></div>
379
  </div>
380
  </div>
381
  </div>
 
403
  crosshair: {{ mode: 1, vertLine: {{ color: '#444', labelBackgroundColor: '#444' }}, horzLine: {{ color: '#444', labelBackgroundColor: '#444' }} }}
404
  }};
405
 
406
+ // 1. MAIN PRICE CHART
407
  const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts);
408
+ const priceSeries = priceChart.addLineSeries({{ color: '#2979ff', lineWidth: 2, title: 'Price' }}); // BLUE
409
+ const predSeries = priceChart.addLineSeries({{ color: '#ffeb3b', lineWidth: 2, lineStyle: 2, title: 'Forecast' }}); // YELLOW
410
 
411
+ // 2. DEPTH CHARTS
412
  const rawChart = LightweightCharts.createChart(document.getElementById('tv-raw'), {{
413
+ ...chartOpts, localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
 
414
  }});
415
  const bidSeries = rawChart.addAreaSeries({{ lineColor: '#00ff9d', topColor: 'rgba(0, 255, 157, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
416
  const askSeries = rawChart.addAreaSeries({{ lineColor: '#ff3b3b', topColor: 'rgba(255, 59, 59, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
417
 
418
  const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{
419
+ ...chartOpts, localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
 
420
  }});
421
  const netSeries = netChart.addHistogramSeries({{ color: '#2979ff' }});
422
 
423
+ // 3. SIDEBAR VOLUME CHART
424
+ const volChart = LightweightCharts.createChart(document.getElementById('sidebar-chart'), {{
425
+ ...chartOpts,
426
+ grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }},
427
+ rightPriceScale: {{ visible: false }},
428
+ timeScale: {{ visible: false }},
429
+ handleScroll: false,
430
+ handleScale: false
431
+ }});
432
+ const volBuySeries = volChart.addHistogramSeries({{ color: '#00ff9d' }});
433
+ const volSellSeries = volChart.addHistogramSeries({{ color: '#ff3b3b' }});
434
+
435
  let activeLines = [];
436
 
437
+ // RESIZE OBSERVER
438
  new ResizeObserver(entries => {{
439
  for(let entry of entries) {{
440
  const {{width, height}} = entry.contentRect;
441
  if(entry.target.id === 'tv-price') priceChart.applyOptions({{width, height}});
442
  if(entry.target.id === 'tv-raw') rawChart.applyOptions({{width, height}});
443
  if(entry.target.id === 'tv-net') netChart.applyOptions({{width, height}});
444
+ if(entry.target.id === 'sidebar-chart') volChart.applyOptions({{width, height}});
445
  }}
446
  }}).observe(document.body);
447
 
448
+ ['tv-price', 'tv-raw', 'tv-net', 'sidebar-chart'].forEach(id => {{
449
  new ResizeObserver(e => {{
450
+ const t = document.getElementById(id);
451
+ if(id === 'tv-price') priceChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
452
+ if(id === 'tv-raw') rawChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
453
+ if(id === 'tv-net') netChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
454
+ if(id === 'sidebar-chart') volChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
455
  }}).observe(document.getElementById(id));
456
  }});
457
 
 
472
 
473
  if (data.analysis) {{
474
  const proj = data.analysis.projected;
 
475
  const rho = data.analysis.rho;
476
 
477
  predSeries.setData([
 
492
  }}
493
  }}
494
 
495
+ // WALLS
496
  if (data.walls) {{
497
  activeLines.forEach(l => priceSeries.removePriceLine(l));
498
  activeLines = [];
 
509
 
510
  data.walls.asks.forEach(w => addWall(w, 'ASK'));
511
  data.walls.bids.forEach(w => addWall(w, 'BID'));
512
+ dom.wallList.innerHTML = html || '<span class="c-dim" style="font-size:11px">Scanning...</span>';
513
+ }}
514
+
515
+ // VOLUME CHART IN SIDEBAR
516
+ if (data.trade_history && data.trade_history.length) {{
517
+ const buyData = [];
518
+ const sellData = [];
519
+ data.trade_history.forEach(t => {{
520
+ const time = Math.floor(t.t);
521
+ buyData.push({{ time: time, value: t.buy }});
522
+ sellData.push({{ time: time, value: t.sell }});
523
+ }});
524
+ // Ensure unique time points for LW Charts
525
+ const uniqueBuys = [...new Map(buyData.map(i => [i.time, i])).values()];
526
+ const uniqueSells = [...new Map(sellData.map(i => [i.time, i])).values()];
527
 
528
+ volBuySeries.setData(uniqueBuys);
529
+ volSellSeries.setData(uniqueSells);
530
  }}
531
 
532
+ // DEPTH
533
  if (data.depth_x.length) {{
534
  const bids = [], asks = [], nets = [];
535
  for(let i=0; i<data.depth_x.length; i++) {{
 
560
  try:
561
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
562
  logging.info(f"🔌 Connected to Kraken ({SYMBOL_KRAKEN})")
563
+
564
+ # SUBSCRIBE TO BOOK AND TRADES
565
  await ws.send(json.dumps({
566
  "method": "subscribe",
567
  "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}
568
  }))
569
+ await ws.send(json.dumps({
570
+ "method": "subscribe",
571
+ "params": {"channel": "trade", "symbol": [SYMBOL_KRAKEN]}
572
+ }))
573
 
574
  async for message in ws:
575
  payload = json.loads(message)
 
600
  market_state['history'].append({'t': now, 'p': mid})
601
  if len(market_state['history']) > HISTORY_LENGTH:
602
  market_state['history'].pop(0)
603
+
604
+ elif channel == "trade":
605
+ # Process trades for volume history
606
+ for trade in data:
607
+ # Kraken Trade format: [price, qty, time, side, order_type, misc]
608
+ # side: 'buy' or 'sell'
609
+ try:
610
+ qty = float(trade['qty'])
611
+ side = trade['side'] # 'buy' or 'sell'
612
+ if side == 'buy':
613
+ market_state['current_vol_window']['buy'] += qty
614
+ else:
615
+ market_state['current_vol_window']['sell'] += qty
616
+ except:
617
+ pass
618
 
619
  except Exception as e:
620
  logging.warning(f"⚠️ Reconnecting: {e}")