Alvin3y1 commited on
Commit
df9c251
·
verified ·
1 Parent(s): 0dee7c2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +616 -265
app.py CHANGED
@@ -11,7 +11,7 @@ from sklearn.ensemble import RandomForestRegressor
11
  SYMBOL_KRAKEN = "BTC/USD"
12
  PORT = 7860
13
  BROADCAST_RATE = 1.0
14
- PREDICTION_HORIZON = 60
15
 
16
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
17
 
@@ -36,11 +36,15 @@ def calculate_indicators(candles):
36
  df[c] = df[c].astype(float)
37
 
38
  df['ema'] = df['close'].ewm(span=20, adjust=False).mean()
 
 
 
39
  df['sma20'] = df['close'].rolling(window=20).mean()
40
  df['std'] = df['close'].rolling(window=20).std()
41
  df['bb_upper'] = df['sma20'] + (df['std'] * 2)
42
  df['bb_lower'] = df['sma20'] - (df['std'] * 2)
43
-
 
44
  delta = df['close'].diff()
45
  gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
46
  loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
@@ -56,20 +60,26 @@ def calculate_indicators(candles):
56
  low_min = df['low'].rolling(window=14).min()
57
  high_max = df['high'].rolling(window=14).max()
58
  df['stoch_k'] = 100 * ((df['close'] - low_min) / (high_max - low_min))
59
-
 
60
  df['tr0'] = abs(df['high'] - df['low'])
61
  df['tr1'] = abs(df['high'] - df['close'].shift())
62
  df['tr2'] = abs(df['low'] - df['close'].shift())
63
  df['tr'] = df[['tr0', 'tr1', 'tr2']].max(axis=1)
64
  df['atr'] = df['tr'].rolling(window=14).mean()
65
 
 
 
66
  df['tp'] = (df['high'] + df['low'] + df['close']) / 3
67
  df['vwap'] = (df['tp'] * df['volume']).cumsum() / df['volume'].cumsum()
68
 
69
  return df
70
 
71
  def train_model(df):
72
- feature_cols = ['close', 'ema', 'bb_upper', 'bb_lower', 'rsi', 'macd', 'stoch_k', 'atr', 'vwap']
 
 
 
73
  data = df.dropna().copy()
74
 
75
  future_shifts = {}
@@ -85,20 +95,23 @@ def train_model(df):
85
  data = data.dropna()
86
 
87
  if len(data) < 100:
 
88
  return None
89
 
90
  X = data[feature_cols].values
91
  y = data[targets].values
92
 
93
- model = RandomForestRegressor(n_estimators=30, max_depth=8, n_jobs=-1, random_state=42)
94
  model.fit(X, y)
 
 
95
  return model
96
 
97
  def get_prediction(df, model):
98
  if model is None:
99
  return []
100
 
101
- feature_cols = ['close', 'ema', 'bb_upper', 'bb_lower', 'rsi', 'macd', 'stoch_k', 'atr', 'vwap']
102
  last_row = df.iloc[[-1]][feature_cols]
103
 
104
  if last_row.isnull().values.any():
@@ -134,8 +147,8 @@ def process_market_data():
134
  predictions = []
135
  try:
136
  predictions = get_prediction(df, market_state['model'])
137
- except Exception:
138
- pass
139
 
140
  df_clean = df.replace([np.inf, -np.inf], np.nan)
141
  df_clean = df_clean.astype(object).where(pd.notnull(df_clean), None)
@@ -148,6 +161,7 @@ def process_market_data():
148
  market_state['price_change'] = price_change
149
 
150
  full_data = df_clean.to_dict('records')
 
151
  last_row = df.iloc[-1] if len(df) > 0 else {}
152
 
153
  return {
@@ -156,10 +170,10 @@ def process_market_data():
156
  "stats": {
157
  "price": last_close,
158
  "change": round(price_change, 2),
159
- "rsi": round(float(last_row.get('rsi')), 1) if pd.notna(last_row.get('rsi')) else 0,
160
- "macd": round(float(last_row.get('macd')), 2) if pd.notna(last_row.get('macd')) else 0,
161
- "atr": round(float(last_row.get('atr')), 2) if pd.notna(last_row.get('atr')) else 0,
162
- "volume": round(float(last_row.get('volume')), 2) if pd.notna(last_row.get('volume')) else 0
163
  }
164
  }
165
 
@@ -169,296 +183,585 @@ HTML_PAGE = """
169
  <head>
170
  <meta charset="UTF-8">
171
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
172
- <title>QuantAI Terminal | BTC/USD</title>
173
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
174
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
175
  <style>
176
- :root {
177
- --bg-dark: #0b0e11;
178
- --bg-panel: #151a21;
179
- --accent: #2962ff;
180
- --accent-glow: rgba(41, 98, 255, 0.3);
181
- --up: #00e396;
182
- --down: #ff004c;
183
- --text-main: #eceff1;
184
- --text-dim: #94a3b8;
185
- --border: rgba(255, 255, 255, 0.08);
186
- }
187
  * { margin: 0; padding: 0; box-sizing: border-box; }
 
188
  body {
189
- background-color: var(--bg-dark);
190
- color: var(--text-main);
191
- font-family: 'Inter', sans-serif;
192
  height: 100vh;
 
 
193
  overflow: hidden;
194
- display: grid;
195
- grid-template-rows: 60px 1fr;
196
  }
 
197
  .header {
198
- background: var(--bg-panel);
199
- border-bottom: 1px solid var(--border);
 
 
200
  display: flex;
201
  align-items: center;
202
  justify-content: space-between;
203
- padding: 0 24px;
204
- z-index: 10;
205
  }
206
- .brand {
 
207
  display: flex;
208
  align-items: center;
209
- gap: 12px;
210
  }
211
- .brand-text { font-family: 'JetBrains Mono', monospace; font-weight: 700; font-size: 18px; color: #fff; }
212
- .live-badge {
213
- background: rgba(0, 227, 150, 0.15);
214
- color: var(--up);
215
- padding: 4px 8px;
216
- border-radius: 4px;
217
- font-size: 11px;
 
 
 
 
 
 
 
 
 
218
  font-weight: 600;
 
 
 
 
219
  display: flex;
 
220
  align-items: center;
221
- gap: 6px;
222
  }
223
- .live-dot { width: 6px; height: 6px; background: var(--up); border-radius: 50%; animation: pulse 1.5s infinite; }
224
- .live-dot.offline { background: var(--down); animation: none; }
225
 
226
- .dashboard-grid {
227
- display: grid;
228
- grid-template-columns: 280px 1fr;
229
- height: 100%;
230
- overflow: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  }
232
- .sidebar {
233
- background: var(--bg-panel);
234
- border-right: 1px solid var(--border);
235
- padding: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  display: flex;
237
  flex-direction: column;
238
- gap: 20px;
239
- }
240
- .stat-card {
241
- background: rgba(255,255,255,0.02);
242
- border: 1px solid var(--border);
243
- border-radius: 8px;
244
- padding: 16px;
245
- }
246
- .stat-label { font-size: 11px; color: var(--text-dim); text-transform: uppercase; margin-bottom: 6px; letter-spacing: 0.5px; }
247
- .stat-value { font-family: 'JetBrains Mono', monospace; font-size: 20px; font-weight: 500; }
248
- .stat-value.up { color: var(--up); }
249
- .stat-value.down { color: var(--down); }
250
-
251
- .main-view {
252
- display: grid;
253
- grid-template-rows: 3fr 1fr;
254
  position: relative;
255
  }
256
- .chart-container { position: relative; width: 100%; height: 100%; }
257
- .chart-overlay {
 
 
 
 
 
 
 
 
 
258
  position: absolute;
259
- top: 20px;
260
- left: 20px;
261
- z-index: 5;
262
  display: flex;
263
- gap: 15px;
264
- font-size: 12px;
265
- font-family: 'JetBrains Mono', monospace;
266
  pointer-events: none;
267
  }
268
- .legend-item { display: flex; align-items: center; gap: 6px; color: var(--text-dim); }
269
- .legend-dot { width: 8px; height: 8px; border-radius: 2px; }
270
 
271
- .loader-screen {
 
 
 
 
 
 
 
 
 
 
 
 
272
  position: absolute;
273
- inset: 0;
274
- background: var(--bg-dark);
 
 
 
275
  display: flex;
276
  flex-direction: column;
277
  align-items: center;
278
  justify-content: center;
279
- z-index: 50;
280
- transition: opacity 0.3s;
281
- }
282
- .spinner {
283
- width: 40px;
284
- height: 40px;
285
- border: 3px solid rgba(41, 98, 255, 0.2);
286
- border-top-color: var(--accent);
 
 
 
 
 
 
287
  border-radius: 50%;
288
  animation: spin 1s linear infinite;
289
  }
290
- @keyframes spin { to { transform: rotate(360deg); } }
291
- @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } }
292
 
293
- .pred-pill {
294
- background: linear-gradient(135deg, rgba(140, 0, 255, 0.2), rgba(41, 98, 255, 0.2));
295
- border: 1px solid rgba(140, 0, 255, 0.4);
296
- color: #d09bff;
 
 
 
 
 
 
 
 
 
 
 
 
297
  padding: 4px 10px;
298
  border-radius: 12px;
299
- font-size: 11px;
300
- font-weight: 600;
 
301
  }
302
  </style>
303
  </head>
304
  <body>
305
  <div class="header">
306
- <div class="brand">
307
- <div style="width: 24px; height: 24px; background: var(--accent); border-radius: 6px;"></div>
308
- <span class="brand-text">QuantAI Terminal</span>
309
  </div>
310
- <div class="live-badge">
311
- <div id="status-dot" class="live-dot"></div>
312
- <span id="status-text">CONNECTING</span>
313
- </div>
314
- </div>
315
-
316
- <div class="dashboard-grid">
317
- <div class="sidebar">
318
- <div class="stat-card">
319
- <div class="stat-label">Market Price</div>
320
- <div id="price" class="stat-value">--</div>
321
  </div>
322
- <div class="stat-card">
323
- <div class="stat-label">24h Change</div>
324
- <div id="change" class="stat-value">--</div>
325
  </div>
326
- <div class="stat-card">
327
- <div class="stat-label">RSI (14)</div>
328
- <div id="rsi" class="stat-value">--</div>
329
  </div>
330
- <div class="stat-card">
331
- <div class="stat-label">ATR Volatility</div>
332
- <div id="atr" class="stat-value">--</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  </div>
334
- <div class="stat-card">
335
- <div class="stat-label">AI Model Status</div>
336
- <div id="model-status" style="font-size: 13px; color: var(--accent);">Active</div>
 
 
 
337
  </div>
338
  </div>
339
 
340
- <div class="main-view">
341
- <div id="main-chart" class="chart-container">
342
- <div class="loader-screen" id="loader">
343
- <div class="spinner"></div>
344
- <div style="margin-top:15px; font-size:12px; color: #666;">PROCESSING MARKET DATA</div>
345
- </div>
346
- <div class="chart-overlay">
347
- <div class="legend-item"><div class="legend-dot" style="background: #00e396"></div>Price</div>
348
- <div class="legend-item"><div class="legend-dot" style="background: #2962ff"></div>EMA 20</div>
349
- <div class="legend-item"><div class="legend-dot" style="background: #d09bff"></div>AI Forecast</div>
350
- <div class="pred-pill">RF MODEL V2</div>
351
- </div>
352
  </div>
353
- <div id="sub-chart" class="chart-container" style="border-top: 1px solid var(--border);"></div>
354
  </div>
355
  </div>
356
 
357
  <script>
358
- const upColor = '#00e396';
359
- const downColor = '#ff004c';
360
- const bgDark = '#0b0e11';
361
-
362
- const mainChartEl = document.getElementById('main-chart');
363
- const subChartEl = document.getElementById('sub-chart');
364
-
365
- const chartOpts = {
366
- layout: { background: { color: bgDark }, textColor: '#94a3b8' },
367
- grid: { vertLines: { color: 'rgba(255,255,255,0.04)' }, horzLines: { color: 'rgba(255,255,255,0.04)' } },
368
- timeScale: { borderColor: 'rgba(255,255,255,0.1)', timeVisible: true },
369
- rightPriceScale: { borderColor: 'rgba(255,255,255,0.1)' },
370
- crosshair: { mode: LightweightCharts.CrosshairMode.Normal }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  };
372
-
373
- const chart = LightweightCharts.createChart(mainChartEl, chartOpts);
374
- const subChart = LightweightCharts.createChart(subChartEl, chartOpts);
375
-
376
- const candles = chart.addCandlestickSeries({
377
- upColor: bgDark, downColor: downColor, borderUpColor: upColor, borderDownColor: downColor,
378
- wickUpColor: upColor, wickDownColor: downColor
 
 
 
 
 
379
  });
380
-
381
- const emaSeries = chart.addLineSeries({ color: '#2962ff', lineWidth: 2, crosshairMarkerVisible: false });
382
- const predSeries = chart.addLineSeries({ color: '#d09bff', lineWidth: 2, lineStyle: 2, crosshairMarkerVisible: false });
383
-
384
- const rsiSeries = subChart.addLineSeries({ color: '#f9a825', lineWidth: 2, priceScaleId: 'right' });
385
- const rsiLine70 = subChart.createPriceLine({ price: 70, color: 'rgba(255,255,255,0.3)', lineWidth: 1, lineStyle: 2, axisLabelVisible: false });
386
- const rsiLine30 = subChart.createPriceLine({ price: 30, color: 'rgba(255,255,255,0.3)', lineWidth: 1, lineStyle: 2, axisLabelVisible: false });
387
-
388
- function resize() {
389
- chart.applyOptions({ width: mainChartEl.clientWidth, height: mainChartEl.clientHeight });
390
- subChart.applyOptions({ width: subChartEl.clientWidth, height: subChartEl.clientHeight });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  }
392
- window.onresize = resize;
393
-
394
- chart.timeScale().subscribeVisibleLogicalRangeChange(range => subChart.timeScale().setVisibleLogicalRange(range));
395
- subChart.timeScale().subscribeVisibleLogicalRangeChange(range => chart.timeScale().setVisibleLogicalRange(range));
396
-
397
- let isLoaded = false;
398
-
399
- function connect() {
400
- const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
 
402
- ws.onopen = () => {
403
- document.getElementById('status-dot').className = 'live-dot';
404
- document.getElementById('status-text').innerText = 'LIVE FEED';
405
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
 
407
- ws.onclose = () => {
408
- document.getElementById('status-dot').className = 'live-dot offline';
409
- document.getElementById('status-text').innerText = 'RECONNECTING';
410
- setTimeout(connect, 2000);
411
- };
412
 
413
- ws.onmessage = (event) => {
414
  try {
415
- const msg = JSON.parse(event.data);
416
- if(msg.error) return;
417
-
418
- const data = msg.data;
419
- const last = data[data.length - 1];
420
-
421
- const candleData = data.map(d => ({
422
- time: d.time, open: d.open, high: d.high, low: d.low, close: d.close
423
- }));
424
-
425
- candles.setData(candleData);
426
-
427
- const emaData = data.filter(d => d.ema).map(d => ({ time: d.time, value: d.ema }));
428
- emaSeries.setData(emaData);
429
-
430
- const rsiData = data.filter(d => d.rsi).map(d => ({ time: d.time, value: d.rsi }));
431
- rsiSeries.setData(rsiData);
432
-
433
- if (msg.prediction && msg.prediction.length) {
434
- const predPoints = [{ time: last.time, value: last.close }, ...msg.prediction];
435
- predSeries.setData(predPoints);
436
- }
437
 
438
- document.getElementById('price').innerText = '$' + msg.stats.price.toLocaleString(undefined, {minimumFractionDigits: 2});
439
 
440
- const chg = msg.stats.change;
441
- const chgEl = document.getElementById('change');
442
- chgEl.innerText = (chg > 0 ? '+' : '') + chg + '%';
443
- chgEl.className = 'stat-value ' + (chg >= 0 ? 'up' : 'down');
444
-
445
- document.getElementById('rsi').innerText = msg.stats.rsi;
446
- document.getElementById('atr').innerText = msg.stats.atr;
447
-
448
- if (!isLoaded) {
449
- isLoaded = true;
450
- document.getElementById('loader').style.opacity = '0';
451
- setTimeout(() => document.getElementById('loader').remove(), 300);
452
- chart.timeScale().fitContent();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
  }
454
- } catch (e) {
455
- console.error(e);
456
  }
457
  };
 
 
 
 
 
 
 
458
  }
459
 
460
  connect();
461
- setTimeout(resize, 100);
462
  </script>
463
  </body>
464
  </html>
@@ -468,7 +771,7 @@ async def fetch_initial_data():
468
  try:
469
  async with aiohttp.ClientSession() as session:
470
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
471
- async with session.get(url, timeout=30) as response:
472
  if response.status == 200:
473
  data = await response.json()
474
  if 'result' in data:
@@ -476,83 +779,131 @@ async def fetch_initial_data():
476
  if key != 'last':
477
  raw = data['result'][key]
478
  market_state['ohlc_history'] = [
479
- {'time': int(c[0]), 'open': float(c[1]), 'high': float(c[2]), 'low': float(c[3]), 'close': float(c[4]), 'volume': float(c[6])}
 
 
 
 
 
 
 
480
  for c in raw[-720:]
481
  ]
482
  market_state['ready'] = True
483
- except Exception:
484
- pass
 
 
 
485
 
486
- async def worker_kraken():
487
  await fetch_initial_data()
 
488
  while True:
489
  try:
490
  async with aiohttp.ClientSession() as session:
491
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
492
- async with session.get(url, timeout=10) as response:
493
  if response.status == 200:
494
  data = await response.json()
495
- for key in data['result']:
496
- if key != 'last':
497
- raw = data['result'][key]
498
- new_candles = [
499
- {'time': int(c[0]), 'open': float(c[1]), 'high': float(c[2]), 'low': float(c[3]), 'close': float(c[4]), 'volume': float(c[6])}
500
- for c in raw[-5:]
501
- ]
502
- current_times = {c['time'] for c in market_state['ohlc_history']}
503
- for c in new_candles:
504
- if c['time'] not in current_times:
505
- market_state['ohlc_history'].append(c)
506
- else:
507
- for i, exist in enumerate(market_state['ohlc_history']):
508
- if exist['time'] == c['time']:
509
- market_state['ohlc_history'][i] = c
510
- market_state['ohlc_history'] = market_state['ohlc_history'][-800:]
511
- market_state['ready'] = True
512
- except Exception:
513
- pass
514
- await asyncio.sleep(10)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
 
516
- async def worker_broadcast():
517
  while True:
518
  if connected_clients and market_state['ready']:
519
  payload = process_market_data()
520
  if payload and "data" in payload:
521
  msg = json.dumps(payload)
522
- for ws in list(connected_clients):
 
523
  try:
524
  await ws.send_str(msg)
525
  except Exception:
526
- connected_clients.discard(ws)
 
527
  await asyncio.sleep(BROADCAST_RATE)
528
 
529
- async def ws_handler(request):
530
  ws = web.WebSocketResponse()
531
  await ws.prepare(request)
532
  connected_clients.add(ws)
 
533
  try:
534
- async for msg in ws: pass
 
535
  finally:
536
  connected_clients.discard(ws)
 
537
  return ws
538
 
539
- async def idx_handler(request):
540
  return web.Response(text=HTML_PAGE, content_type='text/html')
541
 
 
 
 
 
 
 
 
 
542
  async def main():
543
  app = web.Application()
544
- app.add_routes([web.get('/', idx_handler), web.get('/ws', ws_handler)])
545
- asyncio.create_task(worker_kraken())
546
- asyncio.create_task(worker_broadcast())
 
 
 
 
547
  runner = web.AppRunner(app)
548
  await runner.setup()
549
  site = web.TCPSite(runner, '0.0.0.0', PORT)
550
  await site.start()
551
- logging.info(f"http://localhost:{PORT}")
 
 
552
  await asyncio.Event().wait()
553
 
554
  if __name__ == "__main__":
555
  try:
556
  asyncio.run(main())
557
  except KeyboardInterrupt:
558
- pass
 
11
  SYMBOL_KRAKEN = "BTC/USD"
12
  PORT = 7860
13
  BROADCAST_RATE = 1.0
14
+ PREDICTION_HORIZON = 100
15
 
16
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
17
 
 
36
  df[c] = df[c].astype(float)
37
 
38
  df['ema'] = df['close'].ewm(span=20, adjust=False).mean()
39
+ df['ema_fast'] = df['close'].ewm(span=9, adjust=False).mean()
40
+ df['ema_slow'] = df['close'].ewm(span=50, adjust=False).mean()
41
+
42
  df['sma20'] = df['close'].rolling(window=20).mean()
43
  df['std'] = df['close'].rolling(window=20).std()
44
  df['bb_upper'] = df['sma20'] + (df['std'] * 2)
45
  df['bb_lower'] = df['sma20'] - (df['std'] * 2)
46
+ df['bb_mid'] = df['sma20']
47
+
48
  delta = df['close'].diff()
49
  gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
50
  loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
 
60
  low_min = df['low'].rolling(window=14).min()
61
  high_max = df['high'].rolling(window=14).max()
62
  df['stoch_k'] = 100 * ((df['close'] - low_min) / (high_max - low_min))
63
+ df['stoch_d'] = df['stoch_k'].rolling(window=3).mean()
64
+
65
  df['tr0'] = abs(df['high'] - df['low'])
66
  df['tr1'] = abs(df['high'] - df['close'].shift())
67
  df['tr2'] = abs(df['low'] - df['close'].shift())
68
  df['tr'] = df[['tr0', 'tr1', 'tr2']].max(axis=1)
69
  df['atr'] = df['tr'].rolling(window=14).mean()
70
 
71
+ df['obv'] = (np.sign(df['close'].diff()) * df['volume']).fillna(0).cumsum()
72
+
73
  df['tp'] = (df['high'] + df['low'] + df['close']) / 3
74
  df['vwap'] = (df['tp'] * df['volume']).cumsum() / df['volume'].cumsum()
75
 
76
  return df
77
 
78
  def train_model(df):
79
+ logging.info("Training ML Model...")
80
+
81
+ feature_cols = ['close', 'ema', 'bb_upper', 'bb_lower', 'rsi', 'macd', 'stoch_k', 'atr', 'obv', 'vwap']
82
+
83
  data = df.dropna().copy()
84
 
85
  future_shifts = {}
 
95
  data = data.dropna()
96
 
97
  if len(data) < 100:
98
+ logging.warning("Not enough data to train model yet.")
99
  return None
100
 
101
  X = data[feature_cols].values
102
  y = data[targets].values
103
 
104
+ model = RandomForestRegressor(n_estimators=50, max_depth=10, n_jobs=-1, random_state=42)
105
  model.fit(X, y)
106
+
107
+ logging.info(f"Model Trained on {len(X)} samples.")
108
  return model
109
 
110
  def get_prediction(df, model):
111
  if model is None:
112
  return []
113
 
114
+ feature_cols = ['close', 'ema', 'bb_upper', 'bb_lower', 'rsi', 'macd', 'stoch_k', 'atr', 'obv', 'vwap']
115
  last_row = df.iloc[[-1]][feature_cols]
116
 
117
  if last_row.isnull().values.any():
 
147
  predictions = []
148
  try:
149
  predictions = get_prediction(df, market_state['model'])
150
+ except Exception as e:
151
+ logging.error(f"Prediction failed: {e}")
152
 
153
  df_clean = df.replace([np.inf, -np.inf], np.nan)
154
  df_clean = df_clean.astype(object).where(pd.notnull(df_clean), None)
 
161
  market_state['price_change'] = price_change
162
 
163
  full_data = df_clean.to_dict('records')
164
+
165
  last_row = df.iloc[-1] if len(df) > 0 else {}
166
 
167
  return {
 
170
  "stats": {
171
  "price": last_close,
172
  "change": round(price_change, 2),
173
+ "rsi": round(float(last_row.get('rsi', 0)), 1) if pd.notna(last_row.get('rsi')) else 0,
174
+ "macd": round(float(last_row.get('macd', 0)), 2) if pd.notna(last_row.get('macd')) else 0,
175
+ "atr": round(float(last_row.get('atr', 0)), 2) if pd.notna(last_row.get('atr')) else 0,
176
+ "volume": round(float(last_row.get('volume', 0)), 2) if pd.notna(last_row.get('volume')) else 0
177
  }
178
  }
179
 
 
183
  <head>
184
  <meta charset="UTF-8">
185
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
186
+ <title>BTC/USD AI Predictor</title>
187
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
188
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
189
  <style>
 
 
 
 
 
 
 
 
 
 
 
190
  * { margin: 0; padding: 0; box-sizing: border-box; }
191
+
192
  body {
193
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
194
+ background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 100%);
195
+ color: #ffffff;
196
  height: 100vh;
197
+ display: flex;
198
+ flex-direction: column;
199
  overflow: hidden;
 
 
200
  }
201
+
202
  .header {
203
+ background: rgba(15, 15, 25, 0.95);
204
+ backdrop-filter: blur(20px);
205
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
206
+ padding: 12px 24px;
207
  display: flex;
208
  align-items: center;
209
  justify-content: space-between;
210
+ z-index: 100;
 
211
  }
212
+
213
+ .logo-section {
214
  display: flex;
215
  align-items: center;
216
+ gap: 16px;
217
  }
218
+
219
+ .logo {
220
+ font-size: 24px;
221
+ font-weight: 700;
222
+ background: linear-gradient(135deg, #00ff88 0%, #00d4ff 100%);
223
+ -webkit-background-clip: text;
224
+ -webkit-text-fill-color: transparent;
225
+ letter-spacing: -0.5px;
226
+ }
227
+
228
+ .symbol-badge {
229
+ background: rgba(0, 255, 136, 0.1);
230
+ border: 1px solid rgba(0, 255, 136, 0.3);
231
+ padding: 6px 14px;
232
+ border-radius: 20px;
233
+ font-size: 13px;
234
  font-weight: 600;
235
+ color: #00ff88;
236
+ }
237
+
238
+ .stats-row {
239
  display: flex;
240
+ gap: 24px;
241
  align-items: center;
 
242
  }
 
 
243
 
244
+ .stat-item {
245
+ display: flex;
246
+ flex-direction: column;
247
+ align-items: flex-end;
248
+ }
249
+
250
+ .stat-label {
251
+ font-size: 10px;
252
+ color: #666;
253
+ text-transform: uppercase;
254
+ letter-spacing: 0.5px;
255
+ }
256
+
257
+ .stat-value {
258
+ font-size: 15px;
259
+ font-weight: 600;
260
+ font-variant-numeric: tabular-nums;
261
+ }
262
+
263
+ .stat-value.positive { color: #00ff88; }
264
+ .stat-value.negative { color: #ff4757; }
265
+ .stat-value.neutral { color: #ffd700; }
266
+
267
+ .status-indicator {
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 8px;
271
+ font-size: 12px;
272
+ color: #888;
273
+ }
274
+
275
+ .status-dot {
276
+ width: 8px;
277
+ height: 8px;
278
+ border-radius: 50%;
279
+ background: #00ff88;
280
+ animation: pulse 2s infinite;
281
+ }
282
+
283
+ .status-dot.disconnected {
284
+ background: #ff4757;
285
+ animation: none;
286
+ }
287
+
288
+ @keyframes pulse {
289
+ 0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(0, 255, 136, 0.4); }
290
+ 50% { opacity: 0.8; box-shadow: 0 0 0 8px rgba(0, 255, 136, 0); }
291
+ }
292
+
293
+ .indicator-panel {
294
+ background: rgba(15, 15, 25, 0.8);
295
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
296
+ padding: 10px 24px;
297
+ display: flex;
298
+ gap: 32px;
299
+ overflow-x: auto;
300
  }
301
+
302
+ .indicator-group {
303
+ display: flex;
304
+ align-items: center;
305
+ gap: 12px;
306
+ }
307
+
308
+ .indicator-label {
309
+ font-size: 11px;
310
+ color: #666;
311
+ text-transform: uppercase;
312
+ }
313
+
314
+ .indicator-value {
315
+ font-size: 13px;
316
+ font-weight: 500;
317
+ font-variant-numeric: tabular-nums;
318
+ }
319
+
320
+ .charts-container {
321
+ flex: 1;
322
  display: flex;
323
  flex-direction: column;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  position: relative;
325
  }
326
+
327
+ .chart-wrapper {
328
+ position: relative;
329
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
330
+ }
331
+
332
+ #main-chart { flex: 5; }
333
+ #volume-chart { flex: 1; min-height: 60px; }
334
+ #osc-chart { flex: 1.5; min-height: 80px; }
335
+
336
+ .chart-label {
337
  position: absolute;
338
+ top: 12px;
339
+ left: 16px;
340
+ z-index: 10;
341
  display: flex;
342
+ gap: 16px;
343
+ font-size: 11px;
 
344
  pointer-events: none;
345
  }
 
 
346
 
347
+ .chart-label span {
348
+ display: flex;
349
+ align-items: center;
350
+ gap: 6px;
351
+ }
352
+
353
+ .chart-label .dot {
354
+ width: 8px;
355
+ height: 8px;
356
+ border-radius: 50%;
357
+ }
358
+
359
+ .loading-overlay {
360
  position: absolute;
361
+ top: 0;
362
+ left: 0;
363
+ right: 0;
364
+ bottom: 0;
365
+ background: rgba(10, 10, 15, 0.95);
366
  display: flex;
367
  flex-direction: column;
368
  align-items: center;
369
  justify-content: center;
370
+ z-index: 1000;
371
+ transition: opacity 0.5s ease;
372
+ }
373
+
374
+ .loading-overlay.hidden {
375
+ opacity: 0;
376
+ pointer-events: none;
377
+ }
378
+
379
+ .loader {
380
+ width: 50px;
381
+ height: 50px;
382
+ border: 3px solid rgba(0, 255, 136, 0.1);
383
+ border-top-color: #00ff88;
384
  border-radius: 50%;
385
  animation: spin 1s linear infinite;
386
  }
 
 
387
 
388
+ @keyframes spin {
389
+ to { transform: rotate(360deg); }
390
+ }
391
+
392
+ .loading-text {
393
+ margin-top: 20px;
394
+ font-size: 14px;
395
+ color: #666;
396
+ }
397
+
398
+ .prediction-badge {
399
+ position: absolute;
400
+ top: 12px;
401
+ right: 16px;
402
+ background: rgba(191, 90, 242, 0.15);
403
+ border: 1px solid rgba(191, 90, 242, 0.3);
404
  padding: 4px 10px;
405
  border-radius: 12px;
406
+ font-size: 10px;
407
+ color: #bf5af2;
408
+ z-index: 10;
409
  }
410
  </style>
411
  </head>
412
  <body>
413
  <div class="header">
414
+ <div class="logo-section">
415
+ <div class="logo">QuantAI</div>
416
+ <div class="symbol-badge">BTC/USD</div>
417
  </div>
418
+
419
+ <div class="stats-row">
420
+ <div class="stat-item">
421
+ <span class="stat-label">Price</span>
422
+ <span id="price" class="stat-value">$--</span>
423
+ </div>
424
+ <div class="stat-item">
425
+ <span class="stat-label">Change</span>
426
+ <span id="change" class="stat-value neutral">--%</span>
 
 
427
  </div>
428
+ <div class="stat-item">
429
+ <span class="stat-label">RSI</span>
430
+ <span id="rsi" class="stat-value">--</span>
431
  </div>
432
+ <div class="stat-item">
433
+ <span class="stat-label">ATR</span>
434
+ <span id="atr" class="stat-value">--</span>
435
  </div>
436
+ </div>
437
+
438
+ <div class="status-indicator">
439
+ <div id="status-dot" class="status-dot"></div>
440
+ <span id="status-text">Connecting...</span>
441
+ </div>
442
+ </div>
443
+
444
+ <div class="indicator-panel">
445
+ <div class="indicator-group">
446
+ <span class="indicator-label">EMA 20</span>
447
+ <span id="ema-val" class="indicator-value" style="color: #2962FF">--</span>
448
+ </div>
449
+ <div class="indicator-group">
450
+ <span class="indicator-label">BB Upper</span>
451
+ <span id="bb-upper" class="indicator-value" style="color: #26a69a">--</span>
452
+ </div>
453
+ <div class="indicator-group">
454
+ <span class="indicator-label">BB Lower</span>
455
+ <span id="bb-lower" class="indicator-value" style="color: #ef5350">--</span>
456
+ </div>
457
+ <div class="indicator-group">
458
+ <span class="indicator-label">MACD</span>
459
+ <span id="macd-val" class="indicator-value">--</span>
460
+ </div>
461
+ <div class="indicator-group">
462
+ <span class="indicator-label">Stoch K</span>
463
+ <span id="stoch-val" class="indicator-value" style="color: #ff9800">--</span>
464
+ </div>
465
+ <div class="indicator-group">
466
+ <span class="indicator-label">Volume</span>
467
+ <span id="vol-val" class="indicator-value" style="color: #888">--</span>
468
+ </div>
469
+ </div>
470
+
471
+ <div class="charts-container">
472
+ <div class="loading-overlay" id="loading">
473
+ <div class="loader"></div>
474
+ <div class="loading-text">Loading market data...</div>
475
+ </div>
476
+
477
+ <div id="main-chart" class="chart-wrapper">
478
+ <div class="chart-label">
479
+ <span><div class="dot" style="background: #00ff88"></div>Price</span>
480
+ <span><div class="dot" style="background: #2962FF"></div>EMA 20</span>
481
+ <span><div class="dot" style="background: #26a69a; opacity: 0.5"></div>Bollinger</span>
482
  </div>
483
+ <div class="prediction-badge">AI Forecast: 100 candles</div>
484
+ </div>
485
+
486
+ <div id="volume-chart" class="chart-wrapper">
487
+ <div class="chart-label">
488
+ <span><div class="dot" style="background: #5c6bc0"></div>Volume</span>
489
  </div>
490
  </div>
491
 
492
+ <div id="osc-chart" class="chart-wrapper">
493
+ <div class="chart-label">
494
+ <span><div class="dot" style="background: #9C27B0"></div>RSI</span>
495
+ <span><div class="dot" style="background: #26a69a"></div>MACD Hist</span>
 
 
 
 
 
 
 
 
496
  </div>
 
497
  </div>
498
  </div>
499
 
500
  <script>
501
+ document.addEventListener('DOMContentLoaded', () => {
502
+ const mainEl = document.getElementById('main-chart');
503
+ const volEl = document.getElementById('volume-chart');
504
+ const oscEl = document.getElementById('osc-chart');
505
+ const loading = document.getElementById('loading');
506
+
507
+ const chartOptions = {
508
+ layout: {
509
+ background: { type: 'solid', color: 'transparent' },
510
+ textColor: '#666'
511
+ },
512
+ grid: {
513
+ vertLines: { color: 'rgba(255,255,255,0.03)' },
514
+ horzLines: { color: 'rgba(255,255,255,0.03)' }
515
+ },
516
+ timeScale: {
517
+ timeVisible: true,
518
+ secondsVisible: false,
519
+ borderColor: 'rgba(255,255,255,0.1)'
520
+ },
521
+ rightPriceScale: {
522
+ borderColor: 'rgba(255,255,255,0.1)'
523
+ },
524
+ crosshair: {
525
+ mode: LightweightCharts.CrosshairMode.Normal,
526
+ vertLine: {
527
+ color: 'rgba(255,255,255,0.2)',
528
+ labelBackgroundColor: '#1a1a2e'
529
+ },
530
+ horzLine: {
531
+ color: 'rgba(255,255,255,0.2)',
532
+ labelBackgroundColor: '#1a1a2e'
533
+ }
534
+ }
535
  };
536
+
537
+ const mainChart = LightweightCharts.createChart(mainEl, chartOptions);
538
+ const volChart = LightweightCharts.createChart(volEl, chartOptions);
539
+ const oscChart = LightweightCharts.createChart(oscEl, chartOptions);
540
+
541
+ const candles = mainChart.addCandlestickSeries({
542
+ upColor: '#00ff88',
543
+ downColor: '#ff4757',
544
+ borderUpColor: '#00ff88',
545
+ borderDownColor: '#ff4757',
546
+ wickUpColor: '#00ff88',
547
+ wickDownColor: '#ff4757'
548
  });
549
+
550
+ const ema = mainChart.addLineSeries({
551
+ color: '#2962FF',
552
+ lineWidth: 2,
553
+ crosshairMarkerVisible: false
554
+ });
555
+
556
+ const bbUpper = mainChart.addLineSeries({
557
+ color: 'rgba(38, 166, 154, 0.4)',
558
+ lineWidth: 1,
559
+ crosshairMarkerVisible: false
560
+ });
561
+
562
+ const bbLower = mainChart.addLineSeries({
563
+ color: 'rgba(239, 83, 80, 0.4)',
564
+ lineWidth: 1,
565
+ crosshairMarkerVisible: false
566
+ });
567
+
568
+ const predLine = mainChart.addLineSeries({
569
+ color: '#bf5af2',
570
+ lineWidth: 2,
571
+ lineStyle: LightweightCharts.LineStyle.Dashed,
572
+ crosshairMarkerVisible: false
573
+ });
574
+
575
+ const volumeSeries = volChart.addHistogramSeries({
576
+ priceFormat: { type: 'volume' },
577
+ priceScaleId: ''
578
+ });
579
+ volChart.priceScale('').applyOptions({
580
+ scaleMargins: { top: 0.1, bottom: 0 }
581
+ });
582
+
583
+ const rsi = oscChart.addLineSeries({
584
+ color: '#9C27B0',
585
+ lineWidth: 2,
586
+ priceScaleId: 'rsi'
587
+ });
588
+ oscChart.priceScale('rsi').applyOptions({
589
+ scaleMargins: { top: 0.1, bottom: 0.1 }
590
+ });
591
+
592
+ const macdHist = oscChart.addHistogramSeries({
593
+ priceScaleId: 'macd'
594
+ });
595
+ oscChart.priceScale('macd').applyOptions({
596
+ scaleMargins: { top: 0.6, bottom: 0 }
597
+ });
598
+
599
+ function resizeCharts() {
600
+ const mainH = mainEl.clientHeight;
601
+ const volH = volEl.clientHeight;
602
+ const oscH = oscEl.clientHeight;
603
+ const w = mainEl.clientWidth;
604
+
605
+ mainChart.applyOptions({ width: w, height: mainH });
606
+ volChart.applyOptions({ width: w, height: volH });
607
+ oscChart.applyOptions({ width: w, height: oscH });
608
  }
609
+
610
+ new ResizeObserver(resizeCharts).observe(document.body);
611
+ setTimeout(resizeCharts, 100);
612
+
613
+ function syncTimeScales(charts) {
614
+ charts.forEach((chart, i) => {
615
+ chart.timeScale().subscribeVisibleLogicalRangeChange(range => {
616
+ if (range) {
617
+ charts.forEach((c, j) => {
618
+ if (i !== j) c.timeScale().setVisibleLogicalRange(range);
619
+ });
620
+ }
621
+ });
622
+ });
623
+ }
624
+ syncTimeScales([mainChart, volChart, oscChart]);
625
+
626
+ function updateStats(stats, lastData) {
627
+ if (stats) {
628
+ document.getElementById('price').textContent = '$' + stats.price.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2});
629
+
630
+ const changeEl = document.getElementById('change');
631
+ changeEl.textContent = (stats.change >= 0 ? '+' : '') + stats.change + '%';
632
+ changeEl.className = 'stat-value ' + (stats.change > 0 ? 'positive' : stats.change < 0 ? 'negative' : 'neutral');
633
+
634
+ const rsiVal = stats.rsi;
635
+ const rsiEl = document.getElementById('rsi');
636
+ rsiEl.textContent = rsiVal;
637
+ rsiEl.className = 'stat-value ' + (rsiVal > 70 ? 'negative' : rsiVal < 30 ? 'positive' : 'neutral');
638
+
639
+ document.getElementById('atr').textContent = stats.atr;
640
+ }
641
 
642
+ if (lastData) {
643
+ document.getElementById('ema-val').textContent = lastData.ema ? lastData.ema.toFixed(2) : '--';
644
+ document.getElementById('bb-upper').textContent = lastData.bb_upper ? lastData.bb_upper.toFixed(2) : '--';
645
+ document.getElementById('bb-lower').textContent = lastData.bb_lower ? lastData.bb_lower.toFixed(2) : '--';
646
+
647
+ const macdVal = lastData.macd;
648
+ const macdEl = document.getElementById('macd-val');
649
+ if (macdVal !== null && macdVal !== undefined) {
650
+ macdEl.textContent = macdVal.toFixed(2);
651
+ macdEl.style.color = macdVal >= 0 ? '#26a69a' : '#ef5350';
652
+ }
653
+
654
+ document.getElementById('stoch-val').textContent = lastData.stoch_k ? lastData.stoch_k.toFixed(1) : '--';
655
+ document.getElementById('vol-val').textContent = lastData.volume ? lastData.volume.toFixed(2) : '--';
656
+ }
657
+ }
658
+
659
+ function setStatus(connected) {
660
+ const dot = document.getElementById('status-dot');
661
+ const text = document.getElementById('status-text');
662
+ if (connected) {
663
+ dot.className = 'status-dot';
664
+ text.textContent = 'Live';
665
+ } else {
666
+ dot.className = 'status-dot disconnected';
667
+ text.textContent = 'Reconnecting...';
668
+ }
669
+ }
670
+
671
+ let hasData = false;
672
+
673
+ function connect() {
674
+ const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
675
+ const ws = new WebSocket(protocol + '://' + location.host + '/ws');
676
 
677
+ ws.onopen = () => setStatus(true);
 
 
 
 
678
 
679
+ ws.onmessage = (e) => {
680
  try {
681
+ const payload = JSON.parse(e.data);
682
+ if (!payload.data || payload.data.length === 0) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
683
 
684
+ const d = payload.data;
685
 
686
+ const safeMap = (arr, key) => arr
687
+ .filter(x => x && x.time && x[key] !== null && x[key] !== undefined && !isNaN(x[key]))
688
+ .map(x => ({ time: x.time, value: x[key] }));
689
+
690
+ const candleData = d
691
+ .filter(x => x && x.time && x.open && x.high && x.low && x.close)
692
+ .map(x => ({
693
+ time: x.time,
694
+ open: x.open,
695
+ high: x.high,
696
+ low: x.low,
697
+ close: x.close
698
+ }));
699
+
700
+ if (candleData.length > 0) {
701
+ candles.setData(candleData);
702
+
703
+ const emaData = safeMap(d, 'ema');
704
+ if (emaData.length > 0) ema.setData(emaData);
705
+
706
+ const bbUpperData = safeMap(d, 'bb_upper');
707
+ if (bbUpperData.length > 0) bbUpper.setData(bbUpperData);
708
+
709
+ const bbLowerData = safeMap(d, 'bb_lower');
710
+ if (bbLowerData.length > 0) bbLower.setData(bbLowerData);
711
+
712
+ const volData = d
713
+ .filter(x => x && x.time && x.volume !== null && x.volume !== undefined)
714
+ .map(x => ({
715
+ time: x.time,
716
+ value: x.volume,
717
+ color: x.close >= x.open ? 'rgba(0, 255, 136, 0.5)' : 'rgba(255, 71, 87, 0.5)'
718
+ }));
719
+ if (volData.length > 0) volumeSeries.setData(volData);
720
+
721
+ const rsiData = safeMap(d, 'rsi');
722
+ if (rsiData.length > 0) rsi.setData(rsiData);
723
+
724
+ const macdData = d
725
+ .filter(x => x && x.time && x.macd_hist !== null && x.macd_hist !== undefined && !isNaN(x.macd_hist))
726
+ .map(x => ({
727
+ time: x.time,
728
+ value: x.macd_hist,
729
+ color: x.macd_hist >= 0 ? '#26a69a' : '#ef5350'
730
+ }));
731
+ if (macdData.length > 0) macdHist.setData(macdData);
732
+
733
+ if (payload.prediction && payload.prediction.length > 0) {
734
+ const lastCandle = candleData[candleData.length - 1];
735
+ const predData = [
736
+ { time: lastCandle.time, value: lastCandle.close },
737
+ ...payload.prediction.filter(p => p && p.time && p.value !== null && !isNaN(p.value))
738
+ ];
739
+ predLine.setData(predData);
740
+ }
741
+
742
+ updateStats(payload.stats, d[d.length - 1]);
743
+
744
+ if (!hasData) {
745
+ hasData = true;
746
+ loading.classList.add('hidden');
747
+ mainChart.timeScale().fitContent();
748
+ }
749
  }
750
+ } catch (err) {
751
+ console.error("Chart error:", err);
752
  }
753
  };
754
+
755
+ ws.onclose = () => {
756
+ setStatus(false);
757
+ setTimeout(connect, 2000);
758
+ };
759
+
760
+ ws.onerror = () => ws.close();
761
  }
762
 
763
  connect();
764
+ });
765
  </script>
766
  </body>
767
  </html>
 
771
  try:
772
  async with aiohttp.ClientSession() as session:
773
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
774
+ async with session.get(url, timeout=aiohttp.ClientTimeout(total=30)) as response:
775
  if response.status == 200:
776
  data = await response.json()
777
  if 'result' in data:
 
779
  if key != 'last':
780
  raw = data['result'][key]
781
  market_state['ohlc_history'] = [
782
+ {
783
+ 'time': int(c[0]),
784
+ 'open': float(c[1]),
785
+ 'high': float(c[2]),
786
+ 'low': float(c[3]),
787
+ 'close': float(c[4]),
788
+ 'volume': float(c[6])
789
+ }
790
  for c in raw[-720:]
791
  ]
792
  market_state['ready'] = True
793
+ logging.info(f"Loaded {len(market_state['ohlc_history'])} initial candles")
794
+ return True
795
+ except Exception as e:
796
+ logging.error(f"Initial data fetch error: {e}")
797
+ return False
798
 
799
+ async def kraken_rest_worker():
800
  await fetch_initial_data()
801
+
802
  while True:
803
  try:
804
  async with aiohttp.ClientSession() as session:
805
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
806
+ async with session.get(url, timeout=aiohttp.ClientTimeout(total=30)) as response:
807
  if response.status == 200:
808
  data = await response.json()
809
+ if 'result' in data:
810
+ for key in data['result']:
811
+ if key != 'last':
812
+ raw = data['result'][key]
813
+ new_candles = [
814
+ {
815
+ 'time': int(c[0]),
816
+ 'open': float(c[1]),
817
+ 'high': float(c[2]),
818
+ 'low': float(c[3]),
819
+ 'close': float(c[4]),
820
+ 'volume': float(c[6])
821
+ }
822
+ for c in raw[-10:]
823
+ ]
824
+
825
+ if market_state['ohlc_history']:
826
+ existing_times = {c['time'] for c in market_state['ohlc_history']}
827
+ for nc in new_candles:
828
+ if nc['time'] in existing_times:
829
+ for i, ec in enumerate(market_state['ohlc_history']):
830
+ if ec['time'] == nc['time']:
831
+ market_state['ohlc_history'][i] = nc
832
+ break
833
+ else:
834
+ market_state['ohlc_history'].append(nc)
835
+
836
+ market_state['ohlc_history'].sort(key=lambda x: x['time'])
837
+
838
+ if len(market_state['ohlc_history']) > 800:
839
+ market_state['ohlc_history'] = market_state['ohlc_history'][-800:]
840
+
841
+ market_state['ready'] = True
842
+ break
843
+ except Exception as e:
844
+ logging.warning(f"REST update error: {e}")
845
+
846
+ await asyncio.sleep(5)
847
 
848
+ async def broadcast_worker():
849
  while True:
850
  if connected_clients and market_state['ready']:
851
  payload = process_market_data()
852
  if payload and "data" in payload:
853
  msg = json.dumps(payload)
854
+ disconnected = set()
855
+ for ws in connected_clients:
856
  try:
857
  await ws.send_str(msg)
858
  except Exception:
859
+ disconnected.add(ws)
860
+ connected_clients.difference_update(disconnected)
861
  await asyncio.sleep(BROADCAST_RATE)
862
 
863
+ async def websocket_handler(request):
864
  ws = web.WebSocketResponse()
865
  await ws.prepare(request)
866
  connected_clients.add(ws)
867
+ logging.info(f"Client connected. Total: {len(connected_clients)}")
868
  try:
869
+ async for msg in ws:
870
+ pass
871
  finally:
872
  connected_clients.discard(ws)
873
+ logging.info(f"Client disconnected. Total: {len(connected_clients)}")
874
  return ws
875
 
876
+ async def handle_index(request):
877
  return web.Response(text=HTML_PAGE, content_type='text/html')
878
 
879
+ async def handle_health(request):
880
+ return web.json_response({
881
+ "status": "ok",
882
+ "ready": market_state['ready'],
883
+ "candles": len(market_state['ohlc_history']),
884
+ "clients": len(connected_clients)
885
+ })
886
+
887
  async def main():
888
  app = web.Application()
889
+ app.router.add_get('/', handle_index)
890
+ app.router.add_get('/ws', websocket_handler)
891
+ app.router.add_get('/health', handle_health)
892
+
893
+ asyncio.create_task(kraken_rest_worker())
894
+ asyncio.create_task(broadcast_worker())
895
+
896
  runner = web.AppRunner(app)
897
  await runner.setup()
898
  site = web.TCPSite(runner, '0.0.0.0', PORT)
899
  await site.start()
900
+
901
+ logging.info(f"Server running at http://localhost:{PORT}")
902
+
903
  await asyncio.Event().wait()
904
 
905
  if __name__ == "__main__":
906
  try:
907
  asyncio.run(main())
908
  except KeyboardInterrupt:
909
+ logging.info("Shutting down...")