Alvin3y1 commited on
Commit
1530d0a
·
verified ·
1 Parent(s): a0c3495

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +80 -100
app.py CHANGED
@@ -14,12 +14,7 @@ HISTORY_LENGTH = 300
14
  BROADCAST_RATE = 0.1 # 10Hz updates
15
 
16
  # --- HFT Damping Configuration ---
17
- # DECAY_LAMBDA: Controls how fast "relevance" drops off with distance.
18
- # 100 means an order $100 away has ~36% weight. 50 is tighter (scalping), 200 is wider (swing).
19
  DECAY_LAMBDA = 100.0
20
-
21
- # IMPACT_SENSITIVITY: Converts the weighted volume score into Price Impact ($).
22
- # Multiplier for the Square Root Law.
23
  IMPACT_SENSITIVITY = 0.5
24
 
25
  # --- Logging ---
@@ -30,7 +25,6 @@ market_state = {
30
  "bids": {},
31
  "asks": {},
32
  "history": [], # Price history: {t, p}
33
- "liq_history": [], # Liquidity Trend history: {t, v}
34
  "current_mid": 0.0,
35
  "prev_mid": 0.0,
36
  "ready": False
@@ -42,9 +36,6 @@ connected_clients = set()
42
  def analyze_structure(diff_x, diff_y, current_mid):
43
  """
44
  Applies HFT Spatial Decay and Square Root Market Impact models.
45
- Input:
46
- diff_x: List of distances from mid ($).
47
- diff_y: List of CUMULATIVE Net Liquidity (Bids - Asks).
48
  """
49
  if not diff_y or len(diff_y) < 5:
50
  return None
@@ -55,21 +46,15 @@ def analyze_structure(diff_x, diff_y, current_mid):
55
  # 1. Calculate Spatial Weighted Imbalance
56
  for i in range(len(diff_x)):
57
  dist = diff_x[i]
58
- cum_vol = diff_y[i]
59
 
60
- # Unpack cumulative volume to get marginal volume at this step
61
  marginal_vol = cum_vol - prev_vol
62
  prev_vol = cum_vol
63
 
64
- # Apply Exponential Decay (Spatial Damping)
65
- # Orders close to spread (dist=0) have weight 1.0
66
- # Orders far away decay towards 0.0
67
  weight = math.exp(-dist / DECAY_LAMBDA)
68
-
69
  weighted_imbalance += marginal_vol * weight
70
 
71
- # 2. Calculate Market Impact (Square Root Law)
72
- # Impact is not linear; it follows a square root function of volume.
73
  if weighted_imbalance != 0:
74
  impact = math.sqrt(abs(weighted_imbalance)) * IMPACT_SENSITIVITY
75
  if weighted_imbalance < 0:
@@ -79,11 +64,9 @@ def analyze_structure(diff_x, diff_y, current_mid):
79
 
80
  projected_price = current_mid + impact
81
 
82
- # 3. Structural Reversals (Support/Resistance Scans)
83
- # We still use the raw curve to find "Walls"
84
  support_level = None
85
  resistance_level = None
86
-
87
  scan_limit = len(diff_y) // 2
88
 
89
  for i in range(1, scan_limit):
@@ -91,11 +74,8 @@ def analyze_structure(diff_x, diff_y, current_mid):
91
  curr_val = diff_y[i]
92
  dist = diff_x[i]
93
 
94
- # Resistance: Net Liquidity flips from + to - (Buyer exhaustion / Seller Wall)
95
  if prev_val > 0 and curr_val < 0 and resistance_level is None:
96
  resistance_level = current_mid + dist
97
-
98
- # Support: Net Liquidity flips from - to + (Seller exhaustion / Buyer Wall)
99
  if prev_val < 0 and curr_val > 0 and support_level is None:
100
  support_level = current_mid - dist
101
 
@@ -103,7 +83,7 @@ def analyze_structure(diff_x, diff_y, current_mid):
103
  "projected": projected_price,
104
  "support": support_level,
105
  "resistance": resistance_level,
106
- "net_score": weighted_imbalance # Sending the decay-weighted score
107
  }
108
 
109
  def process_market_data():
