Alvin3y1 commited on
Commit
e96c872
·
verified ·
1 Parent(s): 2d2ed3b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +212 -821
app.py CHANGED
@@ -2,447 +2,211 @@ import asyncio
2
  import json
3
  import logging
4
  import time
 
5
  import aiohttp
6
  import pandas as pd
7
  import numpy as np
8
  from aiohttp import web
9
- from sklearn.ensemble import GradientBoostingRegressor
10
- from sklearn.preprocessing import RobustScaler
11
- import warnings
12
- warnings.filterwarnings('ignore')
13
 
14
- # --- CONFIGURATION ---
15
  SYMBOL_KRAKEN = "BTC/USD"
16
  PORT = 7860
17
  BROADCAST_RATE = 1.0
18
  PREDICTION_HORIZON = 100
19
  MAX_HISTORY = 5000
20
  TRAIN_INTERVAL = 300
21
- MIN_TRAINING_SAMPLES = 300
22
 
23
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
24
 
25
- # Feature columns for ML model
26
- FEATURE_COLS = [
27
- 'rsi_norm', 'rsi_slope',
28
- 'macd_hist_norm', 'macd_slope',
29
- 'atr_pct',
30
- 'dist_ema20', 'dist_ema50', 'ema_cross',
31
- 'bb_width', 'bb_pos',
32
- 'vol_zscore',
33
- 'ret_1', 'ret_5', 'ret_10', 'ret_20',
34
- 'volatility_ratio',
35
- 'candle_body', 'upper_wick', 'lower_wick',
36
- 'trend_strength'
37
- ]
38
-
39
- # Key horizons to predict (reduces noise vs predicting all 100)
40
- KEY_HORIZONS = [1, 3, 5, 10, 20, 35, 50, 75, 100]
41
-
42
  market_state = {
43
  "ohlc_history": [],
44
  "ready": False,
45
- "models": {}, # Dictionary of models for each horizon
46
- "scaler": None,
47
  "last_training_time": 0,
48
  "last_price": 0,
49
- "price_change": 0,
50
- "training_metrics": {}
51
  }
52
 
53
  connected_clients = set()
54
 
55
-
56
- def safe_divide(a, b, default=0.0):
57
- """Safe division that handles zeros and NaN"""
58
- with np.errstate(divide='ignore', invalid='ignore'):
59
- result = np.where(b != 0, a / b, default)
60
- result = np.where(np.isfinite(result), result, default)
61
- return result
62
-
63
-
64
  def calculate_indicators(candles):
65
- """Calculate technical indicators with robust normalization"""
66
- if len(candles) < 60:
67
  return None
68
 
69
- df = pd.DataFrame(candles).copy()
70
  cols = ['open', 'high', 'low', 'close', 'volume']
71
  for c in cols:
72
- df[c] = pd.to_numeric(df[c], errors='coerce')
73
-
74
- df = df.dropna(subset=['open', 'high', 'low', 'close'])
75
- if len(df) < 60:
76
- return None
77
 
78
- close = df['close']
79
- high = df['high']
80
- low = df['low']
81
- volume = df['volume'].fillna(0)
82
 
83
- # --- EXPONENTIAL MOVING AVERAGES ---
84
- df['ema20'] = close.ewm(span=20, adjust=False).mean()
85
- df['ema50'] = close.ewm(span=50, adjust=False).mean()
86
-
87
- # --- BOLLINGER BANDS ---
88
- df['sma20'] = close.rolling(window=20).mean()
89
- df['std20'] = close.rolling(window=20).std()
90
- df['bb_upper'] = df['sma20'] + (df['std20'] * 2)
91
- df['bb_lower'] = df['sma20'] - (df['std20'] * 2)
92
 
93
- # --- RSI ---
94
- delta = close.diff()
95
- gain = delta.where(delta > 0, 0).rolling(window=14).mean()
96
  loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
97
- rs = safe_divide(gain.values, loss.values, 1.0)
98
  df['rsi'] = 100 - (100 / (1 + rs))
99
- df['rsi'] = df['rsi'].fillna(50).clip(0, 100)
100
-
101
- # Normalized RSI (centered at 0, range -1 to 1)
102
- df['rsi_norm'] = (df['rsi'] - 50) / 50
103
- df['rsi_slope'] = df['rsi'].diff(5).fillna(0) / 50 # 5-period RSI change
104
 
105
- # --- MACD ---
106
- ema12 = close.ewm(span=12, adjust=False).mean()
107
- ema26 = close.ewm(span=26, adjust=False).mean()
108
- df['macd'] = ema12 - ema26
109
  df['macd_signal'] = df['macd'].ewm(span=9, adjust=False).mean()
110
  df['macd_hist'] = df['macd'] - df['macd_signal']
111
-
112
- # Normalize MACD by ATR to make it price-independent
113
- atr_for_norm = close.rolling(20).std().replace(0, 1)
114
- df['macd_hist_norm'] = df['macd_hist'] / atr_for_norm
115
- df['macd_hist_norm'] = df['macd_hist_norm'].clip(-5, 5)
116
- df['macd_slope'] = df['macd_hist_norm'].diff(3).fillna(0)
117
 
118
- # --- ATR (Average True Range) ---
119
- tr1 = abs(high - low)
120
- tr2 = abs(high - close.shift())
121
- tr3 = abs(low - close.shift())
122
- df['tr'] = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
123
  df['atr'] = df['tr'].rolling(window=14).mean()
124
-
125
- # ATR as percentage of price (volatility measure)
126
- df['atr_pct'] = safe_divide(df['atr'].values, close.values) * 100
127
-
128
- # --- NORMALIZED PRICE FEATURES ---
129
-
130
- # Distance from EMAs (percentage)
131
- df['dist_ema20'] = safe_divide((close - df['ema20']).values, df['ema20'].values) * 100
132
- df['dist_ema50'] = safe_divide((close - df['ema50']).values, df['ema50'].values) * 100
133
-
134
- # EMA cross strength
135
- df['ema_cross'] = safe_divide((df['ema20'] - df['ema50']).values, df['ema50'].values) * 100
136
-
137
- # --- BOLLINGER BAND FEATURES ---
138
- bb_range = df['bb_upper'] - df['bb_lower']
139
- bb_range_safe = bb_range.replace(0, np.nan).fillna(close * 0.01) # Fallback to 1% of price
140
-
141
- df['bb_width'] = safe_divide(bb_range.values, df['sma20'].values) * 100
142
- df['bb_pos'] = safe_divide((close - df['bb_lower']).values, bb_range_safe.values)
143
- df['bb_pos'] = df['bb_pos'].clip(-0.5, 1.5).fillna(0.5) # Allow some overflow
144
 
145
- # --- VOLUME FEATURES ---
146
- vol_mean = volume.rolling(window=20).mean().replace(0, 1)
147
- vol_std = volume.rolling(window=20).std().replace(0, 1)
148
- df['vol_zscore'] = safe_divide((volume - vol_mean).values, vol_std.values)
149
- df['vol_zscore'] = df['vol_zscore'].clip(-3, 3).fillna(0)
150
-
151
- # --- RETURN FEATURES (momentum) ---
152
- df['ret_1'] = close.pct_change(1).fillna(0) * 100
153
- df['ret_5'] = close.pct_change(5).fillna(0) * 100
154
- df['ret_10'] = close.pct_change(10).fillna(0) * 100
155
- df['ret_20'] = close.pct_change(20).fillna(0) * 100
156
-
157
- # Clip extreme returns
158
- for col in ['ret_1', 'ret_5', 'ret_10', 'ret_20']:
159
- df[col] = df[col].clip(-10, 10)
160
 
161
- # --- VOLATILITY FEATURES ---
162
- vol_short = df['ret_1'].rolling(5).std().fillna(0)
163
- vol_long = df['ret_1'].rolling(20).std().replace(0, 1)
164
- df['volatility_ratio'] = safe_divide(vol_short.values, vol_long.values).clip(0, 3)
 
165
 
166
- # --- CANDLESTICK FEATURES ---
167
- candle_range = (high - low).replace(0, 0.01)
168
- df['candle_body'] = safe_divide((close - df['open']).values, candle_range.values)
169
- df['upper_wick'] = safe_divide((high - pd.concat([close, df['open']], axis=1).max(axis=1)).values, candle_range.values)
170
- df['lower_wick'] = safe_divide((pd.concat([close, df['open']], axis=1).min(axis=1) - low).values, candle_range.values)
171
-
172
- # --- TREND STRENGTH ---
173
- # Compare current price to 20-period high/low range
174
- rolling_high = high.rolling(20).max()
175
- rolling_low = low.rolling(20).min()
176
- rolling_range = (rolling_high - rolling_low).replace(0, 1)
177
- df['trend_strength'] = safe_divide((close - rolling_low).values, rolling_range.values) * 2 - 1 # -1 to 1
178
-
179
- # Replace any remaining infinities or NaN
180
- df = df.replace([np.inf, -np.inf], np.nan)
181
-
182
  return df
183
 
