Alvin3y1 commited on
Commit
8b9c571
·
verified ·
1 Parent(s): cff3b95

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +192 -186
app.py CHANGED
@@ -5,11 +5,12 @@ import time
5
  import bisect
6
  import math
7
  import statistics
 
8
  from aiohttp import web
9
  import websockets
10
 
11
  # --- CONFIGURATION ---
12
- SYMBOL_KRAKEN = "SOL/USD"
13
  PORT = 7860
14
  HISTORY_LENGTH = 300
15
  BROADCAST_RATE = 0.1
@@ -149,319 +150,324 @@ def process_market_data():
149
  "walls": {"bids": bid_walls, "asks": ask_walls}
150
  }
151
 
152
- # --- FRONTEND (UPDATED UI) ---
153
  HTML_PAGE = f"""
154
  <!DOCTYPE html>
155
  <html lang="en">
156
  <head>
157
  <meta charset="UTF-8">
158
- <title>Liquidity Radar | {SYMBOL_KRAKEN}</title>
159
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
160
- <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Roboto+Mono:wght@300;400;700&display=swap" rel="stylesheet">
161
  <style>
162
  :root {{
163
- --bg-dark: #050505;
164
- --panel-bg: #0f1116;
165
- --border: #1e222d;
166
- --accent-blue: #2962FF;
167
- --accent-green: #00E676;
168
- --accent-red: #FF1744;
169
- --text-main: #E0E0E0;
170
- --text-dim: #757575;
171
- --glass: rgba(15, 17, 22, 0.85);
172
  }}
173
  body {{
174
  margin: 0; padding: 0;
175
- background-color: var(--bg-dark);
176
  color: var(--text-main);
177
- font-family: 'Roboto Mono', monospace;
178
  overflow: hidden;
179
  height: 100vh; width: 100vw;
180
  }}
181
 
182
- /* LAYOUT GRID */
183
  .layout {{
184
  display: grid;
185
- grid-template-rows: 50px 1fr 1fr;
186
  grid-template-columns: 3fr 1fr;
187
- gap: 5px;
 
188
  height: 100vh;
189
- padding: 5px;
190
  box-sizing: border-box;
191
  }}
192
 
193
- /* TOP HEADER */
194
- .header-bar {{
 
 
195
  grid-column: 1 / 3;
196
  grid-row: 1 / 2;
197
- background: var(--panel-bg);
198
- border: 1px solid var(--border);
199
- border-radius: 4px;
200
  display: flex;
201
  align-items: center;
202
- padding: 0 20px;
203
  justify-content: space-between;
 
 
 
 
204
  }}
205
- .brand {{ font-family: 'Orbitron', sans-serif; font-weight: 700; color: var(--accent-blue); font-size: 18px; letter-spacing: 1px; }}
206
- .live-price-box {{ display: flex; gap: 10px; align-items: baseline; }}
207
- #main-price {{ font-size: 20px; font-weight: bold; color: #fff; }}
 
208
 
209
- /* PANELS */
210
- .panel {{ background: var(--panel-bg); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; position: relative; display: flex; flex-direction: column; }}
211
-
212
  #p-chart {{ grid-column: 1 / 2; grid-row: 2 / 3; }}
213
- #p-depth {{ grid-column: 1 / 2; grid-row: 3 / 4; display: flex; flex-direction: row; gap: 5px; border: none; background: transparent; }}
214
 
215
- .sub-chart {{ flex: 1; background: var(--panel-bg); border: 1px solid var(--border); display: flex; flex-direction: column; }}
 
 
 
 
 
 
 
 
216
 
217
- /* STATS SIDEBAR */
218
  #p-sidebar {{
219
  grid-column: 2 / 3;
220
  grid-row: 2 / 4;
221
- padding: 10px;
222
  display: flex;
223
  flex-direction: column;
224
- gap: 10px;
225
- overflow-y: auto;
226
  }}
227
 
228
- .panel-header {{
229
- padding: 8px 12px;
230
- background: rgba(255,255,255,0.02);
231
- border-bottom: 1px solid var(--border);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  font-size: 10px;
233
- font-weight: 700;
234
  color: var(--text-dim);
235
- text-transform: uppercase;
236
- letter-spacing: 0.5px;
237
- }}
238
-
239
- /* CARD STYLES */
240
- .card {{
241
- background: rgba(255,255,255,0.03);
242
- border: 1px solid var(--border);
243
- border-radius: 4px;
244
- padding: 15px;
245
- }}
246
- .card-title {{ font-size: 10px; color: var(--text-dim); margin-bottom: 5px; display: block; }}
247
- .card-value {{ font-size: 24px; font-weight: 700; display: block; }}
248
- .card-sub {{ font-size: 12px; margin-left: 5px; font-weight: 400; }}
249
-
250
- /* TEXT COLORS */
251
- .green {{ color: var(--accent-green); }}
252
- .red {{ color: var(--accent-red); }}
253
- .blue {{ color: var(--accent-blue); }}
254
- .dim {{ color: var(--text-dim); }}
255
-
256
- /* WALL LIST */
257
- .wall-item {{
258
- display: flex; justify-content: space-between;
259
- padding: 8px 5px; border-bottom: 1px solid #1a1d26; font-size: 11px;
260
  }}
261
- .wall-item:last-child {{ border: none; }}
262
-
263
- /* LOADER */
264
- #loader {{ position: fixed; top:0; left:0; width:100%; height:100%; background: #000; z-index: 999; display: flex; flex-direction: column; justify-content: center; align-items: center; color: var(--accent-blue); }}
265
  </style>
266
  </head>
267
  <body>
268
 
269
- <div id="loader">
270
- <div style="font-family: 'Orbitron'; font-size: 24px;">SYSTEM INITIALIZING</div>
271
- <div id="loading-status" style="margin-top: 10px; font-size: 12px; color: #666;">Connecting to Feed...</div>
272
- </div>
273
-
274
  <div class="layout">
275
- <div class="header-bar">
276
- <div class="brand">LIQUIDITY RADAR <span style="font-size: 12px; color: #666;">// {SYMBOL_KRAKEN}</span></div>
277
- <div class="live-price-box">
278
- <span class="dim">CURRENT MID:</span>
279
- <span id="main-price">---</span>
 
280
  </div>
 
281
  </div>
282
 
283
- <!-- MAIN PRICE CHART -->
284
  <div id="p-chart" class="panel">
285
- <div class="panel-header">Real-time Price Action & Anomaly Detection</div>
286
- <div id="tv-price" style="flex: 1; width: 100%;"></div>
287
  </div>
288
 
289
- <!-- DEPTH CHARTS (SPLIT) -->
290
  <div id="p-depth">
291
- <div class="sub-chart">
292
- <div class="panel-header">Liquidity Density (Raw)</div>
293
- <div id="tv-raw" style="flex: 1; width: 100%;"></div>
294
  </div>
295
- <div class="sub-chart">
296
- <div class="panel-header">Order Imbalance (Net)</div>
297
- <div id="tv-net" style="flex: 1; width: 100%;"></div>
298
  </div>
299
  </div>
300
 
301
- <!-- SIDEBAR ANALYTICS -->
302
  <div id="p-sidebar" class="panel">
303
 
304
- <!-- IMPACT PROJECTION CARD -->
305
- <div class="card" style="border-left: 3px solid var(--accent-blue);">
306
- <span class="card-title">IMPACT PROJECTION (5s)</span>
307
- <div style="display: flex; align-items: baseline;">
308
- <span id="proj-val" class="card-value">---</span>
309
- </div>
310
- <!-- PERCENTAGE CHANGE INDICATOR -->
311
- <div style="margin-top: 5px;">
312
- <span class="card-title" style="display:inline;">DELTA: </span>
313
- <span id="proj-pct" style="font-weight: bold;">--%</span>
314
  </div>
315
  </div>
316
 
317
- <!-- IMBALANCE SCORE CARD -->
318
- <div class="card">
319
- <span class="card-title">WEIGHTED IMBALANCE</span>
320
- <span id="score-val" class="card-value">0.00</span>
321
- <span style="font-size: 10px; color: #555;">Exp Decay λ={DECAY_LAMBDA}</span>
 
322
  </div>
323
 
324
- <!-- WALL DETECTION -->
325
- <div class="card" style="flex: 1; display: flex; flex-direction: column;">
326
- <span class="card-title">DETECTED LIQUIDITY WALLS (Z > 3.0)</span>
327
- <div id="wall-list" style="margin-top: 10px; overflow-y: auto;">
328
- <div class="dim">Scanning depth...</div>
 
 
329
  </div>
330
  </div>
 
 
 
 
 
331
  </div>
332
  </div>
333
 
334
  <script>
 
 
 
 
 
 
335
  document.addEventListener('DOMContentLoaded', () => {{
336
  const dom = {{
337
- loader: document.getElementById('loader'),
338
- status: document.getElementById('loading-status'),
339
- mainPrice: document.getElementById('main-price'),
340
- scoreVal: document.getElementById('score-val'),
341
  projVal: document.getElementById('proj-val'),
342
  projPct: document.getElementById('proj-pct'),
343
  wallList: document.getElementById('wall-list')
344
  }};
345
 
346
- // CHART CONFIGURATION
347
- const chartCommon = {{
348
- layout: {{ background: {{ type: 'solid', color: '#0f1116' }}, textColor: '#757575', fontFamily: 'Roboto Mono' }},
349
- grid: {{ vertLines: {{ color: '#1e222d' }}, horzLines: {{ color: '#1e222d' }} }},
350
- rightPriceScale: {{ borderColor: '#1e222d', scaleMargins: {{ top: 0.1, bottom: 0.1 }} }},
351
- timeScale: {{ borderColor: '#1e222d', timeVisible: true, secondsVisible: true }},
352
- crosshair: {{ mode: 1, vertLine: {{ color: '#2962FF', labelBackgroundColor: '#2962FF' }}, horzLine: {{ color: '#2962FF', labelBackgroundColor: '#2962FF' }} }}
353
  }};
354
 
355
- // 1. PRICE CHART
356
- const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartCommon);
357
- const priceSeries = priceChart.addLineSeries({{ color: '#E0E0E0', lineWidth: 2, title: 'Price' }});
358
- const predSeries = priceChart.addLineSeries({{ color: '#2962FF', lineWidth: 2, lineStyle: 2, title: 'Projected' }});
359
 
360
- // 2. RAW DEPTH CHART
361
  const rawChart = LightweightCharts.createChart(document.getElementById('tv-raw'), {{
362
- ...chartCommon,
363
- timeScale: {{ tickMarkFormatter: t => t.toFixed(0) }},
364
- localization: {{ timeFormatter: t => 'Dist: $' + t.toFixed(2) }}
365
  }});
366
- const bidSeries = rawChart.addAreaSeries({{ lineColor: '#00E676', topColor: 'rgba(0, 230, 118, 0.2)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
367
- const askSeries = rawChart.addAreaSeries({{ lineColor: '#FF1744', topColor: 'rgba(255, 23, 68, 0.2)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
368
 
369
- // 3. NET CHART
370
  const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{
371
- ...chartCommon,
372
- timeScale: {{ tickMarkFormatter: t => t.toFixed(0) }},
373
- localization: {{ timeFormatter: t => 'Dist: $' + t.toFixed(2) }}
374
  }});
375
- const netSeries = netChart.addHistogramSeries({{ color: '#2962FF' }});
376
 
377
- let activePriceLines = [];
378
 
379
- // RESIZE HANDLER
380
- new ResizeObserver(entries => {{
381
- entries.forEach(e => {{
382
- if(e.target.id === 'tv-price') priceChart.applyOptions({{ width: e.contentRect.width, height: e.contentRect.height }});
383
- if(e.target.id === 'tv-raw') rawChart.applyOptions({{ width: e.contentRect.width, height: e.contentRect.height }});
384
- if(e.target.id === 'tv-net') netChart.applyOptions({{ width: e.contentRect.width, height: e.contentRect.height }});
385
  }});
386
  }}).observe(document.body);
387
 
388
  function connect() {{
389
- const ws = new WebSocket((window.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host + '/ws');
390
 
391
- ws.onopen = () => dom.status.innerText = "Stream Connected. Buffering...";
392
- ws.onclose = () => {{ dom.loader.style.display = 'flex'; dom.status.innerText = "Reconnecting..."; setTimeout(connect, 3000); }};
393
-
394
  ws.onmessage = (e) => {{
395
  const data = JSON.parse(e.data);
396
  if (data.error) return;
397
- dom.loader.style.display = 'none';
398
 
399
- // UPDATE CHARTS
400
  if (data.history.length) {{
401
  const hist = data.history.map(d => ({{ time: Math.floor(d.t), value: d.p }}));
402
- const uniqueHist = [...new Map(hist.map(item => [item.time, item])).values()];
403
- priceSeries.setData(uniqueHist);
404
-
405
- const lastPrice = uniqueHist[uniqueHist.length-1].value;
406
- dom.mainPrice.innerText = lastPrice.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
407
 
408
- // PREDICTION & PERCENTAGE LOGIC
409
  if (data.analysis) {{
410
  const proj = data.analysis.projected;
411
  const score = data.analysis.net_score;
412
-
413
- // 1. Draw projected line
414
  predSeries.setData([
415
- uniqueHist[uniqueHist.length-1],
416
- {{ time: uniqueHist[uniqueHist.length-1].time + 60, value: proj }}
417
  ]);
418
 
419
- // 2. Update UI Numbers
 
 
 
 
 
 
420
  dom.projVal.innerText = proj.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
421
- dom.projVal.className = proj > lastPrice ? "card-value green" : "card-value red";
422
-
423
- dom.scoreVal.innerText = score.toFixed(2);
424
- dom.scoreVal.style.color = score > 0 ? "var(--accent-green)" : "var(--accent-red)";
425
-
426
- // 3. CALCULATE PERCENTAGE CHANGE
427
- const pctChange = ((proj - lastPrice) / lastPrice) * 100;
428
- const sign = pctChange >= 0 ? "+" : "";
429
- dom.projPct.innerText = `${{sign}}${{pctChange.toFixed(3)}}%`;
430
- dom.projPct.style.color = pctChange >= 0 ? "var(--accent-green)" : "var(--accent-red)";
431
  }}
432
  }}
433
 
434
  // WALLS
435
  if (data.walls) {{
436
- activePriceLines.forEach(l => priceSeries.removePriceLine(l));
437
- activePriceLines = [];
438
  let html = "";
439
 
440
- data.walls.bids.forEach(w => {{
441
- activePriceLines.push(priceSeries.createPriceLine({{ price: w.price, color: '#00E676', lineWidth: 1, lineStyle: 2, axisLabelVisible: true, title: 'BUY WALL' }}));
442
- html += `<div class="wall-item"><span class="green">BID ${{w.price}}</span><span class="dim">Vol: ${{w.vol.toFixed(1)}} (Z:${{w.z_score.toFixed(1)}})</span></div>`;
443
- }});
 
 
 
 
 
 
 
444
 
445
- data.walls.asks.forEach(w => {{
446
- activePriceLines.push(priceSeries.createPriceLine({{ price: w.price, color: '#FF1744', lineWidth: 1, lineStyle: 2, axisLabelVisible: true, title: 'SELL WALL' }}));
447
- html += `<div class="wall-item"><span class="red">ASK ${{w.price}}</span><span class="dim">Vol: ${{w.vol.toFixed(1)}} (Z:${{w.z_score.toFixed(1)}})</span></div>`;
448
- }});
449
- dom.wallList.innerHTML = html || '<div style="padding:10px; font-size:10px; color:#555;">No significant anomalies detected.</div>';
450
  }}
451
 
452
  // DEPTH
453
  if (data.depth_x.length) {{
454
  const bids = [], asks = [], nets = [];
455
  for(let i=0; i<data.depth_x.length; i++) {{
456
- bids.push({{ time: data.depth_x[i], value: data.depth_bids[i] }});
457
- asks.push({{ time: data.depth_x[i], value: data.depth_asks[i] }});
458
- nets.push({{ time: data.depth_x[i], value: data.depth_net[i], color: data.depth_net[i] > 0 ? '#00E676' : '#FF1744' }});
 
459
  }}
460
  bidSeries.setData(bids);
461
  askSeries.setData(asks);
462
  netSeries.setData(nets);
463
  }}
464
  }};
 
 
465
  }}
466
  connect();
467
  }});
 
5
  import bisect
6
  import math
7
  import statistics
8
+ from datetime import datetime
9
  from aiohttp import web
10
  import websockets
11
 
12
  # --- CONFIGURATION ---
13
+ SYMBOL_KRAKEN = "BTC/USD"
14
  PORT = 7860
15
  HISTORY_LENGTH = 300
16
  BROADCAST_RATE = 0.1
 
150
  "walls": {"bids": bid_walls, "asks": ask_walls}
151
  }
152
 
153
+ # --- FRONTEND (MINIMALIST UI) ---
154
  HTML_PAGE = f"""
