""" ONNX Model Training Script for BTCUSD 1-Minute Data Uses yfinance (Yahoo Finance) for historical data This script trains a neural network model for BTCUSD price prediction on 1-minute timeframe and exports it to ONNX format. Usage: python main.py """ import os import sys from datetime import datetime, timedelta import numpy as np import pandas as pd import yfinance as yf import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers from sklearn.preprocessing import MinMaxScaler from sklearn.model_selection import train_test_split import tf2onnx import onnx from tqdm import tqdm import pickle class BTCUSD1MinTrainer: """ Trainer class for creating ONNX models from BTCUSD 1-minute MT5 data. """ def __init__(self, symbol: str = "BTC-USD", lookback: int = 60, prediction_horizon: int = 1): """ Initialize the trainer. Args: symbol: Trading symbol (default: 'BTC-USD' for Yahoo Finance) lookback: Number of bars to look back for prediction (default: 60) prediction_horizon: Number of bars ahead to predict (default: 1) """ self.symbol = symbol self.lookback = lookback self.prediction_horizon = prediction_horizon self.scaler = MinMaxScaler() self.model = None print(f"Using yfinance for data source. Symbol: {self.symbol}") def fetch_data(self, start_date: datetime, end_date: datetime) -> pd.DataFrame: """ Fetch historical data from Yahoo Finance using yfinance. Args: start_date: Start date for data end_date: End date for data Returns: DataFrame with OHLCV data """ print(f"\nFetching {self.symbol} 1-minute data from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}...") # yfinance can only fetch 7 days of 1-minute data at a time # For longer periods, we need to fetch in chunks all_data = [] current_start = start_date # For 1-minute data, yfinance limits to last 7 days # So we'll fetch the most recent 7 days available print("Note: yfinance 1-minute data is limited to last 7 days") print("Fetching most recent available 1-minute data...") # Get ticker ticker = yf.Ticker(self.symbol) # Try to fetch 1-minute data (limited to 7 days) # If we need more data, we'll use daily data and resample try: # Fetch 1-minute data (max 7 days) df = ticker.history(start=start_date, end=end_date, interval='1m') if df is None or len(df) == 0: print("Warning: No 1-minute data available, trying daily data...") # Fall back to daily data df = ticker.history(start=start_date, end=end_date, interval='1d') if df is None or len(df) == 0: raise ValueError(f"No data available for {self.symbol}") print(f"Using daily data instead (will resample to 1-minute for training)") except Exception as e: print(f"Error fetching 1-minute data: {e}") print("Falling back to daily data...") df = ticker.history(start=start_date, end=end_date, interval='1d') if df is None or len(df) == 0: raise ValueError(f"No data available for {self.symbol}: {e}") # Rename columns to match expected format df.columns = [col.lower().replace(' ', '_') for col in df.columns] # Ensure we have the required columns required_cols = ['open', 'high', 'low', 'close', 'volume'] missing_cols = [col for col in required_cols if col not in df.columns] if missing_cols: raise ValueError(f"Missing required columns: {missing_cols}") # Rename 'volume' to 'tick_volume' for consistency if 'volume' in df.columns: df['tick_volume'] = df['volume'] df = df.drop('volume', axis=1) # Remove duplicates and sort df = df[~df.index.duplicated(keep='first')] df = df.sort_index() print(f"Total fetched: {len(df)} bars") if len(df) > 0: print(f"Date range: {df.index[0]} to {df.index[-1]}") print(f"Timeframe: {df.index[1] - df.index[0] if len(df) > 1 else 'N/A'}") else: raise ValueError("DataFrame is empty after processing") return df def prepare_features(self, df: pd.DataFrame) -> pd.DataFrame: """ Prepare features for training. Args: df: Raw OHLCV data Returns: DataFrame with features """ print("\nPreparing features...") feature_df = df[['open', 'high', 'low', 'close', 'tick_volume']].copy() # Add technical indicators as features print(" Calculating RSI...") feature_df['rsi'] = self._calculate_rsi(df['close'], period=14) print(" Calculating EMAs...") feature_df['ema_20'] = df['close'].ewm(span=20, adjust=False).mean() feature_df['ema_50'] = df['close'].ewm(span=50, adjust=False).mean() print(" Calculating ATR...") feature_df['atr'] = self._calculate_atr(df, period=14) # Price changes feature_df['price_change'] = df['close'].pct_change() feature_df['high_low_ratio'] = df['high'] / df['low'] # Volume features feature_df['volume_ma'] = df['tick_volume'].rolling(window=20).mean() feature_df['volume_ratio'] = df['tick_volume'] / feature_df['volume_ma'] # Drop NaN values feature_df = feature_df.dropna() print(f" Features prepared: {len(feature_df)} samples, {len(feature_df.columns)} features") print(f" Features: {list(feature_df.columns)}") return feature_df def _calculate_rsi(self, prices: pd.Series, period: int = 14) -> pd.Series: """Calculate RSI indicator.""" delta = prices.diff() gain = (delta.where(delta > 0, 0)).rolling(window=period).mean() loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean() rs = gain / loss rsi = 100 - (100 / (1 + rs)) return rsi def _calculate_atr(self, df: pd.DataFrame, period: int = 14) -> pd.Series: """Calculate ATR indicator.""" high_low = df['high'] - df['low'] high_close = np.abs(df['high'] - df['close'].shift()) low_close = np.abs(df['low'] - df['close'].shift()) tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1) atr = tr.rolling(window=period).mean() return atr def create_sequences(self, data: np.ndarray, target: np.ndarray) -> tuple: """ Create sequences for LSTM/RNN training. Args: data: Feature data target: Target values (price change percentages) Returns: Tuple of (X, y) sequences """ print("\nCreating sequences...") X, y = [], [] for i in tqdm(range(self.lookback, len(data) - self.prediction_horizon + 1), desc="Creating sequences"): X.append(data[i - self.lookback:i]) y.append(target[i]) X = np.array(X) y = np.array(y) print(f" Sequences created: X shape {X.shape}, y shape {y.shape}") return X, y def build_model(self, input_shape: tuple) -> keras.Model: """ Build the neural network model. Args: input_shape: Shape of input data (lookback, features) Returns: Compiled Keras model """ print(f"\nBuilding model with input shape: {input_shape}") model = keras.Sequential([ layers.LSTM(128, return_sequences=True, input_shape=input_shape), layers.Dropout(0.3), layers.LSTM(64, return_sequences=True), layers.Dropout(0.3), layers.LSTM(32), layers.Dropout(0.3), layers.Dense(32, activation='relu'), layers.Dense(16, activation='relu'), layers.Dense(1) # Predict price change percentage ]) model.compile( optimizer=keras.optimizers.Adam(learning_rate=0.0005), loss='mse', metrics=['mae'] ) print(f" Model parameters: {model.count_params():,}") model.summary() return model def train(self, start_date: datetime, end_date: datetime, epochs: int = 50, batch_size: int = 32, validation_split: float = 0.2, verbose: int = 1): """ Train the model. Args: start_date: Start date for training data end_date: End date for training data epochs: Number of training epochs batch_size: Batch size for training validation_split: Fraction of data to use for validation verbose: Verbosity level """ # Fetch data df = self.fetch_data(start_date, end_date) feature_df = self.prepare_features(df) # Prepare target: price change percentage for next bar # Calculate future price change: (next_close - current_close) / current_close close_prices = feature_df['close'].values target = [] for i in range(len(close_prices)): if i + self.prediction_horizon < len(close_prices): current_price = close_prices[i] future_price = close_prices[i + self.prediction_horizon] price_change_pct = (future_price - current_price) / current_price if current_price > 0 else 0.0 target.append(price_change_pct) else: target.append(0.0) target = pd.Series(target, index=feature_df.index) # Keep 'close' in features - it's needed for the model # Align target with features valid_idx = ~(target.isna() | feature_df.isna().any(axis=1)) feature_df = feature_df[valid_idx] target = target[valid_idx] print(f"\nValid samples after alignment: {len(feature_df)}") # Normalize features print("\nNormalizing features...") feature_array = self.scaler.fit_transform(feature_df.values) # Create sequences X, y = self.create_sequences(feature_array, target.values) # Split into train and validation split_idx = int(len(X) * (1 - validation_split)) X_train, X_val = X[:split_idx], X[split_idx:] y_train, y_val = y[:split_idx], y[split_idx:] print(f"\nTrain set: {len(X_train)} samples") print(f"Validation set: {len(X_val)} samples") # Build model input_shape = (self.lookback, feature_array.shape[1]) self.model = self.build_model(input_shape) # Train model print(f"\nTraining model for {epochs} epochs...") history = self.model.fit( X_train, y_train, batch_size=batch_size, epochs=epochs, validation_data=(X_val, y_val), verbose=verbose, callbacks=[ keras.callbacks.EarlyStopping( monitor='val_loss', patience=10, restore_best_weights=True ), keras.callbacks.ReduceLROnPlateau( monitor='val_loss', factor=0.5, patience=5, min_lr=1e-7 ) ] ) # Evaluate print("\nEvaluating model...") train_loss = self.model.evaluate(X_train, y_train, verbose=0) val_loss = self.model.evaluate(X_val, y_val, verbose=0) print(f"Train Loss: {train_loss[0]:.6f}, MAE: {train_loss[1]:.6f}") print(f"Val Loss: {val_loss[0]:.6f}, MAE: {val_loss[1]:.6f}") return history def export_to_onnx(self, output_path: str): """ Export the trained model to ONNX format. Args: output_path: Path to save ONNX model """ if self.model is None: raise ValueError("Model must be trained before exporting") print(f"\nExporting model to ONNX format: {output_path}") # Get number of features num_features = self.model.input_shape[2] if len(self.model.input_shape) > 2 else self.model.input_shape[1] print(f"Using {num_features} features for ONNX export") # Create input signature input_shape = (None, self.lookback, num_features) spec = (tf.TensorSpec(input_shape, tf.float32, name="input"),) # Fix output_names for Sequential model if not hasattr(self.model, 'output_names'): if hasattr(self.model, 'outputs') and self.model.outputs: self.model.output_names = [f'output_{i}' for i in range(len(self.model.outputs))] else: self.model.output_names = ['output'] # Convert to ONNX onnx_model, _ = tf2onnx.convert.from_keras( self.model, input_signature=spec, opset=13 ) # Save ONNX model os.makedirs(os.path.dirname(output_path), exist_ok=True) onnx.save_model(onnx_model, output_path) print(f"ONNX model saved to: {output_path}") # Save scaler scaler_path = output_path.replace('.onnx', '_scaler.pkl') with open(scaler_path, 'wb') as f: pickle.dump(self.scaler, f) print(f"Scaler saved to: {scaler_path}") def cleanup(self): """Clean up (no-op for yfinance).""" pass def main(): """Main function.""" print("="*60) print("BTCUSD 1-Minute ONNX Model Training") print("="*60) # Training parameters symbol = "BTC-USD" # Yahoo Finance symbol lookback = 60 # 60 minutes of history epochs = 50 batch_size = 64 # Larger batch for 1-minute data # Date range: Use recent data (yfinance 1m data limited to 7 days) # For longer training, we'll use the most recent available data end_date = datetime.now() start_date = end_date - timedelta(days=7) # Last 7 days for 1-minute data print(f"Note: yfinance 1-minute data is limited to last 7 days") print(f"Using date range: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}") # Output paths output_dir = "models" os.makedirs(output_dir, exist_ok=True) model_path = os.path.join(output_dir, f"{symbol}_M1_model.onnx") trainer = None try: # Create trainer trainer = BTCUSD1MinTrainer( symbol=symbol, lookback=lookback ) # Train model history = trainer.train( start_date=start_date, end_date=end_date, epochs=epochs, batch_size=batch_size, validation_split=0.2, verbose=1 ) # Export to ONNX trainer.export_to_onnx(model_path) print("\n" + "="*60) print("Training completed successfully!") print("="*60) print(f"Model saved to: {model_path}") except Exception as e: print(f"\nERROR: Training failed: {e}") import traceback traceback.print_exc() return 1 finally: if trainer: trainer.cleanup() return 0 if __name__ == '__main__': sys.exit(main())