184
-
185
- def prepare_training_data(df):
186
- """Prepare features and multi-horizon targets for training"""
187
- data = df.copy()
188
-
189
- # Create target: future return at each key horizon
190
- target_cols = []
191
- for h in KEY_HORIZONS:
192
- col_name = f'target_{h}'
193
- future_price = data['close'].shift(-h)
194
- current_price = data['close']
195
- # Target is percentage return
196
- data[col_name] = safe_divide((future_price - current_price).values, current_price.values) * 100
197
- target_cols.append(col_name)
198
-
199
- # Drop rows with NaN in features or targets
200
- required_cols = FEATURE_COLS + target_cols
201
- data = data.dropna(subset=required_cols)
202
-
203
- if len(data) < MIN_TRAINING_SAMPLES:
204
- return None, None
205
-
206
- X = data[FEATURE_COLS].values
207
- y_dict = {h: data[f'target_{h}'].values for h in KEY_HORIZONS}
208
-
209
- return X, y_dict
210
-
211
-
212
  def train_model(df):
213
- """Train separate models for each prediction horizon"""
214
- logging.info(f"Training ML Models on {len(df)} candles...")
215
-
216
- X, y_dict = prepare_training_data(df)
217
-
218
- if X is None:
219
- logging.warning("Not enough training data")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  return None, None
221
-
222
- logging.info(f"Training data: {len(X)} samples, {len(FEATURE_COLS)} features")
223
-
224
- # Robust scaling handles outliers better than StandardScaler
225
- scaler = RobustScaler()
226
- X_scaled = scaler.fit_transform(X)
227
-
228
- models = {}
229
- metrics = {}
230
-
231
- for h in KEY_HORIZONS:
232
- y = y_dict[h]
233
-
234
- # Gradient Boosting with regularization to prevent overfitting
235
- model = GradientBoostingRegressor(
236
- n_estimators=150,
237
- max_depth=4,
238
- learning_rate=0.05,
239
- min_samples_split=30,
240
- min_samples_leaf=15,
241
- subsample=0.8,
242
- max_features='sqrt',
243
- validation_fraction=0.15,
244
- n_iter_no_change=10,
245
- random_state=42,
246
- verbose=0
247
- )
248
-
249
- model.fit(X_scaled, y)
250
- models[h] = model
251
-
252
- # Calculate training R² score
253
- train_score = model.score(X_scaled, y)
254
- metrics[h] = {'r2': round(train_score, 3)}
255
-
256
- logging.info(f" Horizon {h:3d}: R² = {train_score:.3f}")
257
-
258
- # Log feature importance (from longest horizon model)
259
- if 100 in models:
260
- importance = dict(zip(FEATURE_COLS, models[100].feature_importances_))
261
- top_5 = sorted(importance.items(), key=lambda x: x[1], reverse=True)[:5]
262
- logging.info(f"Top features: {[f'{k}:{v:.3f}' for k,v in top_5]}")
263
-
264
- market_state['training_metrics'] = metrics
265
- logging.info("Model training complete")
266
-
267
- return models, scaler
268
-
269
-
270
- def interpolate_predictions(horizon_preds, target_horizon):
271
- """Interpolate between key horizon predictions for smooth curve"""
272
- horizons = sorted(horizon_preds.keys())
273
-
274
- if target_horizon <= horizons[0]:
275
- return horizon_preds[horizons[0]]
276
- if target_horizon >= horizons[-1]:
277
- return horizon_preds[horizons[-1]]
278
-
279
- # Find surrounding horizons
280
- lower_h = max([h for h in horizons if h <= target_horizon])
281
- upper_h = min([h for h in horizons if h >= target_horizon])
282
-
283
- if lower_h == upper_h:
284
- return horizon_preds[lower_h]
285
-
286
- # Cubic interpolation weight for smoother curves
287
- t = (target_horizon - lower_h) / (upper_h - lower_h)
288
- t_smooth = t * t * (3 - 2 * t) # Smoothstep function
289
-
290
- return horizon_preds[lower_h] + (horizon_preds[upper_h] - horizon_preds[lower_h]) * t_smooth
291
-
292
 
293
- def apply_trend_smoothing(predictions, window=5):
294
- """Apply exponential moving average smoothing to predictions"""
295
- if len(predictions) < window:
296
- return predictions
297
-
298
- smoothed = []
299
- alpha = 2 / (window + 1)
300
-
301
- # Initialize with first value
302
- ema = predictions[0]
303
- smoothed.append(ema)
 
 
304
 
305
- for i in range(1, len(predictions)):
306
- ema = alpha * predictions[i] + (1 - alpha) * ema
307
- smoothed.append(ema)
308
 
309
- return smoothed
310
-
311
 
312
- def get_prediction(df, models, scaler):
313
- """Generate price predictions for the next N candles"""
314
- if not models or scaler is None:
315
  return []
316
 
317
- # Check if we have valid features
318
- last_row = df.iloc[-1:].copy()
 
 
 
 
 
 
 
 
319
 
320
- # Validate features
321
- missing_features = [col for col in FEATURE_COLS if col not in last_row.columns]
322
- if missing_features:
323
- logging.error(f"Missing features: {missing_features}")
324
- return []
325
 
326
- feature_values = last_row[FEATURE_COLS]
327
- if feature_values.isnull().values.any():
328
- logging.warning("NaN in prediction features")
329
  return []
 
 
330
 
331
- try:
332
- X = feature_values.values
333
- X_scaled = scaler.transform(X)
334
-
335
- current_price = float(df.iloc[-1]['close'])
336
- current_time = int(df.iloc[-1]['time'])
337
-
338
- # Get predictions at key horizons
339
- horizon_preds = {}
340
- for h in KEY_HORIZONS:
341
- if h in models:
342
- pred_return = models[h].predict(X_scaled)[0]
343
- # Clip extreme predictions
344
- pred_return = np.clip(pred_return, -15, 15) # Max ±15% move
345
- horizon_preds[h] = pred_return
346
-
347
- if not horizon_preds:
348
- return []
349
-
350
- # Interpolate for all time steps
351
- raw_returns = []
352
- for i in range(1, PREDICTION_HORIZON + 1):
353
- pct_return = interpolate_predictions(horizon_preds, i)
354
- raw_returns.append(pct_return)
355
-
356
- # Apply trend smoothing
357
- smoothed_returns = apply_trend_smoothing(raw_returns, window=7)
358
-
359
- # Convert to prices with momentum continuation
360
- predictions = []
361
- prev_price = current_price
362
-
363
- for i, pct_return in enumerate(smoothed_returns):
364
- # Price = current * (1 + cumulative_return%)
365
- future_price = current_price * (1 + pct_return / 100)
366
-
367
- # Add slight momentum continuation (reduces jumps)
368
- if i > 0:
369
- momentum = (future_price - prev_price) * 0.1
370
- future_price = future_price + momentum
371
-
372
- predictions.append({
373
- "time": current_time + ((i + 1) * 60),
374
- "value": round(float(future_price), 2)
375
- })
376
- prev_price = future_price
377
-
378
- return predictions
379
 
380
- except Exception as e:
381
- logging.error(f"Prediction error: {e}")
382
- return []
383
 
 
 
 
 
 
 
 
 
384
 
385
  def process_market_data():
386
- """Process market data and generate predictions"""
387
  if not market_state['ready'] or not market_state['ohlc_history']:
388
  return {"error": "Initializing..."}
389
 
390
- # 1. Calculate Indicators
391
  df = calculate_indicators(market_state['ohlc_history'])
392
- if df is None or len(df) < 60:
393
  return {"error": "Not enough data"}
394
 
395
- # 2. Train Model Periodically
396
- current_time = time.time()
397
- should_train = (
398
- market_state['models'] is None or
399
- len(market_state['models']) == 0 or
400
- (current_time - market_state['last_training_time'] > TRAIN_INTERVAL)
401
- )
402
-
403
- if should_train:
404
  try:
405
- models, scaler = train_model(df)
406
- if models:
407
- market_state['models'] = models
408
- market_state['scaler'] = scaler
409
- market_state['last_training_time'] = current_time
410
  except Exception as e:
411
  logging.error(f"Training failed: {e}")
412
- import traceback
413
- traceback.print_exc()
414
 
415
- # 3. Generate Predictions
416
  predictions = []
417
  try:
418
- predictions = get_prediction(df, market_state['models'], market_state['scaler'])
419
  except Exception as e:
420
  logging.error(f"Prediction failed: {e}")
421
 
422
- # 4. Prepare Display Data
423
  df_clean = df.replace([np.inf, -np.inf], np.nan)
424
  df_clean = df_clean.astype(object).where(pd.notnull(df_clean), None)
425
 
426
- # Calculate stats
427
  last_close = float(df['close'].iloc[-1]) if len(df) > 0 else 0
428
- first_close = float(df['close'].iloc[0]) if len(df) > 0 else last_close
429
  price_change = ((last_close - first_close) / first_close * 100) if first_close > 0 else 0
430
 
431
  market_state['last_price'] = last_close
432
  market_state['price_change'] = price_change
433
 
434
- # Only send last 500 candles to client
435
  display_data = df_clean.tail(500).to_dict('records')
436
-
437
- # Extract last row stats safely
438
- last_row = df.iloc[-1]
439
-
440
- def safe_get(series, key, default=0):
441
- try:
442
- val = series[key] if key in series.index else default
443
- return float(val) if pd.notna(val) and np.isfinite(val) else default
444
- except:
445
- return default
446
 