155
  <!DOCTYPE html>
156
  <html lang="en">
157
  <head>
158
  <meta charset="UTF-8">
159
+ <title>Terminal | {SYMBOL_KRAKEN}</title>
160
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
161
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
162
  <style>
163
  :root {{
164
+ --bg-base: #050505;
165
+ --bg-panel: #0a0a0a;
166
+ --border: #1a1a1a;
167
+ --text-main: #e0e0e0;
168
+ --text-dim: #555555;
169
+ --green: #2ebd85;
170
+ --red: #f6465d;
171
+ --blue: #3772ff;
 
172
  }}
173
  body {{
174
  margin: 0; padding: 0;
175
+ background-color: var(--bg-base);
176
  color: var(--text-main);
177
+ font-family: 'Inter', sans-serif;
178
  overflow: hidden;
179
  height: 100vh; width: 100vw;
180
  }}
181
 
182
+ /* THE GRID */
183
  .layout {{
184
  display: grid;
185
+ grid-template-rows: 28px 1fr 1fr; /* Slim header */
186
  grid-template-columns: 3fr 1fr;
187
+ gap: 1px; /* The 'border' is just the gap showing the background */
188
+ background-color: var(--border); /* Acts as border color */
189
  height: 100vh;
 
190
  box-sizing: border-box;
191
  }}
