Alvin3y1 commited on
Commit
e1711fa
·
verified ·
1 Parent(s): 042d8f2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +157 -140
app.py CHANGED
@@ -6,19 +6,17 @@ import math
6
  import aiohttp
7
  import pandas as pd
8
  import numpy as np
9
- import tensorflow as tf
10
  from aiohttp import web
11
- from tensorflow.keras import layers, models, callbacks
12
- from sklearn.preprocessing import StandardScaler
13
  from concurrent.futures import ThreadPoolExecutor
14
 
 
15
  SYMBOL_KRAKEN = "BTC/USD"
16
  PORT = 7860
17
  BROADCAST_RATE = 1.0
18
  PREDICTION_HORIZON = 100
19
  MAX_HISTORY = 5000
20
- TRAIN_INTERVAL = 600
21
- LOOKBACK_WINDOW = 60
22
 
23
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
24
 
@@ -26,7 +24,7 @@ market_state = {
26
  "ohlc_history": [],
27
  "ready": False,
28
  "model": None,
29
- "scaler": None,
30
  "last_training_time": 0,
31
  "last_price": 0,
32
  "price_change": 0
@@ -36,7 +34,7 @@ connected_clients = set()
36
  executor = ThreadPoolExecutor(max_workers=1)
37
 
38
  def calculate_indicators(candles):
39
- if len(candles) < LOOKBACK_WINDOW + PREDICTION_HORIZON:
40
  return None
41
 
42
  df = pd.DataFrame(candles)
@@ -44,31 +42,37 @@ def calculate_indicators(candles):
44
  for c in cols:
45
  df[c] = df[c].astype(float)
46
 
 
47
  df['ema20'] = df['close'].ewm(span=20, adjust=False).mean()
48
  df['ema50'] = df['close'].ewm(span=50, adjust=False).mean()
49
 
 
50
  df['std'] = df['close'].rolling(window=20).std()
51
  df['bb_upper'] = df['ema20'] + (df['std'] * 2)
52
  df['bb_lower'] = df['ema20'] - (df['std'] * 2)
53
 
 
54
  delta = df['close'].diff()
55
  gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
56
  loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
57
  rs = gain / loss
58
  df['rsi'] = 100 - (100 / (1 + rs))
59
 
 
60
  k = df['close'].ewm(span=12, adjust=False).mean()
61
  d = df['close'].ewm(span=26, adjust=False).mean()
62
  df['macd'] = k - d
63
  df['macd_signal'] = df['macd'].ewm(span=9, adjust=False).mean()
64
  df['macd_hist'] = df['macd'] - df['macd_signal']
65
 
 
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
  df['dist_ema20'] = (df['close'] - df['ema20']) / df['ema20']
73
  df['dist_ema50'] = (df['close'] - df['ema50']) / df['ema50']
74
  df['bb_width'] = (df['bb_upper'] - df['bb_lower']) / df['ema20']
@@ -76,14 +80,22 @@ def calculate_indicators(candles):
76
  df['vol_change'] = df['volume'].pct_change()
77
  df['log_ret'] = np.log(df['close'] / df['close'].shift(1))
78
 
 
79
  df['datetime'] = pd.to_datetime(df['time'], unit='s')
80
  df['hour_sin'] = np.sin(2 * np.pi * df['datetime'].dt.hour / 24)
81
  df['hour_cos'] = np.cos(2 * np.pi * df['datetime'].dt.hour / 24)
82
 
83
- return df.dropna()
 
 
 
 
 
 
 
84
 
85
  def train_model(df):
86
- logging.info(f"Training CNN Model (Candle Output) on {len(df)} candles...")
87
 
88
  feature_cols = [
89
  'rsi', 'macd_hist', 'atr',
@@ -93,75 +105,55 @@ def train_model(df):
93
  'hour_sin', 'hour_cos'
94
  ]
95
 
96
- data_features = df[feature_cols].values
97
- scaler = StandardScaler()
98
- data_scaled = scaler.fit_transform(data_features)
99
-
100
- close_prices = df['close'].values
101
- high_prices = df['high'].values
102
- low_prices = df['low'].values
103
 
104
- targets = []
105
-
106
- # Create targets: 2 values per step (Return, Range)
107
- # Range is normalized by price to make it scale-invariant
108
- for i in range(len(close_prices) - PREDICTION_HORIZON):
109
- current_close = close_prices[i]
110
-
111
- step_targets = []
112
- for h in range(1, PREDICTION_HORIZON + 1):
113
- future_idx = i + h
114
- # 1. Cumulative Return from current step to future step
115
- ret = (close_prices[future_idx] - current_close) / current_close
116
-
117
- # 2. Volatility/Range of that future candle ((High - Low) / Close)
118
- rng = (high_prices[future_idx] - low_prices[future_idx]) / close_prices[future_idx]
119
-
120
- step_targets.extend([ret, rng])
121
-
122
- targets.append(step_targets)
123
 
124
- targets = np.array(targets)
 
 
125
 
126
- X = []
127
- y = []
 
 
 
128
 
129
- valid_length = len(targets) - LOOKBACK_WINDOW
130
- if valid_length <= 0:
131
- return None, None
132
 
133
- for i in range(valid_length):
134
- X.append(data_scaled[i : i + LOOKBACK_WINDOW])
135
- y.append(targets[i + LOOKBACK_WINDOW - 1])
136
-
137
- X = np.array(X)
138
- y = np.array(y)
139
-
140
- if len(X) < 100:
141
  return None, None
142
 
143
- # Model architecture for multi-output regression
144
- model = models.Sequential([
145
- layers.Input(shape=(LOOKBACK_WINDOW, len(feature_cols))),
146
- layers.Conv1D(filters=64, kernel_size=3, activation='relu', padding='same'),
147
- layers.MaxPooling1D(pool_size=2),
148
- layers.Dropout(0.2),
149
- layers.Conv1D(filters=32, kernel_size=3, activation='relu', padding='same'),
150
- layers.GlobalAveragePooling1D(),
151
- layers.Dense(64, activation='relu'),
152
- layers.Dropout(0.1),
153
- layers.Dense(PREDICTION_HORIZON * 2) # Output: Return and Range for each step
154
- ])
155
-
156
- model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss='mse')
157
- early_stop = callbacks.EarlyStopping(monitor='loss', patience=5, restore_best_weights=True)
158
- model.fit(X, y, epochs=25, batch_size=32, verbose=0, callbacks=[early_stop])
 
 
 