447
  return {
448
  "data": display_data,
@@ -450,17 +214,13 @@ def process_market_data():
450
  "stats": {
451
  "price": last_close,
452
  "change": round(price_change, 2),
453
- "rsi": round(safe_get(last_row, 'rsi'), 1),
454
- "macd": round(safe_get(last_row, 'macd'), 2),
455
- "atr": round(safe_get(last_row, 'atr'), 2),
456
- "volume": round(safe_get(last_row, 'volume'), 2),
457
- "candles": len(market_state['ohlc_history']),
458
- "model_ready": len(market_state.get('models', {})) > 0
459
  }
460
  }
461
 
462
-
463
- # --- FRONTEND HTML ---
464
  HTML_PAGE = """
465
  <!DOCTYPE html>
466
  <html lang="en">
@@ -472,7 +232,6 @@ HTML_PAGE = """
472
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
473
  <style>
474
  * { margin: 0; padding: 0; box-sizing: border-box; }
475
-
476
  body {
477
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
478
  background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 100%);
@@ -482,7 +241,6 @@ HTML_PAGE = """
482
  flex-direction: column;
483
  overflow: hidden;
484
  }
485
-
486
  .header {
487
  background: rgba(15, 15, 25, 0.95);
488
  backdrop-filter: blur(20px);
@@ -493,22 +251,14 @@ HTML_PAGE = """
493
  justify-content: space-between;
494
  z-index: 100;
495
  }