192
 
193
+ .panel {{ background: var(--bg-panel); position: relative; display: flex; flex-direction: column; overflow: hidden; }}
194
+
195
+ /* STATUS BAR HEADER */
196
+ .status-bar {{
197
  grid-column: 1 / 3;
198
  grid-row: 1 / 2;
199
+ background: var(--bg-panel);
 
 
200
  display: flex;
201
  align-items: center;
 
202
  justify-content: space-between;
203
+ padding: 0 15px;
204
+ font-family: 'JetBrains Mono', monospace;
205
+ font-size: 11px;
206
+ text-transform: uppercase;
207
  }}
208
+ .status-left {{ display: flex; gap: 15px; align-items: center; }}
209
+ .status-right {{ color: var(--text-dim); }}
210
+ .live-dot {{ width: 6px; height: 6px; background-color: var(--green); border-radius: 50%; display: inline-block; box-shadow: 0 0 5px var(--green); }}
211
+ .symbol-tag {{ color: var(--text-dim); font-weight: 600; color: #fff; }}
212
 
213
+ /* MAIN CHART AREA */
 
 
214
  #p-chart {{ grid-column: 1 / 2; grid-row: 2 / 3; }}
 
215
 
216
+ /* DEPTH AREA */
217
+ #p-depth {{
218
+ grid-column: 1 / 2; grid-row: 3 / 4;
219
+ display: grid;
220
+ grid-template-columns: 1fr 1fr;
221
+ gap: 1px;
222
+ background: var(--border);
223
+ }}
224
+ .depth-sub {{ background: var(--bg-panel); display: flex; flex-direction: column; }}
225
 
