Alvin3y1 commited on
Commit
2503fda
·
verified ·
1 Parent(s): df9c251

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +93 -47
app.py CHANGED
@@ -8,10 +8,13 @@ import numpy as np
8
  from aiohttp import web
9
  from sklearn.ensemble import RandomForestRegressor
10
 
 
11
  SYMBOL_KRAKEN = "BTC/USD"
12
  PORT = 7860
13
  BROADCAST_RATE = 1.0
14
- PREDICTION_HORIZON = 100
 
 
15
 
16
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
17
 
@@ -35,96 +38,132 @@ def calculate_indicators(candles):
35
  for c in cols:
36
  df[c] = df[c].astype(float)
37
 
38
- df['ema'] = df['close'].ewm(span=20, adjust=False).mean()
39
- df['ema_fast'] = df['close'].ewm(span=9, adjust=False).mean()
40
- df['ema_slow'] = df['close'].ewm(span=50, adjust=False).mean()
41
 
42
- df['sma20'] = df['close'].rolling(window=20).mean()
43
  df['std'] = df['close'].rolling(window=20).std()
44
- df['bb_upper'] = df['sma20'] + (df['std'] * 2)
45
- df['bb_lower'] = df['sma20'] - (df['std'] * 2)
46
- df['bb_mid'] = df['sma20']
47
 
 
48
  delta = df['close'].diff()
49
  gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
50
  loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
51
  rs = gain / loss
52
  df['rsi'] = 100 - (100 / (1 + rs))
53
 
 
54
  k = df['close'].ewm(span=12, adjust=False).mean()
55
  d = df['close'].ewm(span=26, adjust=False).mean()
56
  df['macd'] = k - d
57
  df['macd_signal'] = df['macd'].ewm(span=9, adjust=False).mean()
58
  df['macd_hist'] = df['macd'] - df['macd_signal']
59
 
60
- low_min = df['low'].rolling(window=14).min()
61
- high_max = df['high'].rolling(window=14).max()
62
- df['stoch_k'] = 100 * ((df['close'] - low_min) / (high_max - low_min))
63
- df['stoch_d'] = df['stoch_k'].rolling(window=3).mean()
64
-
65
  df['tr0'] = abs(df['high'] - df['low'])
66
  df['tr1'] = abs(df['high'] - df['close'].shift())
67
  df['tr2'] = abs(df['low'] - df['close'].shift())
68
  df['tr'] = df[['tr0', 'tr1', 'tr2']].max(axis=1)
69
  df['atr'] = df['tr'].rolling(window=14).mean()
70
 
71
- df['obv'] = (np.sign(df['close'].diff()) * df['volume']).fillna(0).cumsum()
72
-
73
- df['tp'] = (df['high'] + df['low'] + df['close']) / 3
74
- df['vwap'] = (df['tp'] * df['volume']).cumsum() / df['volume'].cumsum()
 
 
 
 
 
 
 
75
 
 
 
 
 
 
 
76
  return df
77
 
78
  def train_model(df):
79
- logging.info("Training ML Model...")
80
 
81
- feature_cols = ['close', 'ema', 'bb_upper', 'bb_lower', 'rsi', 'macd', 'stoch_k', 'atr', 'obv', 'vwap']
 
 
 
 
 
 
82
 
83
  data = df.dropna().copy()
84
 
85
- future_shifts = {}
86
  targets = []
87
 
 
88
  for i in range(1, PREDICTION_HORIZON + 1):
89
- col_name = f'target_{i}'
90
- future_shifts[col_name] = data['close'].shift(-i)
 
91
  targets.append(col_name)
92
 
93
- target_df = pd.DataFrame(future_shifts, index=data.index)
94
- data = pd.concat([data, target_df], axis=1)
95
  data = data.dropna()
96
 
97
- if len(data) < 100:
98
  logging.warning("Not enough data to train model yet.")
99
  return None
100
 
101
  X = data[feature_cols].values
102
  y = data[targets].values
103
 
104
- model = RandomForestRegressor(n_estimators=50, max_depth=10, n_jobs=-1, random_state=42)
 
 
 
 
 
 
 
105
  model.fit(X, y)
106
 
107
- logging.info(f"Model Trained on {len(X)} samples.")
108
  return model
109
 