496
-
497
- .logo-section {
498
- display: flex;
499
- align-items: center;
500
- gap: 16px;
501
- }
502
-
503
  .logo {
504
  font-size: 24px;
505
  font-weight: 700;
506
  background: linear-gradient(135deg, #00ff88 0%, #00d4ff 100%);
507
  -webkit-background-clip: text;
508
  -webkit-text-fill-color: transparent;
509
- letter-spacing: -0.5px;
510
  }
511
-
512
  .symbol-badge {
513
  background: rgba(0, 255, 136, 0.1);
514
  border: 1px solid rgba(0, 255, 136, 0.3);
@@ -518,77 +268,20 @@ HTML_PAGE = """
518
  font-weight: 600;
519
  color: #00ff88;
520
  }
521
-
522
- .model-badge {
523
- background: rgba(191, 90, 242, 0.1);
524
- border: 1px solid rgba(191, 90, 242, 0.3);
525
- padding: 4px 10px;
526
- border-radius: 12px;
527
- font-size: 11px;
528
- color: #bf5af2;
529
- }
530
-
531
- .model-badge.ready {
532
- background: rgba(0, 255, 136, 0.1);
533
- border-color: rgba(0, 255, 136, 0.3);
534
- color: #00ff88;
535
- }
536
-
537
- .stats-row {
538
- display: flex;
539
- gap: 24px;
540
- align-items: center;
541
- }
542
-
543
- .stat-item {
544
- display: flex;
545
- flex-direction: column;
546
- align-items: flex-end;
547
- }
548
-
549
- .stat-label {
550
- font-size: 10px;
551
- color: #666;
552
- text-transform: uppercase;
553
- letter-spacing: 0.5px;
554
- }
555
-
556
- .stat-value {
557
- font-size: 15px;
558
- font-weight: 600;
559
- font-variant-numeric: tabular-nums;
560
- }
561
-
562
  .stat-value.positive { color: #00ff88; }
563
  .stat-value.negative { color: #ff4757; }
564
  .stat-value.neutral { color: #ffd700; }
565
-
566
- .status-indicator {
567
- display: flex;
568
- align-items: center;
569
- gap: 8px;
570
- font-size: 12px;
571
- color: #888;
572
- }
573
-
574
- .status-dot {
575
- width: 8px;
576
- height: 8px;
577
- border-radius: 50%;
578
- background: #00ff88;
579
- animation: pulse 2s infinite;
580
- }
581
-
582
- .status-dot.disconnected {
583
- background: #ff4757;
584
- animation: none;
585
- }
586
-
587
  @keyframes pulse {
588
  0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(0, 255, 136, 0.4); }
589
  50% { opacity: 0.8; box-shadow: 0 0 0 8px rgba(0, 255, 136, 0); }
590
  }
591
-
592
  .indicator-panel {
593
  background: rgba(15, 15, 25, 0.8);
594
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
@@ -597,123 +290,42 @@ HTML_PAGE = """
597
  gap: 32px;
598
  overflow-x: auto;
599
  }
600
-
601
- .indicator-group {
602
- display: flex;
603
- align-items: center;
604
- gap: 12px;
605
- }
606
-
607
- .indicator-label {
608
- font-size: 11px;
609
- color: #666;
610
- text-transform: uppercase;
611
- }
612
-
613
- .indicator-value {
614
- font-size: 13px;
615
- font-weight: 500;
616
- font-variant-numeric: tabular-nums;
617
- }
618
-
619
  .charts-container {
620
  flex: 1;
621
  display: flex;
622
  flex-direction: column;
623
  position: relative;
624
  }
625
-
626
- .chart-wrapper {
627
- position: relative;
628
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
629
- }
630
-
631
  #main-chart { flex: 5; }
632
  #volume-chart { flex: 1; min-height: 60px; }
633
  #osc-chart { flex: 1.5; min-height: 80px; }
634
-
635
  .chart-label {
636
- position: absolute;
637
- top: 12px;
638
- left: 16px;
639
- z-index: 10;
640
- display: flex;
641
- gap: 16px;
642
- font-size: 11px;
643
- pointer-events: none;
644
- }
645
-
646
- .chart-label span {
647
- display: flex;
648
- align-items: center;
649
- gap: 6px;
650
  }
651
-
652
- .chart-label .dot {
653
- width: 8px;
654
- height: 8px;
655
- border-radius: 50%;
656
- }
657
-
658
  .loading-overlay {
659
- position: absolute;
660
- top: 0;
661
- left: 0;
662
- right: 0;
663
- bottom: 0;
664
  background: rgba(10, 10, 15, 0.95);
665
- display: flex;
666
- flex-direction: column;
667
- align-items: center;
668
- justify-content: center;
669
- z-index: 1000;
670
- transition: opacity 0.5s ease;
671
- }
672
-
673
- .loading-overlay.hidden {
674
- opacity: 0;
675
- pointer-events: none;
676
  }
677
-
678
  .loader {
679
- width: 50px;
680
- height: 50px;
681
- border: 3px solid rgba(0, 255, 136, 0.1);
682
- border-top-color: #00ff88;
683
- border-radius: 50%;
684
- animation: spin 1s linear infinite;
685
- }
686
-
687
- @keyframes spin {
688
- to { transform: rotate(360deg); }
689
  }
690
-
691
- .loading-text {
692
- margin-top: 20px;
693
- font-size: 14px;
694
- color: #666;
695
- }
696
-
697
  .prediction-badge {
698
- position: absolute;
699
- top: 12px;
700
- right: 16px;
701
- background: rgba(191, 90, 242, 0.15);
702
- border: 1px solid rgba(191, 90, 242, 0.3);
703
- padding: 4px 10px;
704
- border-radius: 12px;
705
- font-size: 10px;
706
- color: #bf5af2;
707
- z-index: 10;
708
- }
709
-
710
- .candle-count {
711
- position: absolute;
712
- bottom: 12px;
713
- right: 16px;
714
- font-size: 10px;
715
- color: #444;
716
- z-index: 10;
717
  }
718
  </style>
719
  </head>
@@ -722,9 +334,7 @@ HTML_PAGE = """
722
  <div class="logo-section">
723
  <div class="logo">QuantAI</div>
724
  <div class="symbol-badge">BTC/USD</div>
725
- <div id="model-status" class="model-badge">Model: Training...</div>
726
  </div>
727
-
728
  <div class="stats-row">
729
  <div class="stat-item">
730
  <span class="stat-label">Price</span>
@@ -743,58 +353,35 @@ HTML_PAGE = """
743
  <span id="atr" class="stat-value">--</span>
744
  </div>
745
  </div>
746
-
747
  <div class="status-indicator">
748
  <div id="status-dot" class="status-dot"></div>
749
  <span id="status-text">Connecting...</span>
750
  </div>
751
  </div>
752
-
753
  <div class="indicator-panel">
754
- <div class="indicator-group">
755
- <span class="indicator-label">EMA 20</span>
756
- <span id="ema-val" class="indicator-value" style="color: #2962FF">--</span>
757
- </div>
758
- <div class="indicator-group">
759
- <span class="indicator-label">BB Upper</span>
760
- <span id="bb-upper" class="indicator-value" style="color: #26a69a">--</span>
761
- </div>
762
- <div class="indicator-group">
763
- <span class="indicator-label">BB Lower</span>
764
- <span id="bb-lower" class="indicator-value" style="color: #ef5350">--</span>
765
- </div>
766
- <div class="indicator-group">
767
- <span class="indicator-label">MACD</span>
768
- <span id="macd-val" class="indicator-value">--</span>
769
- </div>
770
- <div class="indicator-group">
771
- <span class="indicator-label">Volume</span>
772
- <span id="vol-val" class="indicator-value" style="color: #888">--</span>
773
- </div>
774
  </div>
775
-
776
  <div class="charts-container">
777
  <div class="loading-overlay" id="loading">
778
  <div class="loader"></div>
779
  <div class="loading-text">Loading market data...</div>
780
  </div>
781
-
782
  <div id="main-chart" class="chart-wrapper">
783
  <div class="chart-label">
784
  <span><div class="dot" style="background: #00ff88"></div>Price</span>
785
  <span><div class="dot" style="background: #2962FF"></div>EMA 20</span>
786
  <span><div class="dot" style="background: #26a69a; opacity: 0.5"></div>Bollinger</span>
 
787
  </div>
788
  <div class="prediction-badge">AI Forecast: 100 candles</div>
789
- <div id="candle-count" class="candle-count">Candles: --</div>
790
  </div>
791
-
792
  <div id="volume-chart" class="chart-wrapper">
793
- <div class="chart-label">
794
- <span><div class="dot" style="background: #5c6bc0"></div>Volume</span>
795
- </div>
796
  </div>
797
-
798
  <div id="osc-chart" class="chart-wrapper">
799
  <div class="chart-label">
800
  <span><div class="dot" style="background: #9C27B0"></div>RSI</span>
@@ -802,7 +389,6 @@ HTML_PAGE = """
802
  </div>
803
  </div>
804
  </div>
805
-
806
  <script>
807
  document.addEventListener('DOMContentLoaded', () => {
808
  const mainEl = document.getElementById('main-chart');
@@ -811,32 +397,14 @@ document.addEventListener('DOMContentLoaded', () => {
811
  const loading = document.getElementById('loading');
812
 
813
  const chartOptions = {
814
- layout: {
815
- background: { type: 'solid', color: 'transparent' },
816
- textColor: '#666'
817
- },
818
- grid: {
819
- vertLines: { color: 'rgba(255,255,255,0.03)' },
820
- horzLines: { color: 'rgba(255,255,255,0.03)' }
821
- },
822
- timeScale: {
823
- timeVisible: true,
824
- secondsVisible: false,
825
- borderColor: 'rgba(255,255,255,0.1)'
826
- },
827
- rightPriceScale: {
828
- borderColor: 'rgba(255,255,255,0.1)'
829
- },
830
  crosshair: {
831
  mode: LightweightCharts.CrosshairMode.Normal,
832
- vertLine: {
833
- color: 'rgba(255,255,255,0.2)',
834
- labelBackgroundColor: '#1a1a2e'
835
- },
836
- horzLine: {
837
- color: 'rgba(255,255,255,0.2)',
838
- labelBackgroundColor: '#1a1a2e'
839
- }
840
  }
841
  };
842
 
@@ -845,117 +413,51 @@ document.addEventListener('DOMContentLoaded', () => {
845
  const oscChart = LightweightCharts.createChart(oscEl, chartOptions);
846
 
847
  const candles = mainChart.addCandlestickSeries({
848
- upColor: '#00ff88',
849
- downColor: '#ff4757',
850
- borderUpColor: '#00ff88',
851
- borderDownColor: '#ff4757',
852
- wickUpColor: '#00ff88',
853
- wickDownColor: '#ff4757'
854
  });
855
 
856
- const ema = mainChart.addLineSeries({
857
- color: '#2962FF',
858
- lineWidth: 2,
859
- crosshairMarkerVisible: false
860
- });
861
-
862
- const bbUpper = mainChart.addLineSeries({
863
- color: 'rgba(38, 166, 154, 0.4)',
864
- lineWidth: 1,
865
- crosshairMarkerVisible: false
866
- });
867
-
868
- const bbLower = mainChart.addLineSeries({
869
- color: 'rgba(239, 83, 80, 0.4)',
870
- lineWidth: 1,
871
- crosshairMarkerVisible: false
872
- });
873
 
874
  const predLine = mainChart.addLineSeries({
875
- color: '#bf5af2',
876
- lineWidth: 2,
877
- lineStyle: LightweightCharts.LineStyle.Dashed,
878
- crosshairMarkerVisible: false,
879
- title: 'Forecast'
880
  });
881
 
882
- // Prediction confidence band (optional visual)
883
  const predUpper = mainChart.addLineSeries({
884
- color: 'rgba(191, 90, 242, 0.15)',
885
- lineWidth: 1,
886
- lineStyle: LightweightCharts.LineStyle.Dotted,
887
  crosshairMarkerVisible: false
888
  });
889
 
890
  const predLower = mainChart.addLineSeries({
891
- color: 'rgba(191, 90, 242, 0.15)',
892
- lineWidth: 1,
893
- lineStyle: LightweightCharts.LineStyle.Dotted,
894
  crosshairMarkerVisible: false
895
  });
896
 
897
- const volumeSeries = volChart.addHistogramSeries({
898
- priceFormat: { type: 'volume' },
899
- priceScaleId: ''
900
- });
901
- volChart.priceScale('').applyOptions({
902
- scaleMargins: { top: 0.1, bottom: 0 }
903
- });
904
 
905
- const rsi = oscChart.addLineSeries({
906
- color: '#9C27B0',
907
- lineWidth: 2,
908
- priceScaleId: 'rsi'
909
- });
910
-
911
- // RSI overbought/oversold lines
912
- const rsiUpper = oscChart.addLineSeries({
913
- color: 'rgba(239, 83, 80, 0.3)',
914
- lineWidth: 1,
915
- lineStyle: LightweightCharts.LineStyle.Dashed,
916
- priceScaleId: 'rsi'
917
- });
918
-
919
- const rsiLower = oscChart.addLineSeries({
920
- color: 'rgba(38, 166, 154, 0.3)',
921
- lineWidth: 1,
922
- lineStyle: LightweightCharts.LineStyle.Dashed,
923
- priceScaleId: 'rsi'
924
- });
925
-
926
- oscChart.priceScale('rsi').applyOptions({
927
- scaleMargins: { top: 0.1, bottom: 0.1 }
928
- });
929
 
930
- const macdHist = oscChart.addHistogramSeries({
931
- priceScaleId: 'macd'
932
- });
933
- oscChart.priceScale('macd').applyOptions({
934
- scaleMargins: { top: 0.6, bottom: 0 }
935
- });
936
 
937
  function resizeCharts() {
938
- const mainH = mainEl.clientHeight;
939
- const volH = volEl.clientHeight;
940
- const oscH = oscEl.clientHeight;
941
- const w = mainEl.clientWidth;
942
-
943
- mainChart.applyOptions({ width: w, height: mainH });
944
- volChart.applyOptions({ width: w, height: volH });
945
- oscChart.applyOptions({ width: w, height: oscH });
946
  }
947
-
948
  new ResizeObserver(resizeCharts).observe(document.body);
949
  setTimeout(resizeCharts, 100);
950
 
951
  function syncTimeScales(charts) {
952
  charts.forEach((chart, i) => {
953
  chart.timeScale().subscribeVisibleLogicalRangeChange(range => {
954
- if (range) {
955
- charts.forEach((c, j) => {
956
- if (i !== j) c.timeScale().setVisibleLogicalRange(range);
957
- });
958
- }
959
  });
960
  });
961
  }
@@ -963,45 +465,26 @@ document.addEventListener('DOMContentLoaded', () => {
963
 
964
  function updateStats(stats, lastData) {
965
  if (stats) {
966
- document.getElementById('price').textContent = '$' + stats.price.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2});
967
-
968
  const changeEl = document.getElementById('change');
969
  changeEl.textContent = (stats.change >= 0 ? '+' : '') + stats.change + '%';
970
  changeEl.className = 'stat-value ' + (stats.change > 0 ? 'positive' : stats.change < 0 ? 'negative' : 'neutral');
971
-
972
  const rsiVal = stats.rsi;
973
  const rsiEl = document.getElementById('rsi');
974
  rsiEl.textContent = rsiVal;
975
  rsiEl.className = 'stat-value ' + (rsiVal > 70 ? 'negative' : rsiVal < 30 ? 'positive' : 'neutral');
976
-
977
  document.getElementById('atr').textContent = stats.atr;
978
-
979
- // Update model status
980
- const modelBadge = document.getElementById('model-status');
981
- if (stats.model_ready) {
982
- modelBadge.textContent = 'Model: Active';
983
- modelBadge.className = 'model-badge ready';
984
- } else {
985
- modelBadge.textContent = 'Model: Training...';
986
- modelBadge.className = 'model-badge';
987
- }
988
-
989
- // Update candle count
990
- document.getElementById('candle-count').textContent = 'Candles: ' + (stats.candles || '--');
991
  }
992
-
993
  if (lastData) {
994
  document.getElementById('ema-val').textContent = lastData.ema20 ? lastData.ema20.toFixed(2) : '--';
995
  document.getElementById('bb-upper').textContent = lastData.bb_upper ? lastData.bb_upper.toFixed(2) : '--';
996
  document.getElementById('bb-lower').textContent = lastData.bb_lower ? lastData.bb_lower.toFixed(2) : '--';
997
-
998
  const macdVal = lastData.macd;
999
  const macdEl = document.getElementById('macd-val');
1000
  if (macdVal !== null && macdVal !== undefined) {
1001
  macdEl.textContent = macdVal.toFixed(2);
1002
  macdEl.style.color = macdVal >= 0 ? '#26a69a' : '#ef5350';
1003
  }
1004
-
1005
  document.getElementById('vol-val').textContent = lastData.volume ? lastData.volume.toFixed(2) : '--';
1006
  }
1007
  }
@@ -1009,126 +492,59 @@ document.addEventListener('DOMContentLoaded', () => {
1009
  function setStatus(connected) {
1010
  const dot = document.getElementById('status-dot');
1011
  const text = document.getElementById('status-text');
1012
- if (connected) {
1013
- dot.className = 'status-dot';
1014
- text.textContent = 'Live';
1015
- } else {
1016
- dot.className = 'status-dot disconnected';
1017
- text.textContent = 'Reconnecting...';
1018
- }
1019
  }
1020
 
1021
  let hasData = false;
1022
-
1023
  function connect() {
1024
  const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
1025
  const ws = new WebSocket(protocol + '://' + location.host + '/ws');
1026
-
1027
  ws.onopen = () => setStatus(true);
1028
-
1029
  ws.onmessage = (e) => {
1030
  try {
1031
  const payload = JSON.parse(e.data);
1032
  if (!payload.data || payload.data.length === 0) return;
1033
-
1034
  const d = payload.data;
1035
-
1036
- const safeMap = (arr, key) => arr
1037
- .filter(x => x && x.time && x[key] !== null && x[key] !== undefined && !isNaN(x[key]))
1038
- .map(x => ({ time: x.time, value: x[key] }));
1039
-
1040
- const candleData = d
1041
- .filter(x => x && x.time && x.open && x.high && x.low && x.close)
1042
- .map(x => ({
1043
- time: x.time,
1044
- open: x.open,
1045
- high: x.high,
1046
- low: x.low,
1047
- close: x.close
1048
- }));
1049
-
1050
  if (candleData.length > 0) {
1051
  candles.setData(candleData);
 
 
 
 
 
 
 
 
 
 
1052
 
1053
- const emaData = safeMap(d, 'ema20');
1054
- if (emaData.length > 0) ema.setData(emaData);
1055
-
1056
- const bbUpperData = safeMap(d, 'bb_upper');
1057
- if (bbUpperData.length > 0) bbUpper.setData(bbUpperData);
1058
-
1059
- const bbLowerData = safeMap(d, 'bb_lower');
1060
- if (bbLowerData.length > 0) bbLower.setData(bbLowerData);
1061
-
1062
- const volData = d
1063
- .filter(x => x && x.time && x.volume !== null && x.volume !== undefined)
1064
- .map(x => ({
1065
- time: x.time,
1066
- value: x.volume,
1067
- color: x.close >= x.open ? 'rgba(0, 255, 136, 0.5)' : 'rgba(255, 71, 87, 0.5)'
1068
- }));
1069
- if (volData.length > 0) volumeSeries.setData(volData);
1070
-
1071
- const rsiData = safeMap(d, 'rsi');
1072
- if (rsiData.length > 0) {
1073
- rsi.setData(rsiData);
1074
- // Set RSI reference lines
1075
- const times = rsiData.map(x => x.time);
1076
- rsiUpper.setData(times.map(t => ({time: t, value: 70})));
1077
- rsiLower.setData(times.map(t => ({time: t, value: 30})));
1078
- }
1079
-
1080
- const macdData = d
1081
- .filter(x => x && x.time && x.macd_hist !== null && x.macd_hist !== undefined && !isNaN(x.macd_hist))
1082
- .map(x => ({
1083
- time: x.time,
1084
- value: x.macd_hist,
1085
- color: x.macd_hist >= 0 ? '#26a69a' : '#ef5350'
1086
- }));
1087
- if (macdData.length > 0) macdHist.setData(macdData);
1088
-
1089
- // Handle predictions with confidence bands
1090
  if (payload.prediction && payload.prediction.length > 0) {
1091
  const lastCandle = candleData[candleData.length - 1];
1092
- const predData = [
1093
- { time: lastCandle.time, value: lastCandle.close },
1094
- ...payload.prediction.filter(p => p && p.time && p.value !== null && !isNaN(p.value))
1095
- ];
1096
- predLine.setData(predData);
1097
 
1098
- // Add confidence bands (±1% expanding over time)
1099
- const upperBand = predData.map((p, i) => ({
1100
- time: p.time,
1101
- value: p.value * (1 + 0.002 * Math.sqrt(i))
1102
- }));
1103
- const lowerBand = predData.map((p, i) => ({
1104
- time: p.time,
1105
- value: p.value * (1 - 0.002 * Math.sqrt(i))
1106
- }));
1107
- predUpper.setData(upperBand);
1108
- predLower.setData(lowerBand);
1109
  }
1110
-
1111
  updateStats(payload.stats, d[d.length - 1]);
1112
-
1113
  if (!hasData) {
1114
  hasData = true;
1115
  loading.classList.add('hidden');
1116
  mainChart.timeScale().fitContent();
1117
  }
1118
  }
1119
- } catch (err) {
1120
- console.error("Chart error:", err);
1121
- }
1122
  };
1123
-
1124
- ws.onclose = () => {
1125
- setStatus(false);
1126
- setTimeout(connect, 2000);
1127
- };
1128
-
1129
  ws.onerror = () => ws.close();
1130
  }
1131
-
1132
  connect();
1133
  });
1134
  </script>
@@ -1136,9 +552,7 @@ document.addEventListener('DOMContentLoaded', () => {
1136
  </html>
1137
  """
1138
 
1139
-
1140
  async def fetch_initial_data():
1141
- """Fetch initial OHLC data from Kraken"""
1142
  try:
1143
  async with aiohttp.ClientSession() as session:
1144
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
@@ -1167,9 +581,7 @@ async def fetch_initial_data():
1167
  logging.error(f"Initial data fetch error: {e}")
1168
  return False
1169
 
1170
-
1171
  async def kraken_rest_worker():
1172
- """Background worker to fetch and update OHLC data"""
1173
  await fetch_initial_data()
1174
 
1175
  while True:
@@ -1192,7 +604,7 @@ async def kraken_rest_worker():
1192
  'close': float(c[4]),
1193
  'volume': float(c[6])
1194
  }
