""" 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 # Optimal ensemble weights (grid-optimised over 198 walk-forward folds) 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(): # Load models 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")) # Fetch recent data (need 300 days for indicators to warm up) 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) # Predict latest 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()