Alvin3y1 commited on
Commit
30fe49c
·
verified ·
1 Parent(s): 669adc5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +137 -140
app.py CHANGED
@@ -3,14 +3,14 @@ import json
3
  import logging
4
  import time
5
  import bisect
6
- from aiohttp import web, WSMsgType
7
  import websockets
8
 
9
  # --- Configuration ---
10
  SYMBOL_KRAKEN = "BTC/USD"
11
  PORT = 7860
12
  HISTORY_LENGTH = 300
13
- BROADCAST_RATE = 0.1 # Broadcast updates every 100ms (10Hz)
14
 
15
  # --- Logging ---
16
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
@@ -19,13 +19,13 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
19
  market_state = {
20
  "bids": {},
21
  "asks": {},
22
- "history": [],
 
23
  "current_mid": 0.0,
24
  "prev_mid": 0.0,
25
  "ready": False
26
  }
27
 
28
- # Set of connected client websockets
29
  connected_clients = set()
30
 
31
  # --- AI Logic Helper ---
@@ -34,7 +34,7 @@ def analyze_structure(diff_x, diff_y, current_mid):
34
  return None
35
 
36
  # 1. Momentum Projection
37
- net_total = diff_y[-1]
38
  momentum_shift = net_total * 0.2
39
  projected_price = current_mid + momentum_shift
40
 
@@ -63,13 +63,12 @@ def analyze_structure(diff_x, diff_y, current_mid):
63
  }
64
 
65
  def process_market_data():
66
- """Calculates the payload to send to clients."""
67
  if not market_state['ready']:
68
  return {"error": "Initializing..."}
69
 
70
  mid = market_state['current_mid']
71
 
72
- # Snapshot Top 300
73
  raw_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])[:300]
74
  raw_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])[:300]
75
 
@@ -88,7 +87,7 @@ def process_market_data():
88
  cum += q
89
  d_a_x.append(d); d_a_y.append(cum)
90
 
91
- # Calculate Net Liquidity Curve
92
  diff_x, diff_y = [], []
93
  if d_b_x and d_a_x:
94
  max_dist = min(d_b_x[-1], d_a_x[-1])
@@ -105,11 +104,21 @@ def process_market_data():
105
  diff_y.append(vol_b - vol_a)
106
 
107
  analysis = analyze_structure(diff_x, diff_y, mid)
 
 
 
 
 
 
 
 
 
108
 
109
  return {
110
  "mid": mid,
111
- "history": market_state['history'],
112
- "diff": { "x": diff_x, "y": diff_y },
 
113
  "analysis": analysis
114
  }
115
 
@@ -120,7 +129,6 @@ HTML_PAGE = f"""
120
  <head>
121
  <meta charset="UTF-8">
122
  <title>AI Liquidity Dashboard | {SYMBOL_KRAKEN}</title>
123
- <!-- Lightweight Charts Pinned Version -->
124
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
125
  <style>
126
  :root {{
@@ -133,16 +141,27 @@ HTML_PAGE = f"""
133
  }}
134
  body {{ margin: 0; padding: 0; background-color: var(--bg-color); color: var(--text-main); font-family: monospace; overflow: hidden; height: 100vh; width: 100vw; }}
135
 