1195
- for c in raw[-20:] # Get last 20 candles for merging
1196
  ]
1197
 
1198
  if market_state['ohlc_history']:
@@ -1218,9 +630,7 @@ async def kraken_rest_worker():
1218
 
1219
  await asyncio.sleep(5)
1220
 
1221
-
1222
  async def broadcast_worker():
1223
- """Broadcast market data to connected clients"""
1224
  while True:
1225
  if connected_clients and market_state['ready']:
1226
  payload = process_market_data()
@@ -1235,42 +645,24 @@ async def broadcast_worker():
1235
  connected_clients.difference_update(disconnected)
1236
  await asyncio.sleep(BROADCAST_RATE)
1237
 
1238
-
1239
  async def websocket_handler(request):
1240
- """Handle WebSocket connections"""
1241
  ws = web.WebSocketResponse()
1242
  await ws.prepare(request)
1243
  connected_clients.add(ws)
1244
- logging.info(f"Client connected. Total: {len(connected_clients)}")
1245
  try:
1246
  async for msg in ws:
1247
  pass
1248
  finally:
1249
  connected_clients.discard(ws)
1250
- logging.info(f"Client disconnected. Total: {len(connected_clients)}")
1251
  return ws
1252
 
1253
-
1254
  async def handle_index(request):
1255
  return web.Response(text=HTML_PAGE, content_type='text/html')
1256
 
1257
-
1258
- async def handle_health(request):
1259
- return web.json_response({
1260
- "status": "ok",
1261
- "ready": market_state['ready'],
1262
- "candles": len(market_state['ohlc_history']),
1263
- "clients": len(connected_clients),
1264
- "model_ready": len(market_state.get('models', {})) > 0,
1265
- "training_metrics": market_state.get('training_metrics', {})
1266
- })
1267
-
1268
-
1269
  async def main():
1270
  app = web.Application()
1271
  app.router.add_get('/', handle_index)
1272
  app.router.add_get('/ws', websocket_handler)
1273
- app.router.add_get('/health', handle_health)
1274
 
1275
  asyncio.create_task(kraken_rest_worker())
1276
  asyncio.create_task(broadcast_worker())
@@ -1284,9 +676,8 @@ async def main():
1284
 
1285
  await asyncio.Event().wait()
1286
 
1287
-
1288
  if __name__ == "__main__":
1289
  try:
1290
  asyncio.run(main())
1291
  except KeyboardInterrupt:
1292
- logging.info("Shutting down...")
 
2
  import json
3
  import logging
4
  import time
5
+ import math
6
  import aiohttp
7
  import pandas as pd
8
  import numpy as np
9
  from aiohttp import web
10
+ from sklearn.ensemble import RandomForestRegressor
11
+ from sklearn.metrics import mean_squared_error
 
 
12
 
 
13
  SYMBOL_KRAKEN = "BTC/USD"
14
  PORT = 7860
15
  BROADCAST_RATE = 1.0
16
  PREDICTION_HORIZON = 100
17
  MAX_HISTORY = 5000
18
  TRAIN_INTERVAL = 300
 
19
 
20
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  market_state = {
23
  "ohlc_history": [],
24
  "ready": False,
25
+ "model": None,
26
+ "model_residuals": None,
27
  "last_training_time": 0,
28
  "last_price": 0,
29
+ "price_change": 0
 
30
  }