110
  def get_prediction(df, model):
111
  if model is None:
112
  return []
113
 
114
- feature_cols = ['close', 'ema', 'bb_upper', 'bb_lower', 'rsi', 'macd', 'stoch_k', 'atr', 'obv', 'vwap']
 
 
 
 
 
 
115
  last_row = df.iloc[[-1]][feature_cols]
116
 
117
  if last_row.isnull().values.any():
118
  return []
119
 
120
- prediction = model.predict(last_row.values)[0]
 
121
 
 
 
122
  current_time = int(df.iloc[-1]['time'])
 
123
  pred_data = []
124
- for i, price in enumerate(prediction):
 
 
 
125
  pred_data.append({
126
- "time": current_time + ((i + 1) * 60),
127
- "value": float(price)
128
  })
129
 
130
  return pred_data
@@ -133,23 +172,28 @@ def process_market_data():
133
  if not market_state['ready'] or not market_state['ohlc_history']:
134
  return {"error": "Initializing..."}
135
 
 
136
  df = calculate_indicators(market_state['ohlc_history'])
137
  if df is None or len(df) < 50:
138
  return {"error": "Not enough data"}
139
 
140
- if market_state['model'] is None or (time.time() - market_state['last_training_time'] > 900):
 
141
  try:
142
  market_state['model'] = train_model(df)
143
  market_state['last_training_time'] = time.time()
144
  except Exception as e:
145
  logging.error(f"Training failed: {e}")
146
 
 
147
  predictions = []
148
  try:
149
  predictions = get_prediction(df, market_state['model'])
150
  except Exception as e:
151
  logging.error(f"Prediction failed: {e}")
152
 
 
 
153
  df_clean = df.replace([np.inf, -np.inf], np.nan)
154
  df_clean = df_clean.astype(object).where(pd.notnull(df_clean), None)
155
 
@@ -160,12 +204,12 @@ def process_market_data():
160
  market_state['last_price'] = last_close
161
  market_state['price_change'] = price_change
162
 
163
- full_data = df_clean.to_dict('records')
164
-
165
  last_row = df.iloc[-1] if len(df) > 0 else {}
166
 
167
  return {
168
- "data": full_data,
169
  "prediction": predictions,
170
  "stats": {
171
  "price": last_close,
@@ -177,6 +221,7 @@ def process_market_data():
177
  }
178
  }
179
 
 
180
  HTML_PAGE = """
181
  <!DOCTYPE html>
182
  <html lang="en">
@@ -458,10 +503,6 @@ HTML_PAGE = """
458
  <span class="indicator-label">MACD</span>
459
  <span id="macd-val" class="indicator-value">--</span>
460
  </div>
461
- <div class="indicator-group">
462
- <span class="indicator-label">Stoch K</span>
463
- <span id="stoch-val" class="indicator-value" style="color: #ff9800">--</span>
464
- </div>
465
  <div class="indicator-group">
466
  <span class="indicator-label">Volume</span>
467
  <span id="vol-val" class="indicator-value" style="color: #888">--</span>
@@ -569,7 +610,8 @@ document.addEventListener('DOMContentLoaded', () => {
569
  color: '#bf5af2',
570
  lineWidth: 2,
571
  lineStyle: LightweightCharts.LineStyle.Dashed,
572
- crosshairMarkerVisible: false
 
573
  });
574
 