136
- .grid-container {{ display: grid; grid-template-columns: 3fr 1fr; grid-template-rows: 2fr 1fr; gap: 4px; height: 100vh; padding: 4px; box-sizing: border-box; }}
 
 
 
 
 
 
 
 
 
 
137
  .panel {{ background: #12141a; border: 1px solid var(--border); border-radius: 4px; position: relative; display: flex; flex-direction: column; overflow: hidden; }}
138
 
139
  #p-price {{ grid-column: 1 / 2; grid-row: 1 / 2; }}
140
- #p-depth {{ grid-column: 1 / 2; grid-row: 2 / 3; }}
141
- #p-stats {{ grid-column: 2 / 3; grid-row: 1 / 3; border-left: 2px solid #45a29e; }}
 
142
 
143
- .panel-header {{ padding: 8px 12px; background: #0f1116; border-bottom: 1px solid var(--border); font-size: 12px; font-weight: bold; display: flex; justify-content: space-between; color: var(--accent-green); }}
144
 
145
- #tv-price, #tv-depth {{ flex: 1; width: 100%; position: relative; }}
146
 
147
  .stats-content {{ padding: 15px; overflow-y: auto; flex: 1; }}
148
  .stat-box {{ margin-bottom: 20px; padding: 10px; background: rgba(255,255,255,0.02); border-radius: 4px; }}
@@ -151,17 +170,12 @@ HTML_PAGE = f"""
151
  .green {{ color: var(--accent-green); }}
152
  .red {{ color: var(--accent-red); }}
153
 
154
- .terminal-box {{ margin-top: auto; font-size: 11px; height: 300px; display: flex; flex-direction: column; }}
155
  .term-header {{ border-bottom: 1px dashed #444; margin-bottom: 5px; opacity: 0.7; }}
156
  #term-logs {{ flex: 1; overflow-y: hidden; display: flex; flex-direction: column-reverse; }}
157
  .log-line {{ margin-top: 4px; padding-left: 8px; border-left: 2px solid #333; }}
158
 
159
- .meter-container {{ width: 100%; height: 6px; background: #333; margin-top: 10px; position: relative; overflow: hidden; }}
160
- .meter-bar {{ height: 100%; width: 50%; background: #555; position: absolute; left: 0; transition: all 0.5s; }}
161
- .mid-mark {{ position: absolute; left: 50%; height: 100%; width: 2px; background: #fff; z-index: 10; }}
162
-
163
  #loader {{ position: absolute; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.95); z-index: 999; display: flex; flex-direction: column; justify-content: center; align-items: center; color: var(--accent-green); }}
164
- #loading-status {{ font-size: 14px; color: #888; margin-top: 10px; }}
165
  </style>
166
  </head>
167
  <body>
@@ -172,26 +186,37 @@ HTML_PAGE = f"""
172
  </div>
173
 
174
  <div class="grid-container">
 
 
175
  <div id="p-price" class="panel">
176
  <div class="panel-header"><span>BTC/USD Price Action</span><span id="live-price">---</span></div>
177
  <div id="tv-price"></div>
178
  </div>
 
 
 
 
 
 
 
 
179
  <div id="p-depth" class="panel">
180
- <div class="panel-header"><span>Net Liquidity (Bid - Ask)</span><span>DEPTH 300</span></div>
181
  <div id="tv-depth"></div>
182
  </div>
 
 
183
  <div id="p-stats" class="panel">
184
  <div class="panel-header">ANALYTICS ENGINE</div>
185
  <div class="stats-content">
186
  <div class="stat-box">
187
  <span class="stat-label">NET LIQUIDITY SCORE</span>
188
  <span id="score-val" class="stat-value">0</span>
189
- <div class="meter-container"><div class="mid-mark"></div><div id="score-bar" class="meter-bar" style="left: 50%; width: 0%;"></div></div>
190
  </div>
191
  <div class="stat-box">
192
- <span class="stat-label">STRUCTURE</span>
193
- <div style="display:flex; justify-content:space-between;"><span>RES:</span><span id="res-val" class="red">---</span></div>
194
- <div style="display:flex; justify-content:space-between;"><span>SUP:</span><span id="sup-val" class="green">---</span></div>
195
  </div>
196
  <div class="stat-box" style="border: 1px solid #444;">
197
  <span class="stat-label" style="color:var(--accent-green);">AI PROJECTION</span>
@@ -207,13 +232,12 @@ HTML_PAGE = f"""
207
 
208
  <script>
209
  document.addEventListener('DOMContentLoaded', () => {{
210
- // DOM Elements
211
  const dom = {{
212
  loader: document.getElementById('loader'),
213
  status: document.getElementById('loading-status'),
214
  price: document.getElementById('live-price'),
 
215
  scoreVal: document.getElementById('score-val'),
216
- scoreBar: document.getElementById('score-bar'),
217
  resVal: document.getElementById('res-val'),
218
  supVal: document.getElementById('sup-val'),
219
  projVal: document.getElementById('proj-val'),
@@ -221,98 +245,100 @@ HTML_PAGE = f"""
221
  }};
222
 
223
  // --- CHART INIT ---
224
- const chartOptionsCommon = {{
225
  layout: {{ background: {{ type: 'solid', color: '#12141a' }}, textColor: '#888' }},
226
  grid: {{ vertLines: {{ color: '#1f2833' }}, horzLines: {{ color: '#1f2833' }} }},
227
  rightPriceScale: {{ borderColor: '#2d3842' }},
228
- timeScale: {{ borderColor: '#2d3842' }},
 
229
  }};
230
 
231
  // 1. Price Chart
232
- const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), {{
233
- ...chartOptionsCommon,
234
- timeScale: {{ timeVisible: true, secondsVisible: true }},
235
- }});
236
  const priceSeries = priceChart.addLineSeries({{ color: '#2962FF', lineWidth: 2 }});
237
  const predSeries = priceChart.addLineSeries({{ color: '#ff9800', lineWidth: 2, lineStyle: 2 }});
238
  let supportLine = null, resistanceLine = null;
239
 
240
- // 2. Depth Chart (Lightweight Charts hacked for non-time-series)
 
 
 
 
 
 
 
 
 
 
 
 
241
  const depthChart = LightweightCharts.createChart(document.getElementById('tv-depth'), {{
242
- ...chartOptionsCommon,
243
  timeScale: {{ tickMarkFormatter: (time) => parseFloat(time).toFixed(0) }},
244
  localization: {{ timeFormatter: (time) => 'Dist: $' + parseFloat(time).toFixed(2) }}
245
  }});