31
 
32
  connected_clients = set()
33
 
 
 
 
 
 
 
 
 
 
34
  def calculate_indicators(candles):
35
+ if len(candles) < 100:
 
36
  return None
37
 
38
+ df = pd.DataFrame(candles)
39
  cols = ['open', 'high', 'low', 'close', 'volume']
40
  for c in cols:
41
+ df[c] = df[c].astype(float)
 
 
 
 
42
 
43
+ df['ema20'] = df['close'].ewm(span=20, adjust=False).mean()
44
+ df['ema50'] = df['close'].ewm(span=50, adjust=False).mean()
 
 
45
 
46
+ df['std'] = df['close'].rolling(window=20).std()
47
+ df['bb_upper'] = df['ema20'] + (df['std'] * 2)
48
+ df['bb_lower'] = df['ema20'] - (df['std'] * 2)
 
 
 
 
 
 
49
 
50
+ delta = df['close'].diff()
51
+ gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
 
52
  loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
53
+ rs = gain / loss
54
  df['rsi'] = 100 - (100 / (1 + rs))
 
 
 
 
 
55
 
56
+ k = df['close'].ewm(span=12, adjust=False).mean()
57
+ d = df['close'].ewm(span=26, adjust=False).mean()
58
+ df['macd'] = k - d
 
59
  df['macd_signal'] = df['macd'].ewm(span=9, adjust=False).mean()
60
  df['macd_hist'] = df['macd'] - df['macd_signal']
 
 
 
 
 
 
61
 
62
+ df['tr0'] = abs(df['high'] - df['low'])
63
+ df['tr1'] = abs(df['high'] - df['close'].shift())
64
+ df['tr2'] = abs(df['low'] - df['close'].shift())
65
+ df['tr'] = df[['tr0', 'tr1', 'tr2']].max(axis=1)
 
66
  df['atr'] = df['tr'].rolling(window=14).mean()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
+ df['dist_ema20'] = (df['close'] - df['ema20']) / df['ema20']
69
+ df['dist_ema50'] = (df['close'] - df['ema50']) / df['ema50']
70
+ df['bb_width'] = (df['bb_upper'] - df['bb_lower']) / df['ema20']
71
+ df['bb_pos'] = (df['close'] - df['bb_lower']) / (df['bb_upper'] - df['bb_lower'])
72
+ df['vol_change'] = df['volume'].pct_change()
73
+ df['log_ret'] = np.log(df['close'] / df['close'].shift(1))
 
 
 
 
 
 
 
 
 
74
 
75
+ for lag in [1, 2, 3]:
76
+ df[f'rsi_lag{lag}'] = df['rsi'].shift(lag)
77
+ df[f'macd_hist_lag{lag}'] = df['macd_hist'].shift(lag)
78
+ df[f'log_ret_lag{lag}'] = df['log_ret'].shift(lag)
79
+ df[f'vol_change_lag{lag}'] = df['vol_change'].shift(lag)
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  return df
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  def train_model(df):
84
+ logging.info(f"Training ML Model on {len(df)} candles...")
85
+
86
+ feature_cols = [
87
+ 'rsi', 'macd_hist', 'atr',
88
+ 'dist_ema20', 'dist_ema50',
89
+ 'bb_width', 'bb_pos',
90
+ 'vol_change', 'log_ret',
91
+ 'rsi_lag1', 'rsi_lag2', 'rsi_lag3',
92
+ 'macd_hist_lag1', 'macd_hist_lag2', 'macd_hist_lag3',
93
+ 'log_ret_lag1', 'log_ret_lag2', 'log_ret_lag3',
94
+ 'vol_change_lag1', 'vol_change_lag2', 'vol_change_lag3'
95
+ ]
96
+
97
+ data = df.dropna().copy()
98
+ targets = []
99
+
100
+ for i in range(1, PREDICTION_HORIZON + 1):
101
+ col_name = f'target_return_{i}'
102
+ data[col_name] = (data['close'].shift(-i) - data['close']) / data['close']
103
+ targets.append(col_name)
104
+
105
+ data = data.dropna()
106
+
107
+ if len(data) < 200:
108
  return None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
+ X = data[feature_cols].values
111
+ y = data[targets].values
112
+
113
+ model = RandomForestRegressor(
114
+ n_estimators=150,
115
+ max_depth=20,
116
+ min_samples_split=4,
117
+ min_samples_leaf=2,
118
+ max_features='sqrt',
119
+ n_jobs=-1,
120
+ random_state=42
121
+ )
122
+ model.fit(X, y)
123
 
124
+ predictions = model.predict(X)
125
+ residuals = y - predictions
126
+ residual_std = np.std(residuals, axis=0)
127
 
128
+ return model, residual_std
 
129
 
130
+ def get_prediction(df, model, residual_std):
131
+ if model is None or residual_std is None:
 
132
  return []
133
 
134
+ feature_cols = [
135
+ 'rsi', 'macd_hist', 'atr',
136
+ 'dist_ema20', 'dist_ema50',
137
+ 'bb_width', 'bb_pos',
138
+ 'vol_change', 'log_ret',
139
+ 'rsi_lag1', 'rsi_lag2', 'rsi_lag3',
140
+ 'macd_hist_lag1', 'macd_hist_lag2', 'macd_hist_lag3',
141
+ 'log_ret_lag1', 'log_ret_lag2', 'log_ret_lag3',
142
+ 'vol_change_lag1', 'vol_change_lag2', 'vol_change_lag3'
143
+ ]
144
 
145
+ last_row = df.iloc[[-1]][feature_cols]
 
 
 
 
146
 
147
+ if last_row.isnull().values.any():
 
 
148
  return []
149
+
150
+ predicted_returns = model.predict(last_row.values)[0]
151
 
152
+ current_price = df.iloc[-1]['close']
153
+ current_time = int(df.iloc[-1]['time'])
154
+
155
+ pred_data = []
156
+ confidence_multiplier = 1.96
157
+
158
+ for i, pct_change in enumerate(predicted_returns):
159
+ future_price = current_price * (1 + pct_change)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
+ sigma = residual_std[i]
162
+ upper_bound = future_price * (1 + (sigma * confidence_multiplier))
163
+ lower_bound = future_price * (1 - (sigma * confidence_multiplier))
164
 
165
+ pred_data.append({
166
+ "time": current_time + ((i + 1) * 60),
167
+ "value": float(future_price),
168
+ "upper": float(upper_bound),
169
+ "lower": float(lower_bound)
170
+ })
171
+
172
+ return pred_data
173
 
174
  def process_market_data():
 
175
  if not market_state['ready'] or not market_state['ohlc_history']:
176
  return {"error": "Initializing..."}
177
 
 
178
  df = calculate_indicators(market_state['ohlc_history'])
179
+ if df is None or len(df) < 100:
180
  return {"error": "Not enough data"}
181
 
182
+ if market_state['model'] is None or (time.time() - market_state['last_training_time'] > TRAIN_INTERVAL):
 
 
 
 
 
 
 
 
183
  try:
184
+ model, res_std = train_model(df)
185
+ if model is not None:
186
+ market_state['model'] = model
187
+ market_state['model_residuals'] = res_std
188
+ market_state['last_training_time'] = time.time()
189
  except Exception as e:
190
  logging.error(f"Training failed: {e}")
 
 
191
 
 
192
  predictions = []
193
  try:
194
+ predictions = get_prediction(df, market_state['model'], market_state['model_residuals'])
195
  except Exception as e:
196
  logging.error(f"Prediction failed: {e}")
197
 
 
198
  df_clean = df.replace([np.inf, -np.inf], np.nan)
199
  df_clean = df_clean.astype(object).where(pd.notnull(df_clean), None)
200
 
 
201
  last_close = float(df['close'].iloc[-1]) if len(df) > 0 else 0
202
+ first_close = float(df['close'].iloc[0]) if len(df) > 0 else 0
203
  price_change = ((last_close - first_close) / first_close * 100) if first_close > 0 else 0
204
 
205
  market_state['last_price'] = last_close
206
  market_state['price_change'] = price_change
207
 
 
208
  display_data = df_clean.tail(500).to_dict('records')
209
+ last_row = df.iloc[-1] if len(df) > 0 else {}
 
 
 
 
 
 
 
 
 
210
 
211
  return {
212
  "data": display_data,
 
214
  "stats": {
215
  "price": last_close,
216
  "change": round(price_change, 2),
217
+ "rsi": round(float(last_row.get('rsi', 0)), 1) if pd.notna(last_row.get('rsi')) else 0,
218
+ "macd": round(float(last_row.get('macd', 0)), 2) if pd.notna(last_row.get('macd')) else 0,
219
+ "atr": round(float(last_row.get('atr', 0)), 2) if pd.notna(last_row.get('atr')) else 0,
220
+ "volume": round(float(last_row.get('volume', 0)), 2) if pd.notna(last_row.get('volume')) else 0
 
 
221
  }
222
  }
223
 
 
 