@@ -112,7 +92,7 @@ def process_market_data():
112
 
113
  mid = market_state['current_mid']
114
 
115
- # Snapshot Top 300 for Depth Chart
116
  raw_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])[:300]
117
  raw_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])[:300]
118
 
@@ -131,43 +111,38 @@ def process_market_data():
131
  cum += q
132
  d_a_x.append(d); d_a_y.append(cum)
133
 
134
- # Calculate Net Liquidity Curve (Depth)
135
- # We interpolate to ensure bids and asks are compared at the exact same distances
136
  diff_x, diff_y = [], []
 
 
137
  if d_b_x and d_a_x:
138
  max_dist = min(d_b_x[-1], d_a_x[-1])
139
- # Resolution: 100 steps across the available depth
140
  step_size = max_dist / 100
141
  steps = [i * step_size for i in range(1, 101)]
142
 
143
  for s in steps:
144
- # Find cumulative bid vol at distance s
145
  idx_b = bisect.bisect_right(d_b_x, s)
146
  vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
147
 
148
- # Find cumulative ask vol at distance s
149
  idx_a = bisect.bisect_right(d_a_x, s)
150
  vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
151
 
152
  diff_x.append(s)
153
- diff_y.append(vol_b - vol_a) # Cumulative Net Imbalance
 
 
154
 
155
  analysis = analyze_structure(diff_x, diff_y, mid)
156
-
157
- # Store Liquidity Trend for history
158
- now = time.time()
159
- if analysis:
160
- # Update Trend History if needed (throttle slightly to match graph res)
161
- if not market_state['liq_history'] or (now - market_state['liq_history'][-1]['t'] > 0.5):
162
- market_state['liq_history'].append({'t': now, 'v': analysis['net_score']})
163
- if len(market_state['liq_history']) > HISTORY_LENGTH:
164
- market_state['liq_history'].pop(0)
165
 
166
  return {
167
  "mid": mid,
168
- "history": market_state['history'], # Price History
169
- "liq_history": market_state['liq_history'], # Net Liq History
170
- "diff": { "x": diff_x, "y": diff_y }, # Depth Snapshot
 
 
171
  "analysis": analysis
172
  }
173
 
@@ -190,11 +165,11 @@ HTML_PAGE = f"""
190
  }}
191
  body {{ margin: 0; padding: 0; background-color: var(--bg-color); color: var(--text-main); font-family: monospace; overflow: hidden; height: 100vh; width: 100vw; }}
192
 
193
- /* Grid Layout: 3 Rows in Main Column */
194
  .grid-container {{
195
  display: grid;
196
  grid-template-columns: 3fr 1fr;
197
- grid-template-rows: 2fr 1fr 1fr;
198
  gap: 4px;
199
  height: 100vh;
200
  padding: 4px;
@@ -203,14 +178,16 @@ HTML_PAGE = f"""
203
 