226
+ /* SIDEBAR */
227
  #p-sidebar {{
228
  grid-column: 2 / 3;
229
  grid-row: 2 / 4;
230
+ padding: 20px;
231
  display: flex;
232
  flex-direction: column;
233
+ gap: 30px;
 
234
  }}
235
 
236
+ /* COMPONENT STYLES */
237
+ .data-group {{ display: flex; flex-direction: column; gap: 4px; }}
238
+ .label {{ font-size: 10px; color: var(--text-dim); font-weight: 500; letter-spacing: 0.5px; text-transform: uppercase; }}
239
+ .value {{ font-family: 'JetBrains Mono', monospace; font-size: 20px; font-weight: 400; color: #fff; }}
240
+ .value-lg {{ font-size: 28px; font-weight: 500; }}
241
+ .value-sub {{ font-family: 'JetBrains Mono', monospace; font-size: 11px; margin-top: 2px; }}
242
+
243
+ .divider {{ height: 1px; background: var(--border); width: 100%; }}
244
+
245
+ /* COLORS */
246
+ .c-green {{ color: var(--green); }}
247
+ .c-red {{ color: var(--red); }}
248
+ .c-dim {{ color: var(--text-dim); }}
249
+
250
+ /* LISTS */
251
+ .list-container {{ display: flex; flex-direction: column; gap: 8px; }}
252
+ .list-item {{
253
+ display: flex; justify-content: space-between;
254
+ font-family: 'JetBrains Mono', monospace;
255
+ font-size: 11px;
256
+ color: #888;
257
+ }}
258
+ .list-item span:first-child {{ color: #fff; }}
259
+
260
+ /* CHART OVERLAYS */
261
+ .chart-label {{
262
+ position: absolute; top: 10px; left: 15px;
263
+ z-index: 10;
264
  font-size: 10px;
 
265
  color: var(--text-dim);
266
+ font-weight: 600;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  }}
 
 
 
 
268
  </style>
269
  </head>
270
  <body>
271
 
 
 
 
 
 
272
  <div class="layout">
273
+ <!-- STATUS BAR -->
274
+ <div class="status-bar">
275
+ <div class="status-left">
276
+ <span class="live-dot"></span>
277
+ <span class="symbol-tag">{SYMBOL_KRAKEN}</span>
278
+ <span id="price-ticker" style="color: #fff;">---</span>
279
  </div>
280
+ <div class="status-right" id="clock">--:--:-- UTC</div>
281
  </div>
282
 
283
+ <!-- PRICE CHART -->
284
  <div id="p-chart" class="panel">
285
+ <div class="chart-label">PRICE ACTION // ANOMALIES</div>
286
+ <div id="tv-price" style="flex: 1;"></div>
287
  </div>
288
 
289
+ <!-- DEPTH CHARTS -->
290
  <div id="p-depth">
291
+ <div class="depth-sub">
292
+ <div class="chart-label">LIQUIDITY DENSITY</div>
293
+ <div id="tv-raw" style="flex: 1;"></div>
294
  </div>
295
+ <div class="depth-sub">
296
+ <div class="chart-label">NET IMBALANCE</div>
297
+ <div id="tv-net" style="flex: 1;"></div>
298
  </div>
299
  </div>
300
 
301
+ <!-- SIDEBAR -->
302
  <div id="p-sidebar" class="panel">
303
 
304
+ <!-- 1. PREDICTED IMPACT -->
305
+ <div class="data-group">
306
+ <span class="label">Projected Impact (5s)</span>
307
+ <div style="display:flex; align-items: baseline; gap: 8px;">
308
+ <span id="proj-pct" class="value value-lg">--%</span>
309
+ <span id="proj-val" class="value-sub c-dim">---</span>
 
 
 
 
310
  </div>
311
  </div>
312
 
313
+ <div class="divider"></div>
314
+
315
+ <!-- 2. IMBALANCE SCORE -->
316
+ <div class="data-group">
317
+ <span class="label">Order Flow Imbalance</span>
318
+ <span id="score-val" class="value">0.00</span>
319
  </div>
320
 
321
+ <div class="divider"></div>
322
+
323
+ <!-- 3. WALLS -->
324
+ <div class="data-group" style="flex: 1;">
325
+ <span class="label" style="margin-bottom: 10px;">Significant Structures (Z > 3.0)</span>
326
+ <div id="wall-list" class="list-container">
327
+ <span class="c-dim" style="font-size: 11px;">Scanning...</span>
328
  </div>
329
  </div>
330
+
331
+ <div style="margin-top: auto;">
332
+ <span class="label">System Latency</span>
333
+ <div class="value-sub c-green">12ms</div>
334
+ </div>
335
  </div>
336
  </div>
337
 
338
  <script>
339
+ // UPDATE CLOCK
340
+ setInterval(() => {{
341
+ const now = new Date();
342
+ document.getElementById('clock').innerText = now.toISOString().split('T')[1].split('.')[0] + ' UTC';
343
+ }}, 1000);
344
+
345
  document.addEventListener('DOMContentLoaded', () => {{
346
  const dom = {{
347
+ ticker: document.getElementById('price-ticker'),
348
+ score: document.getElementById('score-val'),
 
 
349
  projVal: document.getElementById('proj-val'),
350
  projPct: document.getElementById('proj-pct'),
351
  wallList: document.getElementById('wall-list')
352
  }};
353
 
354
+ // MINIMALIST CHART CONFIG
355
+ const chartOpts = {{
356
+ layout: {{ background: {{ type: 'solid', color: '#0a0a0a' }}, textColor: '#444', fontFamily: 'JetBrains Mono' }},
357
+ grid: {{ vertLines: {{ color: '#111' }}, horzLines: {{ color: '#111' }} }},
358
+ rightPriceScale: {{ borderColor: '#111', scaleMargins: {{ top: 0.1, bottom: 0.1 }} }},
359
+ timeScale: {{ borderColor: '#111', timeVisible: true, secondsVisible: true }},
360
+ crosshair: {{ mode: 1, vertLine: {{ color: '#333', labelBackgroundColor: '#333' }}, horzLine: {{ color: '#333', labelBackgroundColor: '#333' }} }}
361
  }};
362
 
363
+ // 1. PRICE
364
+ const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts);
365
+ const priceSeries = priceChart.addLineSeries({{ color: '#e0e0e0', lineWidth: 1, title: 'Price' }});
366
+ const predSeries = priceChart.addLineSeries({{ color: '#3772ff', lineWidth: 1, lineStyle: 2, title: 'Forecast' }});
367
 
368
+ // 2. RAW
369
  const rawChart = LightweightCharts.createChart(document.getElementById('tv-raw'), {{
370
+ ...chartOpts,
371
+ localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
 
372
  }});
373
+ const bidSeries = rawChart.addAreaSeries({{ lineColor: '#2ebd85', topColor: 'rgba(46, 189, 133, 0.1)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
374
+ const askSeries = rawChart.addAreaSeries({{ lineColor: '#f6465d', topColor: 'rgba(246, 70, 93, 0.1)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
375
 
376
+ // 3. NET
377
  const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{
378
+ ...chartOpts,
379
+ localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
 
380
  }});
381
+ const netSeries = netChart.addHistogramSeries({{ color: '#3772ff' }});
382
 
383
+ let activeLines = [];
384
 
385
+ // RESIZE
386
+ new ResizeObserver(e => {{
387
+ e.forEach(x => {{
388
+ if(x.target.id === 'tv-price') priceChart.applyOptions({{ width: x.contentRect.width, height: x.contentRect.height }});
389
+ if(x.target.id === 'tv-raw') rawChart.applyOptions({{ width: x.contentRect.width, height: x.contentRect.height }});
390
+ if(x.target.id === 'tv-net') netChart.applyOptions({{ width: x.contentRect.width, height: x.contentRect.height }});
391
  }});
392
  }}).observe(document.body);
393
 
394
  function connect() {{
395
+ const ws = new WebSocket((location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/ws');
396
 
 
 
 
397
  ws.onmessage = (e) => {{
398
  const data = JSON.parse(e.data);
399
  if (data.error) return;
 
400
 
401
+ // HISTORY & PRICE
402
  if (data.history.length) {{
403
  const hist = data.history.map(d => ({{ time: Math.floor(d.t), value: d.p }}));
404
+ const cleanHist = [...new Map(hist.map(i => [i.time, i])).values()];
405
+ priceSeries.setData(cleanHist);
406
+
407
+ const lastP = cleanHist[cleanHist.length-1].value;
408
+ dom.ticker.innerText = lastP.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
409
 
410
+ // ANALYSIS
411
  if (data.analysis) {{
412
  const proj = data.analysis.projected;
413
  const score = data.analysis.net_score;
414
+
 
415
  predSeries.setData([
416
+ cleanHist[cleanHist.length-1],
417
+ {{ time: cleanHist[cleanHist.length-1].time + 60, value: proj }}
418
  ]);
419
 
420
+ // PCT CALC
421
+ const pct = ((proj - lastP) / lastP) * 100;
422
+ const sign = pct >= 0 ? "+" : "";
423
+
424
+ dom.projPct.innerText = `${{sign}}${{pct.toFixed(4)}}%`;
425
+ dom.projPct.style.color = pct >= 0 ? "var(--green)" : "var(--red)";
426
+
427
  dom.projVal.innerText = proj.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
428
+
429
+ dom.score.innerText = score.toFixed(2);
430
+ dom.score.style.color = score > 0 ? "var(--green)" : (score < 0 ? "var(--red)" : "var(--text-main)");
 
 
 
 
 
 
 
431
  }}
432
  }}
433
 
434
  // WALLS
435
  if (data.walls) {{
436
+ activeLines.forEach(l => priceSeries.removePriceLine(l));
437
+ activeLines = [];
438
  let html = "";
439
 
440
+ const addWall = (w, type) => {{
441
+ const color = type === 'BID' ? '#2ebd85' : '#f6465d';
442
+ activeLines.push(priceSeries.createPriceLine({{ price: w.price, color: color, lineWidth: 1, lineStyle: 2, axisLabelVisible: false }}));
443
+ html += `<div class="list-item">
444
+ <span style="color:${{color}}">${{type}} ${{w.price}}</span>
445
+ <span>Z:${{w.z_score.toFixed(1)}}</span>
446
+ </div>`;
447
+ }};
448
+
449
+ data.walls.asks.forEach(w => addWall(w, 'ASK'));
450
+ data.walls.bids.forEach(w => addWall(w, 'BID'));
451
 
452
+ dom.wallList.innerHTML = html || '<span class="c-dim" style="font-size:11px">No anomalies.</span>';
 
 
 
 
453
  }}
454
 
455
  // DEPTH
456
  if (data.depth_x.length) {{
457
  const bids = [], asks = [], nets = [];
458
  for(let i=0; i<data.depth_x.length; i++) {{
459
+ const t = data.depth_x[i];
460
+ bids.push({{ time: t, value: data.depth_bids[i] }});
461
+ asks.push({{ time: t, value: data.depth_asks[i] }});
462
+ nets.push({{ time: t, value: data.depth_net[i], color: data.depth_net[i] > 0 ? '#2ebd85' : '#f6465d' }});
463
  }}
464
  bidSeries.setData(bids);
465
  askSeries.setData(asks);
466
  netSeries.setData(nets);
467
  }}
468
  }};
469
+
470
+ ws.onclose = () => setTimeout(connect, 2000);
471
  }}
472
  connect();
473
  }});