224
  HTML_PAGE = """
225
  <!DOCTYPE html>
226
  <html lang="en">
 
232
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
233
  <style>
234
  * { margin: 0; padding: 0; box-sizing: border-box; }
 
235
  body {
236
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
237
  background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 100%);
 
241
  flex-direction: column;
242
  overflow: hidden;
243
  }
 
244
  .header {
245
  background: rgba(15, 15, 25, 0.95);
246
  backdrop-filter: blur(20px);
 
251
  justify-content: space-between;
252
  z-index: 100;
253
  }
254
+ .logo-section { display: flex; align-items: center; gap: 16px; }
 
 
 
 
 
 
255
  .logo {
256
  font-size: 24px;
257
  font-weight: 700;
258
  background: linear-gradient(135deg, #00ff88 0%, #00d4ff 100%);
259
  -webkit-background-clip: text;
260
  -webkit-text-fill-color: transparent;
 
261
  }
 
262
  .symbol-badge {
263
  background: rgba(0, 255, 136, 0.1);
264
  border: 1px solid rgba(0, 255, 136, 0.3);
 
268
  font-weight: 600;
269
  color: #00ff88;
270
  }
271
+ .stats-row { display: flex; gap: 24px; align-items: center; }
272
+ .stat-item { display: flex; flex-direction: column; align-items: flex-end; }
273
+ .stat-label { font-size: 10px; color: #666; text-transform: uppercase; }
274
+ .stat-value { font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  .stat-value.positive { color: #00ff88; }
276
  .stat-value.negative { color: #ff4757; }
277
  .stat-value.neutral { color: #ffd700; }
278
+ .status-indicator { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #888; }
279
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; background: #00ff88; animation: pulse 2s infinite; }
280
+ .status-dot.disconnected { background: #ff4757; animation: none; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  @keyframes pulse {
282
  0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(0, 255, 136, 0.4); }
283
  50% { opacity: 0.8; box-shadow: 0 0 0 8px rgba(0, 255, 136, 0); }
284
  }
 
285
  .indicator-panel {
286
  background: rgba(15, 15, 25, 0.8);
287
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
 
290
  gap: 32px;
291
  overflow-x: auto;
292
  }
293
+ .indicator-group { display: flex; align-items: center; gap: 12px; }
294
+ .indicator-label { font-size: 11px; color: #666; text-transform: uppercase; }
295
+ .indicator-value { font-size: 13px; font-weight: 500; font-variant-numeric: tabular-nums; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  .charts-container {
297
  flex: 1;
298
  display: flex;
299
  flex-direction: column;
300
  position: relative;
301
  }
302
+ .chart-wrapper { position: relative; border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
 
 
 
 
 
303
  #main-chart { flex: 5; }
304
  #volume-chart { flex: 1; min-height: 60px; }
305
  #osc-chart { flex: 1.5; min-height: 80px; }
 
306
  .chart-label {
307
+ position: absolute; top: 12px; left: 16px; z-index: 10;
308
+ display: flex; gap: 16px; font-size: 11px; pointer-events: none;
 
 
 
 
 
 
 
 
 
 
 
 
309
  }
310
+ .chart-label span { display: flex; align-items: center; gap: 6px; }
311
+ .chart-label .dot { width: 8px; height: 8px; border-radius: 50%; }
 
 
 
 
 
312
  .loading-overlay {
313
+ position: absolute; top: 0; left: 0; right: 0; bottom: 0;
 
 
 
 
314
  background: rgba(10, 10, 15, 0.95);
315
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
316
+ z-index: 1000; transition: opacity 0.5s ease;
 
 
 
 
 
 
 
 
 
317
  }
318
+ .loading-overlay.hidden { opacity: 0; pointer-events: none; }
319
  .loader {
320
+ width: 50px; height: 50px; border: 3px solid rgba(0, 255, 136, 0.1);
321
+ border-top-color: #00ff88; border-radius: 50%; animation: spin 1s linear infinite;
 
 
 
 
 
 
 
 
322
  }
323
+ @keyframes spin { to { transform: rotate(360deg); } }
324
+ .loading-text { margin-top: 20px; font-size: 14px; color: #666; }
 
 
 
 
 
325
  .prediction-badge {
326
+ position: absolute; top: 12px; right: 16px;
327
+ background: rgba(191, 90, 242, 0.15); border: 1px solid rgba(191, 90, 242, 0.3);
328
+ padding: 4px 10px; border-radius: 12px; font-size: 10px; color: #bf5af2; z-index: 10;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  }
330
  </style>
331
  </head>
 
334
  <div class="logo-section">
335
  <div class="logo">QuantAI</div>
336
  <div class="symbol-badge">BTC/USD</div>
 
337
  </div>
 
338
  <div class="stats-row">
339
  <div class="stat-item">
340
  <span class="stat-label">Price</span>
 
353
  <span id="atr" class="stat-value">--</span>
354
  </div>
355
  </div>
 
356
  <div class="status-indicator">
357
  <div id="status-dot" class="status-dot"></div>
358
  <span id="status-text">Connecting...</span>
359
  </div>
360
  </div>
 
361
  <div class="indicator-panel">
362
+ <div class="indicator-group"><span class="indicator-label">EMA 20</span><span id="ema-val" class="indicator-value" style="color: #2962FF">--</span></div>
363
+ <div class="indicator-group"><span class="indicator-label">BB Upper</span><span id="bb-upper" class="indicator-value" style="color: #26a69a">--</span></div>
364
+ <div class="indicator-group"><span class="indicator-label">BB Lower</span><span id="bb-lower" class="indicator-value" style="color: #ef5350">--</span></div>
365
+ <div class="indicator-group"><span class="indicator-label">MACD</span><span id="macd-val" class="indicator-value">--</span></div>
366
+ <div class="indicator-group"><span class="indicator-label">Volume</span><span id="vol-val" class="indicator-value" style="color: #888">--</span></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  </div>
 
368
  <div class="charts-container">
369
  <div class="loading-overlay" id="loading">
370
  <div class="loader"></div>
371
  <div class="loading-text">Loading market data...</div>
372
  </div>
 
373
  <div id="main-chart" class="chart-wrapper">
374
  <div class="chart-label">
375
  <span><div class="dot" style="background: #00ff88"></div>Price</span>
376
  <span><div class="dot" style="background: #2962FF"></div>EMA 20</span>
377
  <span><div class="dot" style="background: #26a69a; opacity: 0.5"></div>Bollinger</span>
378
+ <span><div class="dot" style="background: #bf5af2"></div>AI + 95% Conf</span>
379
  </div>
380
  <div class="prediction-badge">AI Forecast: 100 candles</div>
 
381
  </div>
 
382
  <div id="volume-chart" class="chart-wrapper">
383
+ <div class="chart-label"><span><div class="dot" style="background: #5c6bc0"></div>Volume</span></div>
 
 
384
  </div>
 
385
  <div id="osc-chart" class="chart-wrapper">
386
  <div class="chart-label">
387
  <span><div class="dot" style="background: #9C27B0"></div>RSI</span>
 
389
  </div>
390
  </div>
391
  </div>
 
392
  <script>
393
  document.addEventListener('DOMContentLoaded', () => {
394
  const mainEl = document.getElementById('main-chart');
 
397
  const loading = document.getElementById('loading');
398
 
399
  const chartOptions = {
400
+ layout: { background: { type: 'solid', color: 'transparent' }, textColor: '#666' },
401
+ grid: { vertLines: { color: 'rgba(255,255,255,0.03)' }, horzLines: { color: 'rgba(255,255,255,0.03)' } },
402
+ timeScale: { timeVisible: true, secondsVisible: false, borderColor: 'rgba(255,255,255,0.1)' },
403
+ rightPriceScale: { borderColor: 'rgba(255,255,255,0.1)' },
 
 
 
 
 
 
 
 
 
 
 
 
404
  crosshair: {
405
  mode: LightweightCharts.CrosshairMode.Normal,
406
+ vertLine: { color: 'rgba(255,255,255,0.2)', labelBackgroundColor: '#1a1a2e' },
407
+ horzLine: { color: 'rgba(255,255,255,0.2)', labelBackgroundColor: '#1a1a2e' }
 
 
 
 
 
 
408
  }
409
  };
410
 
 
413
  const oscChart = LightweightCharts.createChart(oscEl, chartOptions);
414
 
415
  const candles = mainChart.addCandlestickSeries({
416
+ upColor: '#00ff88', downColor: '#ff4757',
417
+ borderUpColor: '#00ff88', borderDownColor: '#ff4757',
418
+ wickUpColor: '#00ff88', wickDownColor: '#ff4757'
 
 
 
419
  });
420
 
421
+ const ema = mainChart.addLineSeries({ color: '#2962FF', lineWidth: 2, crosshairMarkerVisible: false });
422
+ const bbUpper = mainChart.addLineSeries({ color: 'rgba(38, 166, 154, 0.4)', lineWidth: 1, crosshairMarkerVisible: false });
423
+ const bbLower = mainChart.addLineSeries({ color: 'rgba(239, 83, 80, 0.4)', lineWidth: 1, crosshairMarkerVisible: false });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
 
425
  const predLine = mainChart.addLineSeries({
426
+ color: '#bf5af2', lineWidth: 2, lineStyle: LightweightCharts.LineStyle.Dashed,
427
+ crosshairMarkerVisible: false, title: 'Forecast'
 
 
 
428
  });
429
 
 
430
  const predUpper = mainChart.addLineSeries({
431
+ color: 'rgba(191, 90, 242, 0.3)', lineWidth: 1, lineStyle: LightweightCharts.LineStyle.Dotted,
 
 
432
  crosshairMarkerVisible: false
433
  });
434
 
435
  const predLower = mainChart.addLineSeries({
436
+ color: 'rgba(191, 90, 242, 0.3)', lineWidth: 1, lineStyle: LightweightCharts.LineStyle.Dotted,
 
 
437
  crosshairMarkerVisible: false
438
  });
439
 
440
+ const volumeSeries = volChart.addHistogramSeries({ priceFormat: { type: 'volume' }, priceScaleId: '' });
441
+ volChart.priceScale('').applyOptions({ scaleMargins: { top: 0.1, bottom: 0 } });
 
 
 
 
 
442
 
443
+ const rsi = oscChart.addLineSeries({ color: '#9C27B0', lineWidth: 2, priceScaleId: 'rsi' });
444
+ oscChart.priceScale('rsi').applyOptions({ scaleMargins: { top: 0.1, bottom: 0.1 } });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
 
446
+ const macdHist = oscChart.addHistogramSeries({ priceScaleId: 'macd' });
447
+ oscChart.priceScale('macd').applyOptions({ scaleMargins: { top: 0.6, bottom: 0 } });
 
 
 
 
448
 
449
  function resizeCharts() {
450
+ mainChart.applyOptions({ width: mainEl.clientWidth, height: mainEl.clientHeight });
451
+ volChart.applyOptions({ width: mainEl.clientWidth, height: volEl.clientHeight });
452
+ oscChart.applyOptions({ width: mainEl.clientWidth, height: oscEl.clientHeight });
 
 
 
 
 
453
  }
 
454
  new ResizeObserver(resizeCharts).observe(document.body);
455
  setTimeout(resizeCharts, 100);
456
 
457
  function syncTimeScales(charts) {
458
  charts.forEach((chart, i) => {
459
  chart.timeScale().subscribeVisibleLogicalRangeChange(range => {
460
+ if (range) charts.forEach((c, j) => { if (i !== j) c.timeScale().setVisibleLogicalRange(range); });
 
 
 
 
461
  });
462
  });
463
  }
 
465
 
466
  function updateStats(stats, lastData) {
467
  if (stats) {
468
+ document.getElementById('price').textContent = '$' + stats.price.toLocaleString('en-US', {minimumFractionDigits: 2});
 
469
  const changeEl = document.getElementById('change');
470
  changeEl.textContent = (stats.change >= 0 ? '+' : '') + stats.change + '%';
471
  changeEl.className = 'stat-value ' + (stats.change > 0 ? 'positive' : stats.change < 0 ? 'negative' : 'neutral');
 
472
  const rsiVal = stats.rsi;
473
  const rsiEl = document.getElementById('rsi');
474
  rsiEl.textContent = rsiVal;
475
  rsiEl.className = 'stat-value ' + (rsiVal > 70 ? 'negative' : rsiVal < 30 ? 'positive' : 'neutral');
 
476
  document.getElementById('atr').textContent = stats.atr;
 
 
 
 
 
 
 
 
 
 
 
 
 
477
  }
 
478
  if (lastData) {
479
  document.getElementById('ema-val').textContent = lastData.ema20 ? lastData.ema20.toFixed(2) : '--';
480
  document.getElementById('bb-upper').textContent = lastData.bb_upper ? lastData.bb_upper.toFixed(2) : '--';
481
  document.getElementById('bb-lower').textContent = lastData.bb_lower ? lastData.bb_lower.toFixed(2) : '--';
 
482
  const macdVal = lastData.macd;
483
  const macdEl = document.getElementById('macd-val');
484
  if (macdVal !== null && macdVal !== undefined) {
485
  macdEl.textContent = macdVal.toFixed(2);
486
  macdEl.style.color = macdVal >= 0 ? '#26a69a' : '#ef5350';
487
  }
 
488
  document.getElementById('vol-val').textContent = lastData.volume ? lastData.volume.toFixed(2) : '--';
489
  }
490
  }
 
492
  function setStatus(connected) {
493
  const dot = document.getElementById('status-dot');
494
  const text = document.getElementById('status-text');
495
+ if (connected) { dot.className = 'status-dot'; text.textContent = 'Live'; }
496
+ else { dot.className = 'status-dot disconnected'; text.textContent = 'Reconnecting...'; }
 
 
 
 
 
497
  }
498
 
499
  let hasData = false;
 
500
  function connect() {
501
  const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
502
  const ws = new WebSocket(protocol + '://' + location.host + '/ws');
 
503
  ws.onopen = () => setStatus(true);
 
504
  ws.onmessage = (e) => {
505
  try {
506
  const payload = JSON.parse(e.data);
507
  if (!payload.data || payload.data.length === 0) return;
 
508
  const d = payload.data;
509
+ const safeMap = (arr, key) => arr.filter(x => x && x.time && x[key] !== null).map(x => ({ time: x.time, value: x[key] }));
510
+ const candleData = d.filter(x => x && x.time && x.open).map(x => ({
511
+ time: x.time, open: x.open, high: x.high, low: x.low, close: x.close
512
+ }));
 
 
 
 
 
 
 
 
 
 
 
513
  if (candleData.length > 0) {
514
  candles.setData(candleData);
515
+ ema.setData(safeMap(d, 'ema20'));
516
+ bbUpper.setData(safeMap(d, 'bb_upper'));
517
+ bbLower.setData(safeMap(d, 'bb_lower'));
518
+ volumeSeries.setData(d.filter(x => x && x.time).map(x => ({
519
+ time: x.time, value: x.volume, color: x.close >= x.open ? 'rgba(0, 255, 136, 0.5)' : 'rgba(255, 71, 87, 0.5)'
520
+ })));
521
+ rsi.setData(safeMap(d, 'rsi'));
522
+ macdHist.setData(d.filter(x => x && x.time).map(x => ({
523
+ time: x.time, value: x.macd_hist, color: x.macd_hist >= 0 ? '#26a69a' : '#ef5350'
524
+ })));
525
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  if (payload.prediction && payload.prediction.length > 0) {
527
  const lastCandle = candleData[candleData.length - 1];
528
+ const predData = [{ time: lastCandle.time, value: lastCandle.close }, ...payload.prediction.map(p => ({ time: p.time, value: p.value }))];
529
+ const upperData = [{ time: lastCandle.time, value: lastCandle.close }, ...payload.prediction.map(p => ({ time: p.time, value: p.upper }))];
530
+ const lowerData = [{ time: lastCandle.time, value: lastCandle.close }, ...payload.prediction.map(p => ({ time: p.time, value: p.lower }))];
 
 
531
 
532
+ predLine.setData(predData);
533
+ predUpper.setData(upperData);
534
+ predLower.setData(lowerData);
 
 
 
 
 
 
 
 
535
  }
 
536
  updateStats(payload.stats, d[d.length - 1]);
 
537
  if (!hasData) {
538
  hasData = true;
539
  loading.classList.add('hidden');
540
  mainChart.timeScale().fitContent();
541
  }
542
  }
543
+ } catch (err) { console.error("Chart error:", err); }
 
 
544
  };
545
+ ws.onclose = () => { setStatus(false); setTimeout(connect, 2000); };
 
 
 
 
 
546
  ws.onerror = () => ws.close();
547
  }
 
548
  connect();
549
  });
550
  </script>
 
552
  </html>
553
  """
