| """ |
| EUR/USD Direction Prediction — Inference |
| ========================================= |
| Usage: |
| python predict.py |
| |
| Requires: yfinance, lightgbm, xgboost, ta, scikit-learn, joblib, numpy, pandas |
| """ |
| import json |
| import numpy as np |
| import pandas as pd |
| import yfinance as yf |
| import ta |
| import joblib |
|
|
| |
| LGB_WEIGHT = 0.39 |
| XGB_WEIGHT = 0.61 |
|
|
|
|
| def compute_features(df): |
| """Compute all features from OHLCV data.""" |
| LOOKBACK_WINDOWS = [5, 10, 21, 63, 126, 252] |
| |
| for w in LOOKBACK_WINDOWS: |
| df[f"log_return_{w}d"] = np.log(df["Close"] / df["Close"].shift(w)) |
| df[f"momentum_{w}d"] = df["Close"] / df["Close"].shift(w) - 1 |
| |
| df["log_return_1d"] = np.log(df["Close"] / df["Close"].shift(1)) |
| |
| for w in [5, 10, 21, 63]: |
| df[f"volatility_{w}d"] = df["log_return_1d"].rolling(w).std() |
| |
| for w in [5, 10, 20, 50, 100, 200]: |
| sma = df["Close"].rolling(w).mean() |
| df[f"price_to_sma_{w}"] = df["Close"] / sma - 1 |
| |
| df["ema_12"] = df["Close"].ewm(span=12).mean() |
| df["ema_26"] = df["Close"].ewm(span=26).mean() |
| df["ema_cross"] = df["ema_12"] / df["ema_26"] - 1 |
| |
| for w in [7, 14, 21]: |
| df[f"rsi_{w}"] = ta.momentum.RSIIndicator(df["Close"], window=w).rsi() |
| |
| macd = ta.trend.MACD(df["Close"]) |
| df["macd"] = macd.macd() |
| df["macd_signal"] = macd.macd_signal() |
| df["macd_diff"] = macd.macd_diff() |
| |
| bb = ta.volatility.BollingerBands(df["Close"], window=20, window_dev=2) |
| df["bb_pband"] = bb.bollinger_pband() |
| df["bb_wband"] = bb.bollinger_wband() |
| |
| atr = ta.volatility.AverageTrueRange(df["High"], df["Low"], df["Close"], window=14) |
| df["atr_14"] = atr.average_true_range() |
| df["atr_14_pct"] = df["atr_14"] / df["Close"] |
| |
| stoch = ta.momentum.StochasticOscillator(df["High"], df["Low"], df["Close"]) |
| df["stoch_k"] = stoch.stoch() |
| df["stoch_d"] = stoch.stoch_signal() |
| |
| adx = ta.trend.ADXIndicator(df["High"], df["Low"], df["Close"], window=14) |
| df["adx"] = adx.adx() |
| df["adx_pos"] = adx.adx_pos() |
| df["adx_neg"] = adx.adx_neg() |
| |
| df["williams_r"] = ta.momentum.WilliamsRIndicator(df["High"], df["Low"], df["Close"]).williams_r() |
| df["cci"] = ta.trend.CCIIndicator(df["High"], df["Low"], df["Close"]).cci() |
| |
| df["obv"] = ta.volume.OnBalanceVolumeIndicator(df["Close"], df["Volume"]).on_balance_volume() |
| df["obv_pct"] = df["obv"].pct_change(5) |
| |
| df["day_of_week"] = df.index.dayofweek |
| df["month"] = df.index.month |
| df["quarter"] = df.index.quarter |
| |
| df["hl_range"] = (df["High"] - df["Low"]) / df["Close"] |
| df["oc_range"] = (df["Close"] - df["Open"]) / df["Close"] |
| |
| for w in [10, 20, 50]: |
| rolling_high = df["High"].rolling(w).max() |
| rolling_low = df["Low"].rolling(w).min() |
| df[f"channel_pos_{w}"] = (df["Close"] - rolling_low) / (rolling_high - rolling_low + 1e-10) |
| |
| return df |
|
|
|
|
| def predict(): |
| |
| lgb_model = joblib.load("lgb_model.joblib") |
| xgb_model = joblib.load("xgb_model.joblib") |
| scaler = joblib.load("scaler.joblib") |
| feature_cols = json.load(open("feature_columns.json")) |
| |
| |
| df = yf.download("EURUSD=X", period="2y", progress=False) |
| if isinstance(df.columns, pd.MultiIndex): |
| df.columns = df.columns.get_level_values(0) |
| df["Volume"] = df["Volume"].replace(0, 1) |
| |
| df = compute_features(df) |
| df = df.dropna(subset=feature_cols) |
| |
| |
| latest = df.iloc[[-1]] |
| X_latest = scaler.transform(latest[feature_cols].values) |
| |
| lgb_prob = lgb_model.predict_proba(X_latest)[:, 1][0] |
| xgb_prob = xgb_model.predict_proba(X_latest)[:, 1][0] |
| ensemble_prob = LGB_WEIGHT * lgb_prob + XGB_WEIGHT * xgb_prob |
| |
| direction = "UP" if ensemble_prob >= 0.5 else "DOWN" |
| confidence = max(ensemble_prob, 1 - ensemble_prob) |
| |
| print(f"Date: {latest.index[0].date()}") |
| print(f"EUR/USD Close: {latest['Close'].values[0]:.5f}") |
| print(f"Prediction for next day: {direction}") |
| print(f"Confidence: {confidence:.1%}") |
| print(f" LightGBM prob(UP): {lgb_prob:.3f} (weight: {LGB_WEIGHT:.0%})") |
| print(f" XGBoost prob(UP): {xgb_prob:.3f} (weight: {XGB_WEIGHT:.0%})") |
| print(f" Ensemble prob(UP): {ensemble_prob:.3f}") |
|
|
| if __name__ == "__main__": |
| predict() |
|
|