159
 
160
- return model, scaler
161
 
162
- def get_prediction(df, model, scaler):
163
- if model is None or scaler is None:
164
- return []
165
 
166
  feature_cols = [
167
  'rsi', 'macd_hist', 'atr',
@@ -171,95 +163,97 @@ def get_prediction(df, model, scaler):
171
  'hour_sin', 'hour_cos'
172
  ]
173
 
174
- last_window = df.iloc[-LOOKBACK_WINDOW:][feature_cols].values
 
 
 
 
175
 
176
- if len(last_window) < LOOKBACK_WINDOW:
177
- return []
178
 
179
- last_window_scaled = scaler.transform(last_window)
180
- last_window_reshaped = last_window_scaled.reshape(1, LOOKBACK_WINDOW, len(feature_cols))
181
-
182
- # Shape: (1, PREDICTION_HORIZON * 2)
183
- raw_preds = model.predict(last_window_reshaped, verbose=0)[0]
184
 
185
- current_close = df.iloc[-1]['close']
186
  current_time = int(df.iloc[-1]['time'])
 
187
 
188
  pred_candles = []
 
189
 
190
- # Initialize previous close as current real close for the chain
191
- prev_close = current_close
192
-
193
- for i in range(PREDICTION_HORIZON):
194
- # Extract pairs: [Return_1, Range_1, Return_2, Range_2, ...]
195
- idx_ret = i * 2
196
- idx_rng = i * 2 + 1
197
-
198
- pred_ret = raw_preds[idx_ret]
199
- pred_rng = raw_preds[idx_rng]
200
 
201
- # 1. Calculate Close
202
- # Prediction is cumulative return from the START point (current_close)
203
- # This is more stable than recursive step-by-step
204
- future_close = current_close * (1 + pred_ret)
205
 
206
- # 2. Calculate Open
207
- # Logic: Open of candle T is roughly Close of candle T-1
208
- future_open = prev_close
 
209
 
210
- # 3. Calculate High/Low using predicted Range
211
- # Ensure Range isn't negative or impossibly small
212
- candle_range_val = max(pred_rng * future_close, abs(future_close - future_open) * 1.1)
 
213
 
214
- # Center the range around the body
215
- mid_point = (future_open + future_close) / 2
216
 
217
- # Distribute range
218
- future_high = mid_point + (candle_range_val / 2)
219
- future_low = mid_point - (candle_range_val / 2)
 
 
 
 
220
 
221
- # Ensure High >= Max(O, C) and Low <= Min(O, C)
222
- future_high = max(future_high, future_open, future_close)
223
- future_low = min(future_low, future_open, future_close)
 