575
  const volumeSeries = volChart.addHistogramSeries({
@@ -640,7 +682,7 @@ document.addEventListener('DOMContentLoaded', () => {
640
  }
641
 
642
  if (lastData) {
643
- document.getElementById('ema-val').textContent = lastData.ema ? lastData.ema.toFixed(2) : '--';
644
  document.getElementById('bb-upper').textContent = lastData.bb_upper ? lastData.bb_upper.toFixed(2) : '--';
645
  document.getElementById('bb-lower').textContent = lastData.bb_lower ? lastData.bb_lower.toFixed(2) : '--';
646
 
@@ -651,7 +693,6 @@ document.addEventListener('DOMContentLoaded', () => {
651
  macdEl.style.color = macdVal >= 0 ? '#26a69a' : '#ef5350';
652
  }
653
 
654
- document.getElementById('stoch-val').textContent = lastData.stoch_k ? lastData.stoch_k.toFixed(1) : '--';
655
  document.getElementById('vol-val').textContent = lastData.volume ? lastData.volume.toFixed(2) : '--';
656
  }
657
  }
@@ -700,7 +741,7 @@ document.addEventListener('DOMContentLoaded', () => {
700
  if (candleData.length > 0) {
701
  candles.setData(candleData);
702
 
703
- const emaData = safeMap(d, 'ema');
704
  if (emaData.length > 0) ema.setData(emaData);
705
 
706
  const bbUpperData = safeMap(d, 'bb_upper');
@@ -770,6 +811,7 @@ document.addEventListener('DOMContentLoaded', () => {
770
  async def fetch_initial_data():
771
  try:
772
  async with aiohttp.ClientSession() as session:
 
773
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
774
  async with session.get(url, timeout=aiohttp.ClientTimeout(total=30)) as response:
775
  if response.status == 200:
@@ -787,7 +829,7 @@ async def fetch_initial_data():
787
  'close': float(c[4]),
788
  'volume': float(c[6])
789
  }
790
- for c in raw[-720:]
791
  ]
792
  market_state['ready'] = True
793
  logging.info(f"Loaded {len(market_state['ohlc_history'])} initial candles")
@@ -822,21 +864,25 @@ async def kraken_rest_worker():
822
  for c in raw[-10:]
823
  ]
824
 
 
825
  if market_state['ohlc_history']:
826
  existing_times = {c['time'] for c in market_state['ohlc_history']}
827
  for nc in new_candles:
828
  if nc['time'] in existing_times:
 
829
  for i, ec in enumerate(market_state['ohlc_history']):
830
  if ec['time'] == nc['time']:
831
  market_state['ohlc_history'][i] = nc
832
  break
833
  else:
 
834
  market_state['ohlc_history'].append(nc)
835
 
836
  market_state['ohlc_history'].sort(key=lambda x: x['time'])
837
 
838
- if len(market_state['ohlc_history']) > 800:
839
- market_state['ohlc_history'] = market_state['ohlc_history'][-800:]
 
840
 
841
  market_state['ready'] = True
842
  break
 
8
  from aiohttp import web
9
  from sklearn.ensemble import RandomForestRegressor
10
 
11
+ # --- CONFIGURATION ---
12
  SYMBOL_KRAKEN = "BTC/USD"
13
  PORT = 7860
14
  BROADCAST_RATE = 1.0
15
+ PREDICTION_HORIZON = 100 # Predict next 100 candles
16
+ MAX_HISTORY = 5000 # Store up to 5000 candles for training
17
+ TRAIN_INTERVAL = 300 # Retrain model every 5 minutes
18
 
19
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
20
 
 
38
  for c in cols:
39
  df[c] = df[c].astype(float)
40
 
41
+ # --- Standard Indicators ---
42
+ df['ema20'] = df['close'].ewm(span=20, adjust=False).mean()
43
+ df['ema50'] = df['close'].ewm(span=50, adjust=False).mean()
44
 
45
+ # Bollinger Bands
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
+ # RSI
51
  delta = df['close'].diff()
52
  gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
53
  loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
54
  rs = gain / loss
55
  df['rsi'] = 100 - (100 / (1 + rs))
56
 
57
+ # MACD
58
  k = df['close'].ewm(span=12, adjust=False).mean()
59
  d = df['close'].ewm(span=26, adjust=False).mean()
60
  df['macd'] = k - d
61
  df['macd_signal'] = df['macd'].ewm(span=9, adjust=False).mean()
62
  df['macd_hist'] = df['macd'] - df['macd_signal']
63
 
64
+ # ATR
 
 
 
 
65
  df['tr0'] = abs(df['high'] - df['low'])
66
  df['tr1'] = abs(df['high'] - df['close'].shift())
67
  df['tr2'] = abs(df['low'] - df['close'].shift())
68
  df['tr'] = df[['tr0', 'tr1', 'tr2']].max(axis=1)
69
  df['atr'] = df['tr'].rolling(window=14).mean()
70
 
71
+ # --- FEATURE ENGINEERING (Normalization) ---
72
+ # We create features that represent % differences rather than raw prices
73
+ # This helps the model learn patterns regardless of whether BTC is $20k or $100k
74
+
75
+ # Distance from EMAs (Percentage)
76
+ df['dist_ema20'] = (df['close'] - df['ema20']) / df['ema20']
77
+ df['dist_ema50'] = (df['close'] - df['ema50']) / df['ema50']
78
+
79
+ # Bollinger Band Width & Position
80
+ df['bb_width'] = (df['bb_upper'] - df['bb_lower']) / df['ema20']
81
+ df['bb_pos'] = (df['close'] - df['bb_lower']) / (df['bb_upper'] - df['bb_lower'])
82
 
83
+ # Volume Change
84
+ df['vol_change'] = df['volume'].pct_change()
85
+
86
+ # Log Returns (Momentum)
87
+ df['log_ret'] = np.log(df['close'] / df['close'].shift(1))
88
+
89
  return df
90
 
91
  def train_model(df):
92
+ logging.info(f"Training ML Model on {len(df)} candles...")
93
 
94
+ # Use normalized features for input
95
+ feature_cols = [
96
+ 'rsi', 'macd_hist', 'atr',
97
+ 'dist_ema20', 'dist_ema50',
98
+ 'bb_width', 'bb_pos',
99
+ 'vol_change', 'log_ret'
100
+ ]
101
 
102
  data = df.dropna().copy()
103
 
104
+ # --- CREATE TARGETS (Percentage Change) ---
105
  targets = []
106
 
107
+ # We want to predict the % return for the next 1 to N steps relative to CURRENT price
108
  for i in range(1, PREDICTION_HORIZON + 1):
109
+ col_name = f'target_return_{i}'
110
+ # Formula: (Price_Future - Price_Current) / Price_Current
111
+ data[col_name] = (data['close'].shift(-i) - data['close']) / data['close']
112
  targets.append(col_name)
113
 
 
 
114
  data = data.dropna()
115
 
116
+ if len(data) < 200:
117
  logging.warning("Not enough data to train model yet.")
118
  return None
119
 
120
  X = data[feature_cols].values
121
  y = data[targets].values
122
 
123
+ # Increase estimators for better stability
124
+ model = RandomForestRegressor(
125
+ n_estimators=100,
126
+ max_depth=15,
127
+ min_samples_split=5,
128
+ n_jobs=-1,
129
+ random_state=42
130
+ )
131
  model.fit(X, y)
132
 
133
+ logging.info(f"Model Trained successfully.")
134
  return model
135
 
136
  def get_prediction(df, model):
137
  if model is None:
138
  return []
139
 
140
+ feature_cols = [
141
+ 'rsi', 'macd_hist', 'atr',
142
+ 'dist_ema20', 'dist_ema50',
143
+ 'bb_width', 'bb_pos',
144
+ 'vol_change', 'log_ret'
145
+ ]
146
+
147
  last_row = df.iloc[[-1]][feature_cols]
148
 
149
  if last_row.isnull().values.any():
150
  return []
151
 
152
+ # The model predicts Percentage Returns
153
+ predicted_returns = model.predict(last_row.values)[0]
154
 
155
+ # Convert Percentage Returns back to Absolute Prices
156
+ current_price = df.iloc[-1]['close']
157
  current_time = int(df.iloc[-1]['time'])
158
+
159
  pred_data = []
160
+ for i, pct_change in enumerate(predicted_returns):
161
+ # Reconstruct: Price = Current * (1 + Predicted_Return)
162
+ future_price = current_price * (1 + pct_change)
163
+
164
  pred_data.append({
165
+ "time": current_time + ((i + 1) * 60), # Add 60s for each step
166
+ "value": float(future_price)
167
  })
168
 
169
  return pred_data
 
172
  if not market_state['ready'] or not market_state['ohlc_history']:
173
  return {"error": "Initializing..."}
174
 
175
+ # 1. Calculate Indicators
176
  df = calculate_indicators(market_state['ohlc_history'])
177
  if df is None or len(df) < 50:
178
  return {"error": "Not enough data"}
179
 
180
+ # 2. Train Model (Periodically)
181
+ if market_state['model'] is None or (time.time() - market_state['last_training_time'] > TRAIN_INTERVAL):
182
  try:
183
  market_state['model'] = train_model(df)
184
  market_state['last_training_time'] = time.time()
185
  except Exception as e:
186
  logging.error(f"Training failed: {e}")
187
 
188
+ # 3. Get Prediction
189
  predictions = []
190
  try:
191
  predictions = get_prediction(df, market_state['model'])
192
  except Exception as e:
193
  logging.error(f"Prediction failed: {e}")
194
 
195
+ # 4. Prepare Data for Broadcast
196
+ # Clean NaNs for JSON
197
  df_clean = df.replace([np.inf, -np.inf], np.nan)
198
  df_clean = df_clean.astype(object).where(pd.notnull(df_clean), None)
199
 
 
204
  market_state['last_price'] = last_close
205
  market_state['price_change'] = price_change
206
 
207
+ # Only send last 500 candles to client to save bandwidth, but keep full history in memory
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,
213
  "prediction": predictions,
214
  "stats": {
215
  "price": last_close,
 
221
  }
222
  }
223
 
224
+ # --- FRONTEND HTML (No changes needed, handles price data perfectly) ---
225
  HTML_PAGE = """
226
  <!DOCTYPE html>
227
  <html lang="en">
 
503
  <span class="indicator-label">MACD</span>
504
  <span id="macd-val" class="indicator-value">--</span>
505
  </div>
 
 
 
 
506
  <div class="indicator-group">
507
  <span class="indicator-label">Volume</span>
508
  <span id="vol-val" class="indicator-value" style="color: #888">--</span>
 
610
  color: '#bf5af2',
611
  lineWidth: 2,
612
  lineStyle: LightweightCharts.LineStyle.Dashed,
613
+ crosshairMarkerVisible: false,
614
+ title: 'Forecast'
615
  });
616
 
617
  const volumeSeries = volChart.addHistogramSeries({
 
682
  }
683
 
684
  if (lastData) {
685
+ document.getElementById('ema-val').textContent = lastData.ema20 ? lastData.ema20.toFixed(2) : '--';
686
  document.getElementById('bb-upper').textContent = lastData.bb_upper ? lastData.bb_upper.toFixed(2) : '--';
687
  document.getElementById('bb-lower').textContent = lastData.bb_lower ? lastData.bb_lower.toFixed(2) : '--';
688
 
 
693
  macdEl.style.color = macdVal >= 0 ? '#26a69a' : '#ef5350';
694
  }
695
 
 
696
  document.getElementById('vol-val').textContent = lastData.volume ? lastData.volume.toFixed(2) : '--';
697
  }
698
  }
 
741
  if (candleData.length > 0) {
742
  candles.setData(candleData);
743
 
744
+ const emaData = safeMap(d, 'ema20');
745
  if (emaData.length > 0) ema.setData(emaData);
746
 
747
  const bbUpperData = safeMap(d, 'bb_upper');
 
811
  async def fetch_initial_data():
812
  try:
813
  async with aiohttp.ClientSession() as session:
814
+ # Although Kraken returns limited data, we set logic to accumulate it over time.
815
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
816
  async with session.get(url, timeout=aiohttp.ClientTimeout(total=30)) as response:
817
  if response.status == 200:
 
829
  'close': float(c[4]),
830
  'volume': float(c[6])
831
  }
832
+ for c in raw
833
  ]
834
  market_state['ready'] = True
835
  logging.info(f"Loaded {len(market_state['ohlc_history'])} initial candles")
 
864
  for c in raw[-10:]
865
  ]
866
 
867
+ # Intelligent Merge to keep history
868
  if market_state['ohlc_history']:
869
  existing_times = {c['time'] for c in market_state['ohlc_history']}
870
  for nc in new_candles:
871
  if nc['time'] in existing_times:
872
+ # Update existing (in case close price changed)
873
  for i, ec in enumerate(market_state['ohlc_history']):
874
  if ec['time'] == nc['time']:
875
  market_state['ohlc_history'][i] = nc
876
  break
877
  else:
878
+ # Append new
879
  market_state['ohlc_history'].append(nc)
880
 
881
  market_state['ohlc_history'].sort(key=lambda x: x['time'])
882
 
883
+ # Keep MAX_HISTORY (5000)
884
+ if len(market_state['ohlc_history']) > MAX_HISTORY:
885
+ market_state['ohlc_history'] = market_state['ohlc_history'][-MAX_HISTORY:]
886
 
887
  market_state['ready'] = True
888
  break