246
  const bullSeries = depthChart.addAreaSeries({{ topColor: 'rgba(102, 252, 241, 0.4)', bottomColor: 'rgba(102, 252, 241, 0.0)', lineColor: '#66fcf1', lineWidth: 2 }});
247
  const bearSeries = depthChart.addAreaSeries({{ topColor: 'rgba(255, 59, 59, 0.4)', bottomColor: 'rgba(255, 59, 59, 0.0)', lineColor: '#ff3b3b', lineWidth: 2 }});
248
 
249
- // Resizers
250
- new ResizeObserver(e => {{ for(let x of e) priceChart.applyOptions({{width:x.contentRect.width, height:x.contentRect.height}})}}).observe(document.getElementById('tv-price'));
251
- new ResizeObserver(e => {{ for(let x of e) depthChart.applyOptions({{width:x.contentRect.width, height:x.contentRect.height}})}}).observe(document.getElementById('tv-depth'));
252
-
253
- // --- WEBSOCKET LOGIC ---
254
- let ws;
255
- let retryCount = 0;
 
 
 
256
 
 
257
  function log(msg, type='neutral') {{
258
  const div = document.createElement('div');
259
  div.className = 'log-line';
260
  div.style.borderLeftColor = type === 'bull' ? '#66fcf1' : type === 'bear' ? '#ff3b3b' : '#333';
261
  div.innerHTML = `<span style="opacity:0.5">${{new Date().toLocaleTimeString()}}</span> ${{msg}}`;
262
  dom.logs.prepend(div);
263
- if (dom.logs.children.length > 20) dom.logs.removeChild(dom.logs.lastChild);
264
  }}
265
 
266
  function connect() {{
267
  const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
268
  const url = `${{proto}}://${{window.location.host}}/ws`;
269
-
270
- ws = new WebSocket(url);
271
 
272
- ws.onopen = () => {{
273
- console.log("WS Connected");
274
- dom.status.innerText = "Waiting for data stream...";
275
- retryCount = 0;
276
- }};
277
-
278
- ws.onclose = () => {{
279
- console.log("WS Closed");
280
- dom.loader.style.display = 'flex';
281
- dom.status.innerText = `Connection lost. Reconnecting (${{++retryCount}})...`;
282
- setTimeout(connect, 3000);
283
- }};
284
 
285
  ws.onmessage = (event) => {{
286
  const data = JSON.parse(event.data);
287
-
288
- if (data.error) {{
289
- dom.status.innerText = data.error;
290
- return;
291
- }}
292
-
293
- // Data is flowing - Hide Loader
294
  dom.loader.style.display = 'none';
295
 
296
- // 1. Update Price
297
- const uniqueHistory = [];
298
  const seen = new Set();
299
  data.history.forEach(d => {{
300
  const t = Math.floor(d.t);
301
- if (!seen.has(t)) {{ seen.add(t); uniqueHistory.push({{ time: t, value: d.p }}); }}
302
  }});
303
-
304
- if (uniqueHistory.length > 0) {{
305
- priceSeries.setData(uniqueHistory);
306
- const last = uniqueHistory[uniqueHistory.length-1];
307
  dom.price.innerText = last.value.toLocaleString(undefined, {{minimumFractionDigits: 2}});
308
-
 
309
  if (data.analysis) {{
310
  const {{ projected, support, resistance, net_score }} = data.analysis;
311
 
312
- // Prediction
313
  predSeries.setData([last, {{ time: last.time + 60, value: projected }}]);
314
  dom.projVal.innerText = projected.toFixed(0);
315
 
 
 
 
 
 
 
316
  // S/R Lines
317
  if (support) {{
318
  dom.supVal.innerText = support.toFixed(0);
@@ -322,7 +348,6 @@ HTML_PAGE = f"""
322
  dom.supVal.innerText = '---';
323
  if (supportLine) {{ priceSeries.removePriceLine(supportLine); supportLine = null; }}
324
  }}
325
-
326
  if (resistance) {{
327
  dom.resVal.innerText = resistance.toFixed(0);
328
  if (!resistanceLine) resistanceLine = priceSeries.createPriceLine({{ price: resistance, color: '#ff1744', title: 'RES' }});
@@ -332,53 +357,46 @@ HTML_PAGE = f"""
332
  if (resistanceLine) {{ priceSeries.removePriceLine(resistanceLine); resistanceLine = null; }}
333
  }}
334
 
335
- // Meter
336
- dom.scoreVal.innerText = net_score.toFixed(1);
337
- dom.scoreVal.className = net_score > 0 ? "stat-value green" : "stat-value red";
338
- let barW = Math.min(Math.abs(net_score)*2, 50);
339
- dom.scoreBar.style.width = barW + '%';
340
- dom.scoreBar.style.left = net_score > 0 ? '50%' : (50 - barW) + '%';
341
- dom.scoreBar.style.background = net_score > 0 ? '#66fcf1' : '#ff3b3b';
342
-
343
- if (Math.random() > 0.99) {{
344
- if (net_score > 40) log("Significant Bullish Imbalance", 'bull');
345
- else if (net_score < -40) log("Significant Bearish Imbalance", 'bear');
346
  }}
347
  }}
348
  }}
349
 
350
- // 2. Update Depth (Bids/Asks Areas)
351
- if (data.diff && data.diff.x.length > 0) {{
352
- const bullData = [], bearData = [];
 
 
 
 
 
 
 
 
 
 
 
353
  for (let i = 0; i < data.diff.x.length; i++) {{
354
- const xVal = data.diff.x[i];
355
- const yVal = data.diff.y[i];
356
- if (yVal >= 0) {{
357
- bullData.push({{ time: xVal, value: yVal }});
358
- bearData.push({{ time: xVal, value: 0 }});
359
- }} else {{
360
- bullData.push({{ time: xVal, value: 0 }});
361
- bearData.push({{ time: xVal, value: yVal }});
362
- }}
363
  }}
364
- bullSeries.setData(bullData);
365
- bearSeries.setData(bearData);
366
- depthChart.timeScale().fitContent();
367
  }}
368
  }};
369
  }}
370
-
371
- connect(); // Start WS connection
372
  }});
373
  </script>
374
  </body>
375
  </html>
376
  """
377
 
378
- # --- Backend Workers ---
379
-
380
  async def kraken_worker():
381
- """Connects to Kraken WS and updates internal state."""
382
  global market_state
383
  while True:
384
  try:
@@ -392,10 +410,10 @@ async def kraken_worker():
392
  async for message in ws:
393
  payload = json.loads(message)
394
  channel = payload.get("channel")
395
- data_entries = payload.get("data", [])
396
 
397
  if channel == "book":
398
- for item in data_entries:
399
  for bid in item.get('bids', []):
400
  q, p = float(bid['qty']), float(bid['price'])
401
  if q == 0: market_state['bids'].pop(p, None)
@@ -414,47 +432,31 @@ async def kraken_worker():
414
  market_state['ready'] = True
415
 
416
  now = time.time()
417
- # Throttle history recording slightly
418
  if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
419
  market_state['history'].append({'t': now, 'p': mid})
420
  if len(market_state['history']) > HISTORY_LENGTH:
421
  market_state['history'].pop(0)
422
 
423
  except Exception as e:
424
- logging.warning(f"⚠️ Kraken connection lost: {e}")
425
  await asyncio.sleep(3)
426
 
427
  async def broadcast_worker():
428
- """Broadcasts processed data to all connected clients."""
429
  while True:
430
  if connected_clients and market_state['ready']:
431
- # 1. Process data once
432
  payload = process_market_data()
433
- payload_json = json.dumps(payload)
434
-
435
- # 2. Send to all clients
436
- # Copy set to avoid size change iteration error if client disconnects mid-loop
437
  for ws in list(connected_clients):
438
- try:
439
- await ws.send_str(payload_json)
440
- except Exception:
441
- # Connection likely closed, will be removed by handler
442
- pass
443
-
444
  await asyncio.sleep(BROADCAST_RATE)
445
 
446
  async def websocket_handler(request):
447
- """Handles new client WebSocket connections."""
448
  ws = web.WebSocketResponse()
449
  await ws.prepare(request)
450
-
451
  connected_clients.add(ws)
452
- try:
453
- async for msg in ws:
454
- pass # We just listen to keep connection open (or handle incoming client msgs if needed)
455
- finally:
456
- connected_clients.remove(ws)
457
-
458
  return ws
459
 
460
  async def handle_index(request):
@@ -467,25 +469,20 @@ async def start_background(app):
467
  async def cleanup_background(app):
468
  app['kraken_task'].cancel()
469
  app['broadcast_task'].cancel()
470
- try:
471
- await app['kraken_task']
472
- await app['broadcast_task']
473
- except asyncio.CancelledError:
474
- pass
475
 
476
  async def main():
477
  app = web.Application()
478
  app.router.add_get('/', handle_index)
479
- app.router.add_get('/ws', websocket_handler) # WebSocket Endpoint
480
  app.on_startup.append(start_background)
481
  app.on_cleanup.append(cleanup_background)
482
-
483
  runner = web.AppRunner(app)
484
  await runner.setup()
485
  site = web.TCPSite(runner, '0.0.0.0', PORT)
486
  await site.start()
487
-
488
- print(f"🚀 AI Dashboard (WebSocket): http://localhost:{PORT}")
489
  await asyncio.Event().wait()
490
 
491
  if __name__ == "__main__":
 
3
  import logging
4
  import time
5
  import bisect
6
+ from aiohttp import web
7
  import websockets
8
 
9
  # --- Configuration ---
10
  SYMBOL_KRAKEN = "BTC/USD"
11
  PORT = 7860
12
  HISTORY_LENGTH = 300
13
+ BROADCAST_RATE = 0.1 # 10Hz updates
14
 
15
  # --- Logging ---
16
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
 
19
  market_state = {
20
  "bids": {},
21
  "asks": {},
22
+ "history": [], # Price history: {t, p}
23
+ "liq_history": [], # Liquidity Trend history: {t, v}
24
  "current_mid": 0.0,
25
  "prev_mid": 0.0,
26
  "ready": False
27
  }
28
 
 
29
  connected_clients = set()
30
 
31
  # --- AI Logic Helper ---
 
34
  return None
35
 
36
  # 1. Momentum Projection
37
+ net_total = diff_y[-1] # Total cumulative delta at max depth
38
  momentum_shift = net_total * 0.2
39
  projected_price = current_mid + momentum_shift
40
 
 
63
  }
64
 
65
  def process_market_data():
 
66
  if not market_state['ready']:
67
  return {"error": "Initializing..."}
68
 
69
  mid = market_state['current_mid']
70
 
71
+ # Snapshot Top 300 for Depth Chart
72
  raw_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])[:300]
73
  raw_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])[:300]
74
 
 
87
  cum += q
88
  d_a_x.append(d); d_a_y.append(cum)
89
 
90
+ # Calculate Net Liquidity Curve (Depth)
91
  diff_x, diff_y = [], []
92
  if d_b_x and d_a_x:
93
  max_dist = min(d_b_x[-1], d_a_x[-1])
 
104
  diff_y.append(vol_b - vol_a)
105
 
106
  analysis = analyze_structure(diff_x, diff_y, mid)
107
+
108
+ # Store Liquidity Trend for history
109
+ now = time.time()
110
+ if analysis:
111
+ # Update Trend History if needed (throttle slightly to match graph res)
112
+ if not market_state['liq_history'] or (now - market_state['liq_history'][-1]['t'] > 0.5):
113
+ market_state['liq_history'].append({'t': now, 'v': analysis['net_score']})
114
+ if len(market_state['liq_history']) > HISTORY_LENGTH:
115
+ market_state['liq_history'].pop(0)
116
 
117
  return {
118
  "mid": mid,
119
+ "history": market_state['history'], # Price History
120
+ "liq_history": market_state['liq_history'], # Net Liq History
121
+ "diff": { "x": diff_x, "y": diff_y }, # Depth Snapshot
122
  "analysis": analysis
123
  }
124
 
 
129
  <head>
130
  <meta charset="UTF-8">
131
  <title>AI Liquidity Dashboard | {SYMBOL_KRAKEN}</title>
 
132
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
133
  <style>
134
  :root {{
 
141
  }}
142
  body {{ margin: 0; padding: 0; background-color: var(--bg-color); color: var(--text-main); font-family: monospace; overflow: hidden; height: 100vh; width: 100vw; }}
143
 
144
+ /* Updated Grid for 3 Rows in Main Column */
145
+ .grid-container {{
146
+ display: grid;
147
+ grid-template-columns: 3fr 1fr;
148
+ grid-template-rows: 2fr 1fr 1fr; /* Price (50%), Trend (25%), Depth (25%) */
149
+ gap: 4px;
150
+ height: 100vh;
151
+ padding: 4px;
152
+ box-sizing: border-box;
153
+ }}
154
+
155
  .panel {{ background: #12141a; border: 1px solid var(--border); border-radius: 4px; position: relative; display: flex; flex-direction: column; overflow: hidden; }}
156
 
157
  #p-price {{ grid-column: 1 / 2; grid-row: 1 / 2; }}
158
+ #p-trend {{ grid-column: 1 / 2; grid-row: 2 / 3; }} /* NEW PANEL */
159
+ #p-depth {{ grid-column: 1 / 2; grid-row: 3 / 4; }}
160
+ #p-stats {{ grid-column: 2 / 3; grid-row: 1 / 4; border-left: 2px solid #45a29e; }}
161
 
162
+ .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; }}
163
 
164
+ #tv-price, #tv-trend, #tv-depth {{ flex: 1; width: 100%; position: relative; }}
165
 
166
  .stats-content {{ padding: 15px; overflow-y: auto; flex: 1; }}
167
  .stat-box {{ margin-bottom: 20px; padding: 10px; background: rgba(255,255,255,0.02); border-radius: 4px; }}
 
170
  .green {{ color: var(--accent-green); }}
171
  .red {{ color: var(--accent-red); }}
172
 
173
+ .terminal-box {{ margin-top: auto; font-size: 11px; height: 200px; display: flex; flex-direction: column; }}
174
  .term-header {{ border-bottom: 1px dashed #444; margin-bottom: 5px; opacity: 0.7; }}
175
  #term-logs {{ flex: 1; overflow-y: hidden; display: flex; flex-direction: column-reverse; }}
176
  .log-line {{ margin-top: 4px; padding-left: 8px; border-left: 2px solid #333; }}
177
 
 
 
 
 
178
  #loader {{ position: absolute; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.95); z-index: 999; display: flex; flex-direction: column; justify-content: center; align-items: center; color: var(--accent-green); }}
 
179
  </style>
180
  </head>
181
  <body>
 
186
  </div>
187
 
188
  <div class="grid-container">
189
+
190
+ <!-- ROW 1: PRICE -->
191
  <div id="p-price" class="panel">
192
  <div class="panel-header"><span>BTC/USD Price Action</span><span id="live-price">---</span></div>
193
  <div id="tv-price"></div>
194
  </div>
195
+
196
+ <!-- ROW 2: LIQUIDITY TREND (NEW) -->
197
+ <div id="p-trend" class="panel">
198
+ <div class="panel-header"><span>Net Liquidity Trend (Time Series)</span><span id="live-trend">0.0</span></div>
199
+ <div id="tv-trend"></div>
200
+ </div>
201
+
202
+ <!-- ROW 3: DEPTH STRUCTURE -->
203
  <div id="p-depth" class="panel">
204
+ <div class="panel-header"><span>Market Depth (Snapshot)</span><span>Range: $100</span></div>
205
  <div id="tv-depth"></div>
206
  </div>
207
+
208
+ <!-- COL 2: STATS -->
209
  <div id="p-stats" class="panel">
210
  <div class="panel-header">ANALYTICS ENGINE</div>
211
  <div class="stats-content">
212
  <div class="stat-box">
213
  <span class="stat-label">NET LIQUIDITY SCORE</span>
214
  <span id="score-val" class="stat-value">0</span>
 
215
  </div>
216
  <div class="stat-box">
217
+ <span class="stat-label">KEY STRUCTURE</span>
218
+ <div style="display:flex; justify-content:space-between;"><span>RESIST:</span><span id="res-val" class="red">---</span></div>
219
+ <div style="display:flex; justify-content:space-between;"><span>SUPPORT:</span><span id="sup-val" class="green">---</span></div>
220
  </div>
221
  <div class="stat-box" style="border: 1px solid #444;">
222
  <span class="stat-label" style="color:var(--accent-green);">AI PROJECTION</span>
 
232
 
233
  <script>
234
  document.addEventListener('DOMContentLoaded', () => {{
 
235
  const dom = {{
236
  loader: document.getElementById('loader'),
237
  status: document.getElementById('loading-status'),
238
  price: document.getElementById('live-price'),
239
+ trend: document.getElementById('live-trend'),
240
  scoreVal: document.getElementById('score-val'),
 
241
  resVal: document.getElementById('res-val'),
242
  supVal: document.getElementById('sup-val'),
243
  projVal: document.getElementById('proj-val'),
 
245
  }};
246
 
247
  // --- CHART INIT ---
248
+ const chartCommon = {{
249
  layout: {{ background: {{ type: 'solid', color: '#12141a' }}, textColor: '#888' }},
250
  grid: {{ vertLines: {{ color: '#1f2833' }}, horzLines: {{ color: '#1f2833' }} }},
251
  rightPriceScale: {{ borderColor: '#2d3842' }},
252
+ timeScale: {{ borderColor: '#2d3842', timeVisible: true, secondsVisible: true }},
253
+ crosshair: {{ mode: 0 }}
254
  }};
255
 
256
  // 1. Price Chart
257
+ const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartCommon);
 
 
 
258
  const priceSeries = priceChart.addLineSeries({{ color: '#2962FF', lineWidth: 2 }});
259
  const predSeries = priceChart.addLineSeries({{ color: '#ff9800', lineWidth: 2, lineStyle: 2 }});
260
  let supportLine = null, resistanceLine = null;
261
 
262
+ // 2. Trend Chart (Baseline Series)
263
+ const trendChart = LightweightCharts.createChart(document.getElementById('tv-trend'), {{
264
+ ...chartCommon,
265
+ rightPriceScale: {{ scaleMargins: {{ top: 0.1, bottom: 0.1 }} }}
266
+ }});
267
+ // Baseline: Positive = Green, Negative = Red
268
+ const trendSeries = trendChart.addBaselineSeries({{
269
+ baseValue: {{ type: 'price', price: 0 }},
270
+ topLineColor: '#66fcf1', topFillColor1: 'rgba(102, 252, 241, 0.28)', topFillColor2: 'rgba(102, 252, 241, 0.05)',
271
+ bottomLineColor: '#ff3b3b', bottomFillColor1: 'rgba(255, 59, 59, 0.28)', bottomFillColor2: 'rgba(255, 59, 59, 0.05)',
272
+ }});
273
+
274
+ // 3. Depth Chart (Custom Format)
275
  const depthChart = LightweightCharts.createChart(document.getElementById('tv-depth'), {{
276
+ ...chartCommon,
277
  timeScale: {{ tickMarkFormatter: (time) => parseFloat(time).toFixed(0) }},
278
  localization: {{ timeFormatter: (time) => 'Dist: $' + parseFloat(time).toFixed(2) }}
279
  }});
280
  const bullSeries = depthChart.addAreaSeries({{ topColor: 'rgba(102, 252, 241, 0.4)', bottomColor: 'rgba(102, 252, 241, 0.0)', lineColor: '#66fcf1', lineWidth: 2 }});
281
  const bearSeries = depthChart.addAreaSeries({{ topColor: 'rgba(255, 59, 59, 0.4)', bottomColor: 'rgba(255, 59, 59, 0.0)', lineColor: '#ff3b3b', lineWidth: 2 }});
282
 
283
+ // Auto-Resize
284
+ const resizeObserver = new ResizeObserver(entries => {{
285
+ for(let entry of entries) {{
286
+ const {{width, height}} = entry.contentRect;
287
+ if(entry.target.id === 'tv-price') priceChart.applyOptions({{width, height}});
288
+ if(entry.target.id === 'tv-trend') trendChart.applyOptions({{width, height}});
289
+ if(entry.target.id === 'tv-depth') depthChart.applyOptions({{width, height}});
290
+ }}
291
+ }});
292
+ ['tv-price', 'tv-trend', 'tv-depth'].forEach(id => resizeObserver.observe(document.getElementById(id)));
293
 
294
+ // --- WEBSOCKET ---
295
  function log(msg, type='neutral') {{
296
  const div = document.createElement('div');
297
  div.className = 'log-line';
298
  div.style.borderLeftColor = type === 'bull' ? '#66fcf1' : type === 'bear' ? '#ff3b3b' : '#333';
299
  div.innerHTML = `<span style="opacity:0.5">${{new Date().toLocaleTimeString()}}</span> ${{msg}}`;
300
  dom.logs.prepend(div);
301
+ if (dom.logs.children.length > 15) dom.logs.removeChild(dom.logs.lastChild);
302
  }}
303
 
304
  function connect() {{
305
  const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
306
  const url = `${{proto}}://${{window.location.host}}/ws`;
307
+ const ws = new WebSocket(url);
 
308
 
309
+ ws.onopen = () => {{ dom.status.innerText = "Receiving Data Stream..."; }};
310
+ ws.onclose = () => {{ dom.loader.style.display = 'flex'; dom.status.innerText = "Reconnecting..."; setTimeout(connect, 3000); }};
 
 
 
 
 
 
 
 
 
 
311
 
312
  ws.onmessage = (event) => {{
313
  const data = JSON.parse(event.data);
314
+ if (data.error) return;
 
 
 
 
 
 
315
  dom.loader.style.display = 'none';
316
 
317
+ // 1. Price Data
318
+ const cleanHistory = [];
319
  const seen = new Set();
320
  data.history.forEach(d => {{
321
  const t = Math.floor(d.t);
322
+ if (!seen.has(t)) {{ seen.add(t); cleanHistory.push({{ time: t, value: d.p }}); }}
323
  }});
324
+ if (cleanHistory.length) {{
325
+ priceSeries.setData(cleanHistory);
326
+ const last = cleanHistory[cleanHistory.length-1];
 
327
  dom.price.innerText = last.value.toLocaleString(undefined, {{minimumFractionDigits: 2}});
328
+
329
+ // Analysis Overlays
330
  if (data.analysis) {{
331
  const {{ projected, support, resistance, net_score }} = data.analysis;
332
 
 
333
  predSeries.setData([last, {{ time: last.time + 60, value: projected }}]);
334
  dom.projVal.innerText = projected.toFixed(0);
335
 
336
+ // Sync Trend Chart Value
337
+ dom.trend.innerText = net_score.toFixed(1);
338
+ dom.trend.style.color = net_score >= 0 ? 'var(--accent-green)' : 'var(--accent-red)';
339
+ dom.scoreVal.innerText = net_score.toFixed(1);
340
+ dom.scoreVal.className = net_score > 0 ? "stat-value green" : "stat-value red";
341
+
342
  // S/R Lines
343
  if (support) {{
344
  dom.supVal.innerText = support.toFixed(0);
 
348
  dom.supVal.innerText = '---';
349
  if (supportLine) {{ priceSeries.removePriceLine(supportLine); supportLine = null; }}
350
  }}
 
351
  if (resistance) {{
352
  dom.resVal.innerText = resistance.toFixed(0);
353
  if (!resistanceLine) resistanceLine = priceSeries.createPriceLine({{ price: resistance, color: '#ff1744', title: 'RES' }});
 
357
  if (resistanceLine) {{ priceSeries.removePriceLine(resistanceLine); resistanceLine = null; }}
358
  }}
359
 
360
+ // AI Logs
361
+ if (Math.abs(net_score) > 50 && Math.random() > 0.98) {{
362
+ log(net_score > 0 ? "Momentum: Strong Buy" : "Momentum: Strong Sell", net_score > 0 ? 'bull' : 'bear');
 
 
 
 
 
 
 
 
363
  }}
364
  }}
365
  }}
366
 
367
+ // 2. Liquidity Trend Data
368
+ if (data.liq_history) {{
369
+ const trendData = [];
370
+ const seenT = new Set();
371
+ data.liq_history.forEach(d => {{
372
+ const t = Math.floor(d.t);
373
+ if(!seenT.has(t)) {{ seenT.add(t); trendData.push({{ time: t, value: d.v }}); }}
374
+ }});
375
+ if (trendData.length) trendSeries.setData(trendData);
376
+ }}
377
+
378
+ // 3. Depth Snapshot
379
+ if (data.diff && data.diff.x.length) {{
380
+ const bull = [], bear = [];
381
  for (let i = 0; i < data.diff.x.length; i++) {{
382
+ const x = data.diff.x[i];
383
+ const y = data.diff.y[i];
384
+ if (y >= 0) {{ bull.push({{ time: x, value: y }}); bear.push({{ time: x, value: 0 }}); }}
385
+ else {{ bull.push({{ time: x, value: 0 }}); bear.push({{ time: x, value: y }}); }}
 
 
 
 
 
386
  }}
387
+ bullSeries.setData(bull);
388
+ bearSeries.setData(bear);
 
389
  }}
390
  }};
391
  }}
392
+ connect();
 
393
  }});
394
  </script>
395
  </body>
396
  </html>
397
  """
