Alvin3y1 commited on
Commit
8b57377
·
verified ·
1 Parent(s): a4227fa

Update app.py

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