224
 
225
- pred_candles.append({
226
- "time": current_time + ((i + 1) * 60),
227
- "open": float(future_open),
228
- "high": float(future_high),
229
- "low": float(future_low),
230
- "close": float(future_close)
231
  })
232
 
233
  prev_close = future_close
234
 
235
- return pred_candles
236
 
237
  async def process_market_data():
238
  if not market_state['ready'] or not market_state['ohlc_history']:
239
  return {"error": "Initializing..."}
240
 
241
  df = calculate_indicators(market_state['ohlc_history'])
242
- if df is None or len(df) < LOOKBACK_WINDOW + 50:
243
  return {"error": "Not enough data"}
244
 
 
245
  if market_state['model'] is None or (time.time() - market_state['last_training_time'] > TRAIN_INTERVAL):
246
  try:
247
  loop = asyncio.get_running_loop()
248
- model, scaler = await loop.run_in_executor(executor, train_model, df)
249
  if model is not None:
250
  market_state['model'] = model
251
- market_state['scaler'] = scaler
252
  market_state['last_training_time'] = time.time()
253
- logging.info("CNN Model Retrained.")
254
  except Exception as e:
255
  logging.error(f"Training failed: {e}")
256
 
257
  predictions = []
 
258
  try:
259
- predictions = get_prediction(df, market_state['model'], market_state['scaler'])
260
  except Exception as e:
261
  logging.error(f"Prediction failed: {e}")
262
 
 
263
  df_clean = df.replace([np.inf, -np.inf], np.nan)
264
  cols_to_keep = ['time', 'open', 'high', 'low', 'close', 'volume', 'ema20', 'bb_upper', 'bb_lower', 'rsi', 'macd_hist']
265
  df_clean = df_clean[cols_to_keep].where(pd.notnull(df_clean), None)
@@ -277,23 +271,25 @@ async def process_market_data():
277
  return {
278
  "data": display_data,
279
  "prediction": predictions,
 
280
  "stats": {
281
  "price": last_close,
282
  "change": round(price_change, 2),
283
  "rsi": round(float(last_row.get('rsi', 0)), 1) if pd.notna(last_row.get('rsi')) else 0,
284
- "macd": round(float(last_row.get('macd_hist', 0)), 2) if pd.notna(last_row.get('macd_hist')) else 0,
285
  "atr": round(float(last_row.get('atr', 0)), 2) if pd.notna(last_row.get('atr')) else 0,
286
  "volume": round(float(last_row.get('volume', 0)), 2) if pd.notna(last_row.get('volume')) else 0
287
  }
288
  }
289
 
 
290
  HTML_PAGE = """
291
  <!DOCTYPE html>
292
  <html lang="en">
293
  <head>
294
  <meta charset="UTF-8">
295
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
296
- <title>BTC/USD AI Candles</title>
297
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
298
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
299
  <style>
@@ -367,7 +363,7 @@ HTML_PAGE = """
367
  }