554
 
 
555
  async def fetch_initial_data():
 
556
  try:
557
  async with aiohttp.ClientSession() as session:
558
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
 
581
  logging.error(f"Initial data fetch error: {e}")
582
  return False
583
 
 
584
  async def kraken_rest_worker():
 
585
  await fetch_initial_data()
586
 
587
  while True:
 
604
  'close': float(c[4]),
605
  'volume': float(c[6])
606
  }
607
+ for c in raw[-10:]
608
  ]
609
 
610
  if market_state['ohlc_history']:
 
630
 
631
  await asyncio.sleep(5)
632
 
 
633
  async def broadcast_worker():
 
634
  while True:
635
  if connected_clients and market_state['ready']:
636
  payload = process_market_data()
 
645
  connected_clients.difference_update(disconnected)
646
  await asyncio.sleep(BROADCAST_RATE)
647
 
 
648
  async def websocket_handler(request):
 
649
  ws = web.WebSocketResponse()
650
  await ws.prepare(request)
651
  connected_clients.add(ws)
 
652
  try:
653
  async for msg in ws:
654
  pass
655
  finally:
656
  connected_clients.discard(ws)
 
657
  return ws
658
 
 
659
  async def handle_index(request):
660
  return web.Response(text=HTML_PAGE, content_type='text/html')
661
 
 
 
 
 
 
 
 
 
 
 
 
 
662
  async def main():
663
  app = web.Application()
664
  app.router.add_get('/', handle_index)
665
  app.router.add_get('/ws', websocket_handler)
 
666
 
667
  asyncio.create_task(kraken_rest_worker())
668
  asyncio.create_task(broadcast_worker())
 
676
 
677
  await asyncio.Event().wait()
678
 
 
679
  if __name__ == "__main__":
680
  try:
681
  asyncio.run(main())
682
  except KeyboardInterrupt:
683
+ pass