398
 
 
 
399
  async def kraken_worker():
 
400
  global market_state
401
  while True:
402
  try:
 
410
  async for message in ws:
411
  payload = json.loads(message)
412
  channel = payload.get("channel")
413
+ data = payload.get("data", [])
414
 
415
  if channel == "book":
416
+ for item in data:
417
  for bid in item.get('bids', []):
418
  q, p = float(bid['qty']), float(bid['price'])
419
  if q == 0: market_state['bids'].pop(p, None)
 
432
  market_state['ready'] = True
433
 
434
  now = time.time()
 
435
  if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
436
  market_state['history'].append({'t': now, 'p': mid})
437
  if len(market_state['history']) > HISTORY_LENGTH:
438
  market_state['history'].pop(0)
439
 
440
  except Exception as e:
441
+ logging.warning(f"⚠️ Reconnecting: {e}")
442
  await asyncio.sleep(3)
443
 
444
  async def broadcast_worker():
 
445
  while True:
446
  if connected_clients and market_state['ready']:
 
447
  payload = process_market_data()
448
+ msg = json.dumps(payload)
 
 
 
449
  for ws in list(connected_clients):
450
+ try: await ws.send_str(msg)
451
+ except: pass
 
 
 
 
452
  await asyncio.sleep(BROADCAST_RATE)
453
 
454
  async def websocket_handler(request):
 
455
  ws = web.WebSocketResponse()
456
  await ws.prepare(request)
 
457
  connected_clients.add(ws)
458
+ try: async for msg in ws: pass
459
+ finally: connected_clients.remove(ws)
 
 
 
 
460
  return ws
461
 
462
  async def handle_index(request):
 
469
  async def cleanup_background(app):
470
  app['kraken_task'].cancel()
471
  app['broadcast_task'].cancel()
472
+ try: await app['kraken_task']; await app['broadcast_task']
473
+ except: pass
 
 
 
474
 
475
  async def main():
476
  app = web.Application()
477
  app.router.add_get('/', handle_index)
478
+ app.router.add_get('/ws', websocket_handler)
479
  app.on_startup.append(start_background)
480
  app.on_cleanup.append(cleanup_background)
 
481
  runner = web.AppRunner(app)
482
  await runner.setup()
483
  site = web.TCPSite(runner, '0.0.0.0', PORT)
484
  await site.start()
485
+ print(f"🚀 AI Dashboard: http://localhost:{PORT}")
 
486
  await asyncio.Event().wait()
487
 
488
  if __name__ == "__main__":