204
  .panel {{ background: #12141a; border: 1px solid var(--border); border-radius: 4px; position: relative; display: flex; flex-direction: column; overflow: hidden; }}
205
 
 
206
  #p-price {{ grid-column: 1 / 2; grid-row: 1 / 2; }}
207
- #p-trend {{ grid-column: 1 / 2; grid-row: 2 / 3; }}
208
- #p-depth {{ grid-column: 1 / 2; grid-row: 3 / 4; }}
209
- #p-stats {{ grid-column: 2 / 3; grid-row: 1 / 4; border-left: 2px solid #45a29e; }}
 
210
 
211
  .panel-header {{ padding: 6px 10px; background: #0f1116; border-bottom: 1px solid var(--border); font-size: 11px; font-weight: bold; display: flex; justify-content: space-between; color: var(--accent-green); text-transform: uppercase; }}
212
 
213
- #tv-price, #tv-trend, #tv-depth {{ flex: 1; width: 100%; position: relative; }}
214
 
215
  .stats-content {{ padding: 15px; overflow-y: auto; flex: 1; }}
216
  .stat-box {{ margin-bottom: 20px; padding: 10px; background: rgba(255,255,255,0.02); border-radius: 4px; }}
@@ -219,7 +196,7 @@ HTML_PAGE = f"""
219
  .green {{ color: var(--accent-green); }}
220
  .red {{ color: var(--accent-red); }}
221
 
222
- .terminal-box {{ margin-top: auto; font-size: 11px; height: 200px; display: flex; flex-direction: column; }}
223
  .term-header {{ border-bottom: 1px dashed #444; margin-bottom: 5px; opacity: 0.7; }}
224
  #term-logs {{ flex: 1; overflow-y: hidden; display: flex; flex-direction: column-reverse; }}
225
  .log-line {{ margin-top: 4px; padding-left: 8px; border-left: 2px solid #333; }}
@@ -241,19 +218,21 @@ HTML_PAGE = f"""
241
  <div id="tv-price"></div>
242
  </div>
243
 
244
- <!-- ROW 2: LIQUIDITY TREND -->
245
- <div id="p-trend" class="panel">
246
- <div class="panel-header"><span>HFT Weighted Imbalance (Decay {DECAY_LAMBDA})</span><span id="live-trend">0.0</span></div>
247
- <div id="tv-trend"></div>
248
- </div>
249
-
250
- <!-- ROW 3: DEPTH STRUCTURE -->
251
- <div id="p-depth" class="panel">
252
- <div class="panel-header"><span>Net Liquidity Structure</span><span>Range: $100</span></div>
253
- <div id="tv-depth"></div>
 
 
254
  </div>
255
 
256
- <!-- COL 2: STATS -->
257
  <div id="p-stats" class="panel">
258
  <div class="panel-header">HFT ANALYTICS ENGINE</div>
259
  <div class="stats-content">
@@ -284,7 +263,6 @@ HTML_PAGE = f"""
284
  loader: document.getElementById('loader'),
285
  status: document.getElementById('loading-status'),
286
  price: document.getElementById('live-price'),
287
- trend: document.getElementById('live-trend'),
288
  scoreVal: document.getElementById('score-val'),
289
  resVal: document.getElementById('res-val'),
290
  supVal: document.getElementById('sup-val'),
@@ -307,36 +285,35 @@ HTML_PAGE = f"""
307
  const predSeries = priceChart.addLineSeries({{ color: '#ff9800', lineWidth: 2, lineStyle: 2 }});
308
  let supportLine = null, resistanceLine = null;
309
 
310
- // 2. Trend Chart (Baseline Series)
311
- const trendChart = LightweightCharts.createChart(document.getElementById('tv-trend'), {{
312
  ...chartCommon,
313
- rightPriceScale: {{ scaleMargins: {{ top: 0.1, bottom: 0.1 }} }}
314
- }});
315
- const trendSeries = trendChart.addBaselineSeries({{
316
- baseValue: {{ type: 'price', price: 0 }},
317
- topLineColor: '#66fcf1', topFillColor1: 'rgba(102, 252, 241, 0.28)', topFillColor2: 'rgba(102, 252, 241, 0.05)',
318
- bottomLineColor: '#ff3b3b', bottomFillColor1: 'rgba(255, 59, 59, 0.28)', bottomFillColor2: 'rgba(255, 59, 59, 0.05)',
319
  }});
 
 
 
320
 
321
- // 3. Depth Chart
322
- const depthChart = LightweightCharts.createChart(document.getElementById('tv-depth'), {{
323
  ...chartCommon,
324
  timeScale: {{ tickMarkFormatter: (time) => parseFloat(time).toFixed(0) }},
325
  localization: {{ timeFormatter: (time) => 'Dist: $' + parseFloat(time).toFixed(2) }}
326
  }});
327
- const bullSeries = depthChart.addAreaSeries({{ topColor: 'rgba(102, 252, 241, 0.4)', bottomColor: 'rgba(102, 252, 241, 0.0)', lineColor: '#66fcf1', lineWidth: 2 }});
328
- const bearSeries = depthChart.addAreaSeries({{ topColor: 'rgba(255, 59, 59, 0.4)', bottomColor: 'rgba(255, 59, 59, 0.0)', lineColor: '#ff3b3b', lineWidth: 2 }});
329
 
330
  // Auto-Resize
331
  const resizeObserver = new ResizeObserver(entries => {{
332
  for(let entry of entries) {{
333
  const {{width, height}} = entry.contentRect;
334
  if(entry.target.id === 'tv-price') priceChart.applyOptions({{width, height}});
335
- if(entry.target.id === 'tv-trend') trendChart.applyOptions({{width, height}});
336
- if(entry.target.id === 'tv-depth') depthChart.applyOptions({{width, height}});
337
  }}
338
  }});
339
- ['tv-price', 'tv-trend', 'tv-depth'].forEach(id => resizeObserver.observe(document.getElementById(id)));
340
 
341
  // --- WEBSOCKET ---
342
  function log(msg, type='neutral') {{
@@ -361,7 +338,7 @@ HTML_PAGE = f"""
361
  if (data.error) return;
362
  dom.loader.style.display = 'none';
363
 
364
- // 1. Price Data
365
  const cleanHistory = [];
366
  const seen = new Set();
367
  data.history.forEach(d => {{
@@ -379,9 +356,7 @@ HTML_PAGE = f"""
379
  predSeries.setData([last, {{ time: last.time + 60, value: projected }}]);
380
  dom.projVal.innerText = projected.toLocaleString(undefined, {{minimumFractionDigits: 0, maximumFractionDigits: 0}});
381
 
382
- // Sync Trend Chart Value
383
- dom.trend.innerText = net_score.toFixed(2);
384
- dom.trend.style.color = net_score >= 0 ? 'var(--accent-green)' : 'var(--accent-red)';
385
  dom.scoreVal.innerText = net_score.toFixed(2);
386
  dom.scoreVal.className = net_score > 0 ? "stat-value green" : "stat-value red";
387
 
@@ -403,35 +378,40 @@ HTML_PAGE = f"""
403
  if (resistanceLine) {{ priceSeries.removePriceLine(resistanceLine); resistanceLine = null; }}
404
  }}
405
 
406
- // AI Logs based on Weighted Score
407
  if (Math.abs(net_score) > 20 && Math.random() > 0.98) {{
408
  log(net_score > 0 ? "Momentum: Buying Pressure" : "Momentum: Selling Pressure", net_score > 0 ? 'bull' : 'bear');
409
  }}
410
  }}
411
  }}
412
 
413
- // 2. Liquidity Trend Data
414
- if (data.liq_history) {{
415
- const trendData = [];
416
- const seenT = new Set();
417
- data.liq_history.forEach(d => {{
418
- const t = Math.floor(d.t);
419
- if(!seenT.has(t)) {{ seenT.add(t); trendData.push({{ time: t, value: d.v }}); }}
420
- }});
421
- if (trendData.length) trendSeries.setData(trendData);
422
- }}
423
-
424
- // 3. Depth Snapshot
425
- if (data.diff && data.diff.x.length) {{
426
  const bull = [], bear = [];
427
- for (let i = 0; i < data.diff.x.length; i++) {{
428
- const x = data.diff.x[i];
429
- const y = data.diff.y[i];
430
- if (y >= 0) {{ bull.push({{ time: x, value: y }}); bear.push({{ time: x, value: 0 }}); }}
431
- else {{ bull.push({{ time: x, value: 0 }}); bear.push({{ time: x, value: y }}); }}
 
 
 
 
 
 
 
 
 
 
432
  }}
 
 
433
  bullSeries.setData(bull);
434
  bearSeries.setData(bear);
 
 
 
 
435
  }}
436
  }};
437
  }}
 
14
  BROADCAST_RATE = 0.1 # 10Hz updates
15
 
16
  # --- HFT Damping Configuration ---
 
 
17
  DECAY_LAMBDA = 100.0
 
 
 
18
  IMPACT_SENSITIVITY = 0.5
19
 
20
  # --- Logging ---
 
25
  "bids": {},
26
  "asks": {},
27
  "history": [], # Price history: {t, p}
 
28
  "current_mid": 0.0,
29
  "prev_mid": 0.0,
30
  "ready": False
 
36
  def analyze_structure(diff_x, diff_y, current_mid):
37
  """
38
  Applies HFT Spatial Decay and Square Root Market Impact models.
 
 
 
39
  """
40
  if not diff_y or len(diff_y) < 5:
41
  return None
 
46
  # 1. Calculate Spatial Weighted Imbalance
47
  for i in range(len(diff_x)):
48
  dist = diff_x[i]
49
+ cum_vol = diff_y[i] # diff_y is the Net (Bid - Ask)
50
 
 
51
  marginal_vol = cum_vol - prev_vol
52
  prev_vol = cum_vol
53
 
 
 
 
54
  weight = math.exp(-dist / DECAY_LAMBDA)
 
55
  weighted_imbalance += marginal_vol * weight
56
 
57
+ # 2. Market Impact
 
58
  if weighted_imbalance != 0:
59
  impact = math.sqrt(abs(weighted_imbalance)) * IMPACT_SENSITIVITY
60
  if weighted_imbalance < 0:
 
64
 
65
  projected_price = current_mid + impact
66
 
67
+ # 3. Structural Reversals
 
68
  support_level = None
69
  resistance_level = None
 
70
  scan_limit = len(diff_y) // 2
71
 
72
  for i in range(1, scan_limit):
 
74
  curr_val = diff_y[i]
75
  dist = diff_x[i]
76
 
 
77
  if prev_val > 0 and curr_val < 0 and resistance_level is None:
78
  resistance_level = current_mid + dist
 
 
79
  if prev_val < 0 and curr_val > 0 and support_level is None:
80
  support_level = current_mid - dist
81
 
 
83
  "projected": projected_price,
84
  "support": support_level,
85
  "resistance": resistance_level,
86
+ "net_score": weighted_imbalance
87
  }
88
 
89
  def process_market_data():
 
92
 
93
  mid = market_state['current_mid']
94
 
95
+ # Snapshot Top 300
96
  raw_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])[:300]
97
  raw_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])[:300]
98
 
 
111
  cum += q
112
  d_a_x.append(d); d_a_y.append(cum)
113
 
114
+ # Interpolate for Charts
 
115
  diff_x, diff_y = [], []
116
+ chart_bids, chart_asks = [], [] # Separated arrays for the raw depth chart
117
+
118
  if d_b_x and d_a_x:
119
  max_dist = min(d_b_x[-1], d_a_x[-1])
 
120
  step_size = max_dist / 100
121
  steps = [i * step_size for i in range(1, 101)]
122
 
123
  for s in steps:
124
+ # Interpolate Bid Volume at distance s
125
  idx_b = bisect.bisect_right(d_b_x, s)
126
  vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
127
 
128
+ # Interpolate Ask Volume at distance s
129
  idx_a = bisect.bisect_right(d_a_x, s)
130
  vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
131
 
132
  diff_x.append(s)
133
+ diff_y.append(vol_b - vol_a) # Net
134
+ chart_bids.append(vol_b) # Raw Bid
135
+ chart_asks.append(vol_a) # Raw Ask
136
 
137
  analysis = analyze_structure(diff_x, diff_y, mid)
 
 
 
 
 
 
 
 
 
138
 
139
  return {
140
  "mid": mid,
141
+ "history": market_state['history'],
142
+ "depth_x": diff_x,
143
+ "depth_net": diff_y,
144
+ "depth_bids": chart_bids,
145
+ "depth_asks": chart_asks,
146
  "analysis": analysis
147
  }
148
 
 
165
  }}
166
  body {{ margin: 0; padding: 0; background-color: var(--bg-color); color: var(--text-main); font-family: monospace; overflow: hidden; height: 100vh; width: 100vw; }}
167
 
168
+ /* Grid: 2 Columns, 2 Rows. Bottom Row (Depth) is taller/flexible */
169
  .grid-container {{
170
  display: grid;
171
  grid-template-columns: 3fr 1fr;
172
+ grid-template-rows: 55% 45%;
173
  gap: 4px;
174
  height: 100vh;
175
  padding: 4px;
 
178
 
179
  .panel {{ background: #12141a; border: 1px solid var(--border); border-radius: 4px; position: relative; display: flex; flex-direction: column; overflow: hidden; }}
180
 
181
+ /* Grid Assignments */
182
  #p-price {{ grid-column: 1 / 2; grid-row: 1 / 2; }}
183
+ #p-stats {{ grid-column: 2 / 3; grid-row: 1 / 3; border-left: 2px solid #45a29e; }}
184
+ #p-bottom {{ grid-column: 1 / 2; grid-row: 2 / 3; display: flex; gap: 4px; background: transparent; border: none; }}
185
+
186
+ .sub-panel {{ flex: 1; background: #12141a; border: 1px solid var(--border); border-radius: 4px; display: flex; flex-direction: column; overflow: hidden; }}
187
 
188
  .panel-header {{ padding: 6px 10px; background: #0f1116; border-bottom: 1px solid var(--border); font-size: 11px; font-weight: bold; display: flex; justify-content: space-between; color: var(--accent-green); text-transform: uppercase; }}
189
 
190
+ #tv-price, #tv-raw, #tv-net {{ flex: 1; width: 100%; position: relative; }}
191
 
192
  .stats-content {{ padding: 15px; overflow-y: auto; flex: 1; }}
193
  .stat-box {{ margin-bottom: 20px; padding: 10px; background: rgba(255,255,255,0.02); border-radius: 4px; }}
 
196
  .green {{ color: var(--accent-green); }}
197
  .red {{ color: var(--accent-red); }}
198
 
199
+ .terminal-box {{ margin-top: auto; font-size: 11px; height: 250px; display: flex; flex-direction: column; }}
200
  .term-header {{ border-bottom: 1px dashed #444; margin-bottom: 5px; opacity: 0.7; }}
201
  #term-logs {{ flex: 1; overflow-y: hidden; display: flex; flex-direction: column-reverse; }}
202
  .log-line {{ margin-top: 4px; padding-left: 8px; border-left: 2px solid #333; }}
 
218
  <div id="tv-price"></div>
219
  </div>
220
 
221
+ <!-- ROW 2: SPLIT DEPTH -->
222
+ <div id="p-bottom">
223
+ <!-- LEFT: RAW DEPTH -->
224
+ <div class="sub-panel">
225
+ <div class="panel-header"><span>Market Depth (Bids vs Asks)</span></div>
226
+ <div id="tv-raw"></div>
227
+ </div>
228
+ <!-- RIGHT: NET DELTA -->
229
+ <div class="sub-panel">
230
+ <div class="panel-header"><span>Net Delta (Bids - Asks)</span></div>
231
+ <div id="tv-net"></div>
232
+ </div>
233
  </div>
234
 
235
+ <!-- RIGHT COL: STATS -->
236
  <div id="p-stats" class="panel">
237
  <div class="panel-header">HFT ANALYTICS ENGINE</div>
238
  <div class="stats-content">
 
263
  loader: document.getElementById('loader'),
264
  status: document.getElementById('loading-status'),
265
  price: document.getElementById('live-price'),
 
266
  scoreVal: document.getElementById('score-val'),
267
  resVal: document.getElementById('res-val'),
268
  supVal: document.getElementById('sup-val'),
 
285
  const predSeries = priceChart.addLineSeries({{ color: '#ff9800', lineWidth: 2, lineStyle: 2 }});
286
  let supportLine = null, resistanceLine = null;
287
 
288
+ // 2. Raw Depth Chart (Left Bottom) - TimeScale hacked to show Distance
289
+ const rawChart = LightweightCharts.createChart(document.getElementById('tv-raw'), {{
290
  ...chartCommon,
291
+ timeScale: {{ tickMarkFormatter: (time) => parseFloat(time).toFixed(0) }},
292
+ localization: {{ timeFormatter: (time) => 'Dist: $' + parseFloat(time).toFixed(2) }}
 
 
 
 
293
  }});
294
+ // Bids (Green) vs Asks (Red)
295
+ const rawBidSeries = rawChart.addAreaSeries({{ lineColor: '#00e676', topColor: 'rgba(0, 230, 118, 0.2)', bottomColor: 'rgba(0, 230, 118, 0.0)', lineWidth: 2, title: "Bids" }});
296
+ const rawAskSeries = rawChart.addAreaSeries({{ lineColor: '#ff1744', topColor: 'rgba(255, 23, 68, 0.2)', bottomColor: 'rgba(255, 23, 68, 0.0)', lineWidth: 2, title: "Asks" }});
297
 
298
+ // 3. Net Delta Chart (Right Bottom)
299
+ const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{
300
  ...chartCommon,
301
  timeScale: {{ tickMarkFormatter: (time) => parseFloat(time).toFixed(0) }},
302
  localization: {{ timeFormatter: (time) => 'Dist: $' + parseFloat(time).toFixed(2) }}
303
  }});
304
+ const bullSeries = netChart.addAreaSeries({{ topColor: 'rgba(102, 252, 241, 0.4)', bottomColor: 'rgba(102, 252, 241, 0.0)', lineColor: '#66fcf1', lineWidth: 2 }});
305
+ const bearSeries = netChart.addAreaSeries({{ topColor: 'rgba(255, 59, 59, 0.4)', bottomColor: 'rgba(255, 59, 59, 0.0)', lineColor: '#ff3b3b', lineWidth: 2 }});
306
 
307
  // Auto-Resize
308
  const resizeObserver = new ResizeObserver(entries => {{
309
  for(let entry of entries) {{
310
  const {{width, height}} = entry.contentRect;
311
  if(entry.target.id === 'tv-price') priceChart.applyOptions({{width, height}});
312
+ if(entry.target.id === 'tv-raw') rawChart.applyOptions({{width, height}});
313
+ if(entry.target.id === 'tv-net') netChart.applyOptions({{width, height}});
314
  }}
315
  }});
316
+ ['tv-price', 'tv-raw', 'tv-net'].forEach(id => resizeObserver.observe(document.getElementById(id)));
317
 
318
  // --- WEBSOCKET ---
319
  function log(msg, type='neutral') {{
 
338
  if (data.error) return;
339
  dom.loader.style.display = 'none';
340
 
341
+ // 1. Price Update
342
  const cleanHistory = [];
343
  const seen = new Set();
344
  data.history.forEach(d => {{
 
356
  predSeries.setData([last, {{ time: last.time + 60, value: projected }}]);
357
  dom.projVal.innerText = projected.toLocaleString(undefined, {{minimumFractionDigits: 0, maximumFractionDigits: 0}});
358
 
359
+ // Stats
 
 
360
  dom.scoreVal.innerText = net_score.toFixed(2);
361
  dom.scoreVal.className = net_score > 0 ? "stat-value green" : "stat-value red";
362
 
 
378
  if (resistanceLine) {{ priceSeries.removePriceLine(resistanceLine); resistanceLine = null; }}
379
  }}
380
 
381
+ // AI Logs
382
  if (Math.abs(net_score) > 20 && Math.random() > 0.98) {{
383
  log(net_score > 0 ? "Momentum: Buying Pressure" : "Momentum: Selling Pressure", net_score > 0 ? 'bull' : 'bear');
384
  }}
385
  }}
386
  }}
387
 
388
+ // 2. Depth Charts Update
389
+ if (data.depth_x && data.depth_x.length) {{
 
 
 
 
 
 
 
 
 
 
 
390
  const bull = [], bear = [];
391
+ const rawBids = [], rawAsks = [];
392
+
393
+ for (let i = 0; i < data.depth_x.length; i++) {{
394
+ const x = data.depth_x[i];
395
+ const netY = data.depth_net[i];
396
+ const bidY = data.depth_bids[i];
397
+ const askY = data.depth_asks[i];
398
+
399
+ // Net Chart
400
+ if (netY >= 0) {{ bull.push({{ time: x, value: netY }}); bear.push({{ time: x, value: 0 }}); }}
401
+ else {{ bull.push({{ time: x, value: 0 }}); bear.push({{ time: x, value: netY }}); }}
402
+
403
+ // Raw Chart
404
+ rawBids.push({{ time: x, value: bidY }});
405
+ rawAsks.push({{ time: x, value: askY }});
406
  }}
407
+
408
+ // Update Net
409
  bullSeries.setData(bull);
410
  bearSeries.setData(bear);
411
+
412
+ // Update Raw
413
+ rawBidSeries.setData(rawBids);
414
+ rawAskSeries.setData(rawAsks);
415
  }}
416
  }};
417
  }}