368
  .chart-wrapper { position: relative; border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
369
  #main-chart { flex: 5; }
370
- #volume-chart { flex: 1; min-height: 60px; }
371
  #osc-chart { flex: 1.5; min-height: 80px; }
372
  .chart-label {
373
  position: absolute; top: 12px; left: 16px; z-index: 10;
@@ -387,7 +383,6 @@ HTML_PAGE = """
387
  border-top-color: #00ff88; border-radius: 50%; animation: spin 1s linear infinite;
388
  }
389
  @keyframes spin { to { transform: rotate(360deg); } }
390
- .loading-text { margin-top: 20px; font-size: 14px; color: #666; }
391
  .prediction-badge {
392
  position: absolute; top: 12px; right: 16px;
393
  background: rgba(191, 90, 242, 0.15); border: 1px solid rgba(191, 90, 242, 0.3);
@@ -429,29 +424,28 @@ HTML_PAGE = """
429
  <div class="indicator-group"><span class="indicator-label">BB Upper</span><span id="bb-upper" class="indicator-value" style="color: #26a69a">--</span></div>
430
  <div class="indicator-group"><span class="indicator-label">BB Lower</span><span id="bb-lower" class="indicator-value" style="color: #ef5350">--</span></div>
431
  <div class="indicator-group"><span class="indicator-label">MACD</span><span id="macd-val" class="indicator-value">--</span></div>
432
- <div class="indicator-group"><span class="indicator-label">Volume</span><span id="vol-val" class="indicator-value" style="color: #888">--</span></div>
433
  </div>
434
  <div class="charts-container">
435
  <div class="loading-overlay" id="loading">
436
  <div class="loader"></div>
437
- <div class="loading-text">Loading market data...</div>
438
  </div>
439
  <div id="main-chart" class="chart-wrapper">
440
  <div class="chart-label">
441
- <span><div class="dot" style="background: #00ff88"></div>History</span>
442
- <span><div class="dot" style="background: #00b8d4"></div>AI Prediction</span>
443
- <span><div class="dot" style="background: #2962FF"></div>EMA 20</span>
444
- <span><div class="dot" style="background: #26a69a; opacity: 0.5"></div>Bollinger</span>
445
  </div>
446
- <div class="prediction-badge">CNN: 100 Candles</div>
447
  </div>
448
  <div id="volume-chart" class="chart-wrapper">
449
- <div class="chart-label"><span><div class="dot" style="background: #5c6bc0"></div>Volume</span></div>
 
 
 
450
  </div>
451
  <div id="osc-chart" class="chart-wrapper">
452
  <div class="chart-label">
453
  <span><div class="dot" style="background: #9C27B0"></div>RSI</span>
454
- <span><div class="dot" style="background: #26a69a"></div>MACD Hist</span>
455
  </div>
456
  </div>
457
  </div>
@@ -478,27 +472,42 @@ document.addEventListener('DOMContentLoaded', () => {
478
  const volChart = LightweightCharts.createChart(volEl, chartOptions);
479
  const oscChart = LightweightCharts.createChart(oscEl, chartOptions);
480
 
481
- // Historic Data Series
482
  const candles = mainChart.addCandlestickSeries({
483
  upColor: '#00ff88', downColor: '#ff4757',
484
  borderUpColor: '#00ff88', borderDownColor: '#ff4757',
485
  wickUpColor: '#00ff88', wickDownColor: '#ff4757'
486
  });
487
 
488
- // Prediction Data Series (Distinct Colors)
489
- const predCandles = mainChart.addCandlestickSeries({
490
- upColor: '#00b8d4', downColor: '#aa00ff', // Cyan for up, Purple for down
491
- borderUpColor: '#00b8d4', borderDownColor: '#aa00ff',
492
- wickUpColor: '#00b8d4', wickDownColor: '#aa00ff'
493
- });
494
-
495
  const ema = mainChart.addLineSeries({ color: '#2962FF', lineWidth: 2, crosshairMarkerVisible: false });
496
  const bbUpper = mainChart.addLineSeries({ color: 'rgba(38, 166, 154, 0.4)', lineWidth: 1, crosshairMarkerVisible: false });
497
  const bbLower = mainChart.addLineSeries({ color: 'rgba(239, 83, 80, 0.4)', lineWidth: 1, crosshairMarkerVisible: false });
498
 
 
 
 
 
 
 
 
 
499
  const volumeSeries = volChart.addHistogramSeries({ priceFormat: { type: 'volume' }, priceScaleId: '' });
500
  volChart.priceScale('').applyOptions({ scaleMargins: { top: 0.1, bottom: 0 } });
501
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502
  const rsi = oscChart.addLineSeries({ color: '#9C27B0', lineWidth: 2, priceScaleId: 'rsi' });
503
  oscChart.priceScale('rsi').applyOptions({ scaleMargins: { top: 0.1, bottom: 0.1 } });
504
 
@@ -544,7 +553,6 @@ document.addEventListener('DOMContentLoaded', () => {
544
  macdEl.textContent = macdVal.toFixed(2);
545
  macdEl.style.color = macdVal >= 0 ? '#26a69a' : '#ef5350';
546
  }
547
- document.getElementById('vol-val').textContent = lastData.volume ? lastData.volume.toFixed(2) : '--';
548
  }
549
  }
550
 
@@ -569,6 +577,7 @@ document.addEventListener('DOMContentLoaded', () => {
569
  const candleData = d.filter(x => x && x.time && x.open).map(x => ({
570
  time: x.time, open: x.open, high: x.high, low: x.low, close: x.close
571
  }));
 
572
  if (candleData.length > 0) {
573
  candles.setData(candleData);
574
  ema.setData(safeMap(d, 'ema20'));
@@ -582,10 +591,17 @@ document.addEventListener('DOMContentLoaded', () => {
582
  time: x.time, value: x.macd_hist, color: x.macd_hist >= 0 ? '#26a69a' : '#ef5350'
583
  })));
584
 
 
585
  if (payload.prediction && payload.prediction.length > 0) {
 
586
  predCandles.setData(payload.prediction);
587
  }
588
 
 
 
 
 
 
589
  updateStats(payload.stats, d[d.length - 1]);
590
  if (!hasData) {
591
  hasData = true;
@@ -689,6 +705,7 @@ async def broadcast_worker():
689
  payload = await process_market_data()
690
  if payload and "data" in payload:
691
  msg = json.dumps(payload)
 
692
  current_clients = connected_clients.copy()
693
  disconnected = set()
694
  for ws in current_clients:
 
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 concurrent.futures import ThreadPoolExecutor
12
 
13
+ # --- CONFIGURATION ---
14
  SYMBOL_KRAKEN = "BTC/USD"
15
  PORT = 7860
16
  BROADCAST_RATE = 1.0
17
  PREDICTION_HORIZON = 100
18
  MAX_HISTORY = 5000
19
+ TRAIN_INTERVAL = 300
 
20
 
21
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
22
 
 
24
  "ohlc_history": [],
25
  "ready": False,
26
  "model": None,
27
+ "model_residuals": None,
28
  "last_training_time": 0,
29
  "last_price": 0,
30
  "price_change": 0
 
34
  executor = ThreadPoolExecutor(max_workers=1)
35
 
36
  def calculate_indicators(candles):
37
+ if len(candles) < 100:
38
  return None
39
 
40
  df = pd.DataFrame(candles)
 
42
  for c in cols:
43
  df[c] = df[c].astype(float)
44
 
45
+ # Moving Averages
46
  df['ema20'] = df['close'].ewm(span=20, adjust=False).mean()
47
  df['ema50'] = df['close'].ewm(span=50, adjust=False).mean()
48
 
49
+ # Bollinger Bands
50
  df['std'] = df['close'].rolling(window=20).std()
51
  df['bb_upper'] = df['ema20'] + (df['std'] * 2)
52
  df['bb_lower'] = df['ema20'] - (df['std'] * 2)
53
 
54
+ # RSI
55
  delta = df['close'].diff()
56
  gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
57
  loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
58
  rs = gain / loss
59
  df['rsi'] = 100 - (100 / (1 + rs))
60
 
61
+ # MACD
62
  k = df['close'].ewm(span=12, adjust=False).mean()
63
  d = df['close'].ewm(span=26, adjust=False).mean()
64
  df['macd'] = k - d
65
  df['macd_signal'] = df['macd'].ewm(span=9, adjust=False).mean()
66
  df['macd_hist'] = df['macd'] - df['macd_signal']
67
 
68
+ # ATR
69
  df['tr0'] = abs(df['high'] - df['low'])
70
  df['tr1'] = abs(df['high'] - df['close'].shift())
71
  df['tr2'] = abs(df['low'] - df['close'].shift())
72
  df['tr'] = df[['tr0', 'tr1', 'tr2']].max(axis=1)
73
  df['atr'] = df['tr'].rolling(window=14).mean()
74
 
75
+ # Features
76
  df['dist_ema20'] = (df['close'] - df['ema20']) / df['ema20']
77
  df['dist_ema50'] = (df['close'] - df['ema50']) / df['ema50']
78
  df['bb_width'] = (df['bb_upper'] - df['bb_lower']) / df['ema20']
 
80
  df['vol_change'] = df['volume'].pct_change()
81
  df['log_ret'] = np.log(df['close'] / df['close'].shift(1))
82
 
83
+ # Time encoding
84
  df['datetime'] = pd.to_datetime(df['time'], unit='s')
85
  df['hour_sin'] = np.sin(2 * np.pi * df['datetime'].dt.hour / 24)
86
  df['hour_cos'] = np.cos(2 * np.pi * df['datetime'].dt.hour / 24)
87
 
88
+ # Lag Features
89
+ for lag in [1, 2, 3, 5, 8]:
90
+ df[f'rsi_lag{lag}'] = df['rsi'].shift(lag)
91
+ df[f'macd_hist_lag{lag}'] = df['macd_hist'].shift(lag)
92
+ df[f'log_ret_lag{lag}'] = df['log_ret'].shift(lag)
93
+ df[f'vol_change_lag{lag}'] = df['vol_change'].shift(lag)
94
+
95
+ return df
96
 
97
  def train_model(df):
98
+ logging.info(f"Training ML Model on {len(df)} candles...")
99
 
100
  feature_cols = [
101
  'rsi', 'macd_hist', 'atr',
 
105
  'hour_sin', 'hour_cos'
106
  ]
107
 
108
+ for lag in [1, 2, 3, 5, 8]:
109
+ feature_cols.extend([
110
+ f'rsi_lag{lag}', f'macd_hist_lag{lag}',
111
+ f'log_ret_lag{lag}', f'vol_change_lag{lag}'
112
+ ])
 
 
113
 
114
+ data = df.dropna().copy()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
+ # Prepare targets (Future Returns relative to Current Price)
117
+ target_cols_dict = {}
118
+ target_names = []
119
 
120
+ for i in range(1, PREDICTION_HORIZON + 1):
121
+ col_name = f'target_return_{i}'
122
+ # Return at step i relative to step 0
123
+ target_cols_dict[col_name] = (data['close'].shift(-i) - data['close']) / data['close']
124
+ target_names.append(col_name)
125
 
126
+ targets_df = pd.DataFrame(target_cols_dict, index=data.index)
127
+ data = pd.concat([data, targets_df], axis=1).dropna()
 
128
 
129
+ if len(data) < 200:
 
 
 
 
 
 
 
130
  return None, None
131
 
132
+ X = data[feature_cols].values
133
+ y = data[target_names].values
134
+
135
+ model = RandomForestRegressor(
136
+ n_estimators=200,
137
+ max_depth=20,
138
+ min_samples_split=5,
139
+ min_samples_leaf=2,
140
+ max_features='sqrt',
141
+ n_jobs=-1,
142
+ random_state=42
143
+ )
144
+ model.fit(X, y)
145
+
146
+ # Calculate Residuals for Confidence Estimation
147
+ # (Using OOB or training residuals as a proxy for uncertainty)
148
+ predictions = model.predict(X)
149
+ residuals = y - predictions
150
+ residual_std = np.std(residuals, axis=0)
151
 
152
+ return model, residual_std
153
 
154
+ def get_prediction(df, model, residual_std):
155
+ if model is None or residual_std is None:
156
+ return [], []
157
 
158
  feature_cols = [
159
  'rsi', 'macd_hist', 'atr',
 
163
  'hour_sin', 'hour_cos'
164
  ]
165
 
166
+ for lag in [1, 2, 3, 5, 8]:
167
+ feature_cols.extend([
168
+ f'rsi_lag{lag}', f'macd_hist_lag{lag}',
169
+ f'log_ret_lag{lag}', f'vol_change_lag{lag}'
170
+ ])
171
 
172
+ last_row = df.iloc[[-1]][feature_cols]
 
173
 
174
+ if last_row.isnull().values.any():
175
+ return [], []
176
+
177
+ predicted_returns = model.predict(last_row.values)[0]
 
178
 
179
+ current_price = df.iloc[-1]['close']
180
  current_time = int(df.iloc[-1]['time'])
181
+ current_atr = df.iloc[-1]['atr']
182
 
183
  pred_candles = []
184
+ confidence_data = []
185
 
186
+ prev_close = current_price
187
+
188
+ for i, pct_change in enumerate(predicted_returns):
189
+ future_time = current_time + ((i + 1) * 60)
 
 
 
 
 
 
190
 
191
+ # Calculate predicted Close
192
+ future_close = current_price * (1 + pct_change)
 
 
193
 
194
+ # Construct Candle
195
+ # Open is previous candle's close
196
+ open_price = prev_close
197
+ close_price = future_close
198
 
199
+ # Heuristic for High/Low to make it look like a candle
200
+ # Use ATR and some noise or just fixed ratio to visualize structure
201
+ # Here we use a fixed structure based on ATR to keep it clean but candle-like
202
+ half_range = current_atr * 0.4
203
 
204
+ high_price = max(open_price, close_price) + half_range
205
+ low_price = min(open_price, close_price) - half_range
206
 
207
+ pred_candles.append({
208
+ "time": future_time,
209
+ "open": float(open_price),
210
+ "high": float(high_price),
211
+ "low": float(low_price),
212
+ "close": float(close_price)
213
+ })
214
 
215
+ # Confidence Metric (Standard Deviation of Residuals at this step)
216
+ # We plot the error margin width relative to price
217
+ sigma = residual_std[i]
218
+ error_margin = future_close * sigma * 1.96 # 95% CI width approx
219
 
220
+ confidence_data.append({
221
+ "time": future_time,
222
+ "value": float(error_margin)
 
 
 
223
  })
224
 
225
  prev_close = future_close
226
 
227
+ return pred_candles, confidence_data
228
 
229
  async def process_market_data():
230
  if not market_state['ready'] or not market_state['ohlc_history']:
231
  return {"error": "Initializing..."}
232
 
233
  df = calculate_indicators(market_state['ohlc_history'])
234
+ if df is None or len(df) < 100:
235
  return {"error": "Not enough data"}
236
 
237
+ # Retrain periodically
238
  if market_state['model'] is None or (time.time() - market_state['last_training_time'] > TRAIN_INTERVAL):
239
  try:
240
  loop = asyncio.get_running_loop()
241
+ model, res_std = await loop.run_in_executor(executor, train_model, df)
242
  if model is not None:
243
  market_state['model'] = model
244
+ market_state['model_residuals'] = res_std
245
  market_state['last_training_time'] = time.time()
 
246
  except Exception as e:
247
  logging.error(f"Training failed: {e}")
248
 
249
  predictions = []
250
+ confidence = []
251
  try:
252
+ predictions, confidence = get_prediction(df, market_state['model'], market_state['model_residuals'])
253
  except Exception as e:
254
  logging.error(f"Prediction failed: {e}")
255
 
256
+ # Prepare display data
257
  df_clean = df.replace([np.inf, -np.inf], np.nan)
258
  cols_to_keep = ['time', 'open', 'high', 'low', 'close', 'volume', 'ema20', 'bb_upper', 'bb_lower', 'rsi', 'macd_hist']
259
  df_clean = df_clean[cols_to_keep].where(pd.notnull(df_clean), None)
 
271
  return {
272
  "data": display_data,
273
  "prediction": predictions,
274
+ "confidence": confidence,
275
  "stats": {
276
  "price": last_close,
277
  "change": round(price_change, 2),
278
  "rsi": round(float(last_row.get('rsi', 0)), 1) if pd.notna(last_row.get('rsi')) else 0,
279
+ "macd": round(float(last_row.get('macd', 0)), 2) if pd.notna(last_row.get('macd')) else 0,
280
  "atr": round(float(last_row.get('atr', 0)), 2) if pd.notna(last_row.get('atr')) else 0,
281
  "volume": round(float(last_row.get('volume', 0)), 2) if pd.notna(last_row.get('volume')) else 0
282
  }
283
  }
284
 
285
+ # --- HTML/JS ---
286
  HTML_PAGE = """
287
  <!DOCTYPE html>
288
  <html lang="en">
289
  <head>
290
  <meta charset="UTF-8">
291
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
292
+ <title>BTC/USD AI Predictor</title>
293
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
294
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
295
  <style>
 
363
  }
364
  .chart-wrapper { position: relative; border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
365
  #main-chart { flex: 5; }
366
+ #volume-chart { flex: 1.5; min-height: 80px; }
367
  #osc-chart { flex: 1.5; min-height: 80px; }
368
  .chart-label {
369
  position: absolute; top: 12px; left: 16px; z-index: 10;
 
383
  border-top-color: #00ff88; border-radius: 50%; animation: spin 1s linear infinite;
384
  }
385
  @keyframes spin { to { transform: rotate(360deg); } }
 
386
  .prediction-badge {
387
  position: absolute; top: 12px; right: 16px;
388
  background: rgba(191, 90, 242, 0.15); border: 1px solid rgba(191, 90, 242, 0.3);
 
424
  <div class="indicator-group"><span class="indicator-label">BB Upper</span><span id="bb-upper" class="indicator-value" style="color: #26a69a">--</span></div>
425
  <div class="indicator-group"><span class="indicator-label">BB Lower</span><span id="bb-lower" class="indicator-value" style="color: #ef5350">--</span></div>
426
  <div class="indicator-group"><span class="indicator-label">MACD</span><span id="macd-val" class="indicator-value">--</span></div>
 
427
  </div>
428
  <div class="charts-container">
429
  <div class="loading-overlay" id="loading">
430
  <div class="loader"></div>
 
431
  </div>
432
  <div id="main-chart" class="chart-wrapper">
433
  <div class="chart-label">
434
+ <span><div class="dot" style="background: #00ff88"></div>Price</span>
435
+ <span><div class="dot" style="background: #bf5af2"></div>AI Prediction</span>
 
 
436
  </div>
437
+ <div class="prediction-badge">Forecast: 100 Candles</div>
438
  </div>
439
  <div id="volume-chart" class="chart-wrapper">
440
+ <div class="chart-label">
441
+ <span><div class="dot" style="background: #5c6bc0"></div>Volume</span>
442
+ <span><div class="dot" style="background: #ff9f43"></div>AI Uncertainty (±$)</span>
443
+ </div>
444
  </div>
445
  <div id="osc-chart" class="chart-wrapper">
446
  <div class="chart-label">
447
  <span><div class="dot" style="background: #9C27B0"></div>RSI</span>
448
+ <span><div class="dot" style="background: #26a69a"></div>MACD</span>
449
  </div>
450
  </div>
451
  </div>
 
472
  const volChart = LightweightCharts.createChart(volEl, chartOptions);
473
  const oscChart = LightweightCharts.createChart(oscEl, chartOptions);
474
 
475
+ // Main Chart Series
476
  const candles = mainChart.addCandlestickSeries({
477
  upColor: '#00ff88', downColor: '#ff4757',
478
  borderUpColor: '#00ff88', borderDownColor: '#ff4757',
479
  wickUpColor: '#00ff88', wickDownColor: '#ff4757'
480
  });
481
 
 
 
 
 
 
 
 
482
  const ema = mainChart.addLineSeries({ color: '#2962FF', lineWidth: 2, crosshairMarkerVisible: false });
483
  const bbUpper = mainChart.addLineSeries({ color: 'rgba(38, 166, 154, 0.4)', lineWidth: 1, crosshairMarkerVisible: false });
484
  const bbLower = mainChart.addLineSeries({ color: 'rgba(239, 83, 80, 0.4)', lineWidth: 1, crosshairMarkerVisible: false });
485
 
486
+ // AI Prediction Series (Candles)
487
+ const predCandles = mainChart.addCandlestickSeries({
488
+ upColor: 'rgba(191, 90, 242, 0.8)', downColor: 'rgba(191, 90, 242, 0.8)',
489
+ borderUpColor: '#bf5af2', borderDownColor: '#bf5af2',
490
+ wickUpColor: '#bf5af2', wickDownColor: '#bf5af2'
491
+ });
492
+
493
+ // Volume Chart Series
494
  const volumeSeries = volChart.addHistogramSeries({ priceFormat: { type: 'volume' }, priceScaleId: '' });
495
  volChart.priceScale('').applyOptions({ scaleMargins: { top: 0.1, bottom: 0 } });
496
 
497
+ // Confidence/Uncertainty Series (Near Volume)
498
+ const confidenceSeries = volChart.addLineSeries({
499
+ color: '#ff9f43',
500
+ lineWidth: 2,
501
+ priceScaleId: 'confidence',
502
+ lineStyle: LightweightCharts.LineStyle.Solid
503
+ });
504
+ // Position the confidence scale to not overlap heavily with volume (overlay mode)
505
+ volChart.priceScale('confidence').applyOptions({
506
+ scaleMargins: { top: 0.1, bottom: 0.7 }, // Keep it at top of volume pane
507
+ visible: true
508
+ });
509
+
510
+ // Oscillator Chart Series
511
  const rsi = oscChart.addLineSeries({ color: '#9C27B0', lineWidth: 2, priceScaleId: 'rsi' });
512
  oscChart.priceScale('rsi').applyOptions({ scaleMargins: { top: 0.1, bottom: 0.1 } });
513
 
 
553
  macdEl.textContent = macdVal.toFixed(2);
554
  macdEl.style.color = macdVal >= 0 ? '#26a69a' : '#ef5350';
555
  }
 
556
  }
557
  }
558
 
 
577
  const candleData = d.filter(x => x && x.time && x.open).map(x => ({
578
  time: x.time, open: x.open, high: x.high, low: x.low, close: x.close
579
  }));
580
+
581
  if (candleData.length > 0) {
582
  candles.setData(candleData);
583
  ema.setData(safeMap(d, 'ema20'));
 
591
  time: x.time, value: x.macd_hist, color: x.macd_hist >= 0 ? '#26a69a' : '#ef5350'
592
  })));
593
 
594
+ // Update Prediction Candles
595
  if (payload.prediction && payload.prediction.length > 0) {
596
+ // Prediction data is already in OHLC format
597
  predCandles.setData(payload.prediction);
598
  }
599
 
600
+ // Update Confidence Metric (Volume Pane)
601
+ if (payload.confidence && payload.confidence.length > 0) {
602
+ confidenceSeries.setData(payload.confidence);
603
+ }
604
+
605
  updateStats(payload.stats, d[d.length - 1]);
606
  if (!hasData) {
607
  hasData = true;
 
705
  payload = await process_market_data()
706
  if payload and "data" in payload:
707
  msg = json.dumps(payload)
708
+ # Iterate over a copy to avoid RuntimeError if set size changes
709
  current_clients = connected_clients.copy()
710
  disconnected = set()
711
  for ws in current_clients: