Alvin3y1 commited on
Commit
669adc5
·
verified ·
1 Parent(s): 6dbbeb8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +167 -152
app.py CHANGED
@@ -3,13 +3,14 @@ import json
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
 
14
  # --- Logging ---
15
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
@@ -24,6 +25,9 @@ market_state = {
24
  "ready": False
25
  }
26
 
 
 
 
27
  # --- AI Logic Helper ---
28
  def analyze_structure(diff_x, diff_y, current_mid):
29
  if not diff_y or len(diff_y) < 5:
@@ -58,6 +62,57 @@ def analyze_structure(diff_x, diff_y, current_mid):
58
  "net_score": net_total
59
  }
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  # --- HTML Frontend ---
62
  HTML_PAGE = f"""
63
  <!DOCTYPE html>
@@ -65,10 +120,8 @@ HTML_PAGE = f"""
65
  <head>
66
  <meta charset="UTF-8">
67
  <title>AI Liquidity Dashboard | {SYMBOL_KRAKEN}</title>
68
-
69
- <!-- Pinned Version for Stability -->
70
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
71
-
72
  <style>
73
  :root {{
74
  --bg-color: #0b0c10;
@@ -108,31 +161,25 @@ HTML_PAGE = f"""
108
  .mid-mark {{ position: absolute; left: 50%; height: 100%; width: 2px; background: #fff; z-index: 10; }}
109
 
110
  #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); }}
111
- #error-log {{ color: var(--accent-red); font-size: 12px; margin-top: 20px; max-width: 80%; text-align: center; }}
112
  </style>
113
  </head>
114
  <body>
115
 
116
  <div id="loader">
117
- <div style="font-size: 24px; margin-bottom: 10px;">INITIALIZING AI MODELS...</div>
118
- <div id="loading-status" style="font-size: 14px; color: #888;">Connecting to WebSocket...</div>
119
- <div id="error-log"></div>
120
  </div>
121
 
122
  <div class="grid-container">
123
- <!-- Price Chart Panel -->
124
  <div id="p-price" class="panel">
125
  <div class="panel-header"><span>BTC/USD Price Action</span><span id="live-price">---</span></div>
126
  <div id="tv-price"></div>
127
  </div>
128
-
129
- <!-- Depth Chart Panel (Now using Lightweight Charts) -->
130
  <div id="p-depth" class="panel">
131
- <div class="panel-header"><span>Net Liquidity (Green=Bids, Red=Asks)</span><span>DEPTH 300</span></div>
132
  <div id="tv-depth"></div>
133
  </div>
134
-
135
- <!-- Analytics Panel -->
136
  <div id="p-stats" class="panel">
137
  <div class="panel-header">ANALYTICS ENGINE</div>
138
  <div class="stats-content">
@@ -159,16 +206,8 @@ HTML_PAGE = f"""
159
  </div>
160
 
161
  <script>
162
- // --- ERROR HANDLING ---
163
- window.onerror = function(msg, url, lineNo, columnNo, error) {{
164
- const errDiv = document.getElementById('error-log');
165
- errDiv.innerHTML += `ERROR: ${{msg}} (Line: ${{lineNo}})<br>`;
166
- document.getElementById('loading-status').style.color = 'red';
167
- document.getElementById('loading-status').innerText = "SYSTEM FAILURE";
168
- return false;
169
- }};
170
-
171
  document.addEventListener('DOMContentLoaded', () => {{
 
172
  const dom = {{
173
  loader: document.getElementById('loader'),
174
  status: document.getElementById('loading-status'),
@@ -181,7 +220,7 @@ HTML_PAGE = f"""
181
  logs: document.getElementById('term-logs')
182
  }};
183
 
184
- // --- CHART CONFIGURATION ---
185
  const chartOptionsCommon = {{
186
  layout: {{ background: {{ type: 'solid', color: '#12141a' }}, textColor: '#888' }},
187
  grid: {{ vertLines: {{ color: '#1f2833' }}, horzLines: {{ color: '#1f2833' }} }},
@@ -189,9 +228,8 @@ HTML_PAGE = f"""
189
  timeScale: {{ borderColor: '#2d3842' }},
190
  }};
191
 
192
- // 1. PRICE CHART
193
- const priceContainer = document.getElementById('tv-price');
194
- const priceChart = LightweightCharts.createChart(priceContainer, {{
195
  ...chartOptionsCommon,
196
  timeScale: {{ timeVisible: true, secondsVisible: true }},
197
  }});
@@ -199,46 +237,23 @@ HTML_PAGE = f"""
199
  const predSeries = priceChart.addLineSeries({{ color: '#ff9800', lineWidth: 2, lineStyle: 2 }});
200
  let supportLine = null, resistanceLine = null;
201
 
202
- // 2. DEPTH CHART (Hacked for Non-Time Series)
203
- const depthContainer = document.getElementById('tv-depth');
204
- const depthChart = LightweightCharts.createChart(depthContainer, {{
205
  ...chartOptionsCommon,
206
- timeScale: {{
207
- // Override formatted date to just show raw distance
208
- tickMarkFormatter: (time) => parseFloat(time).toFixed(0),
209
- }},
210
- localization: {{
211
- // Override tooltip formatter
212
- timeFormatter: (time) => 'Dist: $' + parseFloat(time).toFixed(2),
213
- }}
214
- }});
215
-
216
- // We use two Area series: one for Positive (Green), one for Negative (Red)
217
- const bullSeries = depthChart.addAreaSeries({{
218
- topColor: 'rgba(102, 252, 241, 0.4)',
219
- bottomColor: 'rgba(102, 252, 241, 0.0)',
220
- lineColor: '#66fcf1', lineWidth: 2
221
- }});
222
- const bearSeries = depthChart.addAreaSeries({{
223
- topColor: 'rgba(255, 59, 59, 0.4)',
224
- bottomColor: 'rgba(255, 59, 59, 0.0)',
225
- lineColor: '#ff3b3b', lineWidth: 2
226
  }});
 
 
 
 
 
 
 
 
 
 
227
 
228
- // Resize Logic
229
- new ResizeObserver(entries => {{
230
- for (let entry of entries) {{
231
- if (entry.target === priceContainer) priceChart.applyOptions({{ width: entry.contentRect.width, height: entry.contentRect.height }});
232
- if (entry.target === depthContainer) depthChart.applyOptions({{ width: entry.contentRect.width, height: entry.contentRect.height }});
233
- }}
234
- }}).observe(priceContainer);
235
- new ResizeObserver(entries => {{
236
- for (let entry of entries) {{
237
- if (entry.target === depthContainer) depthChart.applyOptions({{ width: entry.contentRect.width, height: entry.contentRect.height }});
238
- }}
239
- }}).observe(depthContainer);
240
-
241
- // --- HELPERS ---
242
  function log(msg, type='neutral') {{
243
  const div = document.createElement('div');
244
  div.className = 'log-line';
@@ -248,19 +263,37 @@ HTML_PAGE = f"""
248
  if (dom.logs.children.length > 20) dom.logs.removeChild(dom.logs.lastChild);
249
  }}
250
 
251
- // --- DATA LOOP ---
252
- async function fetchData() {{
253
- try {{
254
- const res = await fetch('/data?t=' + Date.now());
255
- const data = await res.json();
256
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  if (data.error) {{
258
- dom.status.innerText = "Synchronizing Market State...";
259
  return;
260
  }}
 
 
261
  dom.loader.style.display = 'none';
262
 
263
- // -- UPDATE PRICE CHART --
264
  const uniqueHistory = [];
265
  const seen = new Set();
266
  data.history.forEach(d => {{
@@ -270,17 +303,17 @@ HTML_PAGE = f"""
270
 
271
  if (uniqueHistory.length > 0) {{
272
  priceSeries.setData(uniqueHistory);
273
- const last = uniqueHistory[uniqueHistory.length - 1];
274
  dom.price.innerText = last.value.toLocaleString(undefined, {{minimumFractionDigits: 2}});
275
 
276
  if (data.analysis) {{
277
  const {{ projected, support, resistance, net_score }} = data.analysis;
278
 
279
- // Projection
280
  predSeries.setData([last, {{ time: last.time + 60, value: projected }}]);
281
  dom.projVal.innerText = projected.toFixed(0);
282
 
283
- // S/R Lines (Using createPriceLine)
284
  if (support) {{
285
  dom.supVal.innerText = support.toFixed(0);
286
  if (!supportLine) supportLine = priceSeries.createPriceLine({{ price: support, color: '#00e676', title: 'SUP' }});
@@ -299,7 +332,7 @@ HTML_PAGE = f"""
299
  if (resistanceLine) {{ priceSeries.removePriceLine(resistanceLine); resistanceLine = null; }}
300
  }}
301
 
302
- // Meter & Logs
303
  dom.scoreVal.innerText = net_score.toFixed(1);
304
  dom.scoreVal.className = net_score > 0 ? "stat-value green" : "stat-value red";
305
  let barW = Math.min(Math.abs(net_score)*2, 50);
@@ -307,54 +340,45 @@ HTML_PAGE = f"""
307
  dom.scoreBar.style.left = net_score > 0 ? '50%' : (50 - barW) + '%';
308
  dom.scoreBar.style.background = net_score > 0 ? '#66fcf1' : '#ff3b3b';
309
 
310
- if (Math.random() > 0.98) {{
311
- if (net_score > 40) log("Strong Buyers Detected", 'bull');
312
- else if (net_score < -40) log("Strong Sellers Detected", 'bear');
313
  }}
314
  }}
315
  }}
316
 
317
- // -- UPDATE DEPTH CHART --
318
- // Map diff_x (distance) to 'time', diff_y to 'value'
319
- // Split into Bull (Positive) and Bear (Negative) series
320
  if (data.diff && data.diff.x.length > 0) {{
321
- const bullData = [];
322
- const bearData = [];
323
-
324
  for (let i = 0; i < data.diff.x.length; i++) {{
325
- const xVal = data.diff.x[i]; // Distance
326
- const yVal = data.diff.y[i]; // Net Volume
327
-
328
- // Ensure strictly unique and ascending (already guaranteed by logic, but safe to cast)
329
- // We use xVal as 'time'
330
-
331
  if (yVal >= 0) {{
332
  bullData.push({{ time: xVal, value: yVal }});
333
- bearData.push({{ time: xVal, value: 0 }}); // Clamp other series to 0
334
  }} else {{
335
  bullData.push({{ time: xVal, value: 0 }});
336
  bearData.push({{ time: xVal, value: yVal }});
337
  }}
338
  }}
339
-
340
  bullSeries.setData(bullData);
341
  bearSeries.setData(bearData);
342
  depthChart.timeScale().fitContent();
343
  }}
344
-
345
- }} catch (e) {{
346
- console.error(e);
347
- }}
348
  }}
349
-
350
- setInterval(fetchData, 1000);
351
  }});
352
  </script>
353
  </body>
354
  </html>
355
  """
356
 
 
 
357
  async def kraken_worker():
 
358
  global market_state
359
  while True:
360
  try:
@@ -390,78 +414,69 @@ async def kraken_worker():
390
  market_state['ready'] = True
391
 
392
  now = time.time()
 
393
  if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
394
  market_state['history'].append({'t': now, 'p': mid})
395
  if len(market_state['history']) > HISTORY_LENGTH:
396
  market_state['history'].pop(0)
397
 
398
  except Exception as e:
399
- logging.warning(f"⚠️ Reconnecting: {e}")
400
  await asyncio.sleep(3)
401
 
402
- async def handle_index(request):
403
- return web.Response(text=HTML_PAGE, content_type='text/html')
404
-
405
- async def handle_data(request):
406
- if not market_state['ready']:
407
- return web.json_response({"error": "Initializing..."})
408
-
409
- mid = market_state['current_mid']
410
-
411
- raw_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])[:300]
412
- raw_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])[:300]
413
-
414
- d_b_x, d_b_y, cum = [], [], 0
415
- for p, q in raw_bids:
416
- d = mid - p
417
- if d >= 0:
418
- cum += q
419
- d_b_x.append(d); d_b_y.append(cum)
420
-
421
- d_a_x, d_a_y, cum = [], [], 0
422
- for p, q in raw_asks:
423
- d = p - mid
424
- if d >= 0:
425
- cum += q
426
- d_a_x.append(d); d_a_y.append(cum)
427
-
428
- # Net Diff
429
- diff_x, diff_y = [], []
430
- if d_b_x and d_a_x:
431
- max_dist = min(d_b_x[-1], d_a_x[-1])
432
- step_size = max_dist / 100
433
- steps = [i * step_size for i in range(1, 101)]
434
-
435
- for s in steps:
436
- idx_b = bisect.bisect_right(d_b_x, s)
437
- vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
438
- idx_a = bisect.bisect_right(d_a_x, s)
439
- vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
440
 
441
- diff_x.append(s)
442
- diff_y.append(vol_b - vol_a)
443
-
444
- analysis = analyze_structure(diff_x, diff_y, mid)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
 
446
- return web.json_response({
447
- "mid": mid,
448
- "history": market_state['history'],
449
- "diff": { "x": diff_x, "y": diff_y },
450
- "analysis": analysis
451
- })
452
 
453
  async def start_background(app):
454
  app['kraken_task'] = asyncio.create_task(kraken_worker())
 
455
 
456
  async def cleanup_background(app):
457
  app['kraken_task'].cancel()
458
- try: await app['kraken_task']
459
- except asyncio.CancelledError: pass
 
 
 
 
460
 
461
  async def main():
462
  app = web.Application()
463
  app.router.add_get('/', handle_index)
464
- app.router.add_get('/data', handle_data)
465
  app.on_startup.append(start_background)
466
  app.on_cleanup.append(cleanup_background)
467
 
@@ -470,7 +485,7 @@ async def main():
470
  site = web.TCPSite(runner, '0.0.0.0', PORT)
471
  await site.start()
472
 
473
- print(f"🚀 AI Dashboard: http://localhost:{PORT}")
474
  await asyncio.Event().wait()
475
 
476
  if __name__ == "__main__":
 
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')
 
25
  "ready": False
26
  }
27
 
28
+ # Set of connected client websockets
29
+ connected_clients = set()
30
+
31
  # --- AI Logic Helper ---
32
  def analyze_structure(diff_x, diff_y, current_mid):
33
  if not diff_y or len(diff_y) < 5:
 
62
  "net_score": net_total
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
+
76
+ # Calculate Cumulative Volume
77
+ d_b_x, d_b_y, cum = [], [], 0
78
+ for p, q in raw_bids:
79
+ d = mid - p
80
+ if d >= 0:
81
+ cum += q
82
+ d_b_x.append(d); d_b_y.append(cum)
83
+
84
+ d_a_x, d_a_y, cum = [], [], 0
85
+ for p, q in raw_asks:
86
+ d = p - mid
87
+ if d >= 0:
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])
95
+ step_size = max_dist / 100
96
+ steps = [i * step_size for i in range(1, 101)]
97
+
98
+ for s in steps:
99
+ idx_b = bisect.bisect_right(d_b_x, s)
100
+ vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
101
+ idx_a = bisect.bisect_right(d_a_x, s)
102
+ vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
103
+
104
+ diff_x.append(s)
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
+
116
  # --- HTML Frontend ---
117
  HTML_PAGE = f"""
118
  <!DOCTYPE html>
 
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 {{
127
  --bg-color: #0b0c10;
 
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>
168
 
169
  <div id="loader">
170
+ <div style="font-size: 24px;">ESTABLISHING UPLINK...</div>
171
+ <div id="loading-status">Connecting to WebSocket Stream...</div>
 
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">
 
206
  </div>
207
 
208
  <script>
 
 
 
 
 
 
 
 
 
209
  document.addEventListener('DOMContentLoaded', () => {{
210
+ // DOM Elements
211
  const dom = {{
212
  loader: document.getElementById('loader'),
213
  status: document.getElementById('loading-status'),
 
220
  logs: document.getElementById('term-logs')
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' }} }},
 
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
  }});
 
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';
 
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 => {{
 
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);
319
  if (!supportLine) supportLine = priceSeries.createPriceLine({{ price: support, color: '#00e676', title: 'SUP' }});
 
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);
 
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:
 
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):
461
+ return web.Response(text=HTML_PAGE, content_type='text/html')
 
 
 
 
462
 
463
  async def start_background(app):
464
  app['kraken_task'] = asyncio.create_task(kraken_worker())
465
+ app['broadcast_task'] = asyncio.create_task(broadcast_worker())
466
 
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
 
 
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__":