Update app.py
Browse files
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
|
| 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 =
|
| 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 |
-
"
|
| 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) <
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
def train_model(df):
|
| 86 |
-
logging.info(f"Training
|
| 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 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
high_prices = df['high'].values
|
| 102 |
-
low_prices = df['low'].values
|
| 103 |
|
| 104 |
-
|
| 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
|
|
|
|
|
|
|
| 125 |
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
return None, None
|
| 132 |
|
| 133 |
-
|
| 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 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
-
return model,
|
| 161 |
|
| 162 |
-
def get_prediction(df, model,
|
| 163 |
-
if model is None or
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
-
|
| 177 |
-
return []
|
| 178 |
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
raw_preds = model.predict(last_window_reshaped, verbose=0)[0]
|
| 184 |
|
| 185 |
-
|
| 186 |
current_time = int(df.iloc[-1]['time'])
|
|
|
|
| 187 |
|
| 188 |
pred_candles = []
|
|
|
|
| 189 |
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 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 |
-
#
|
| 202 |
-
|
| 203 |
-
# This is more stable than recursive step-by-step
|
| 204 |
-
future_close = current_close * (1 + pred_ret)
|
| 205 |
|
| 206 |
-
#
|
| 207 |
-
#
|
| 208 |
-
|
|
|
|
| 209 |
|
| 210 |
-
#
|
| 211 |
-
#
|
| 212 |
-
|
|
|
|
| 213 |
|
| 214 |
-
|
| 215 |
-
|
| 216 |
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
-
#
|
| 222 |
-
|
| 223 |
-
|
|
|
|
| 224 |
|
| 225 |
-
|
| 226 |
-
"time":
|
| 227 |
-
"
|
| 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) <
|
| 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,
|
| 249 |
if model is not None:
|
| 250 |
market_state['model'] = model
|
| 251 |
-
market_state['
|
| 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['
|
| 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('
|
| 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
|
| 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:
|
| 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>
|
| 442 |
-
<span><div class="dot" style="background: #
|
| 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">
|
| 447 |
</div>
|
| 448 |
<div id="volume-chart" class="chart-wrapper">
|
| 449 |
-
<div class="chart-label"
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
//
|
| 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:
|