Update
This commit is contained in:
@@ -0,0 +1,655 @@
|
||||
"""
|
||||
ONNX Model Training Script for EURUSD 15-Minute Data
|
||||
MetaTrader 5
|
||||
|
||||
This script trains a neural network model for EURUSD price prediction on 15-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 MetaTrader5 as mt5
|
||||
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 EURUSD15MinTrainer:
|
||||
"""
|
||||
Trainer class for creating ONNX models from EURUSD 15-minute MT5 data.
|
||||
"""
|
||||
|
||||
def __init__(self, symbol: str = "EURUSD", timeframe: int = mt5.TIMEFRAME_M15,
|
||||
lookback: int = 60, prediction_horizon: int = 1):
|
||||
"""
|
||||
Initialize the trainer.
|
||||
|
||||
Args:
|
||||
symbol: Trading symbol (default: 'EURUSD')
|
||||
timeframe: MT5 timeframe constant (default: M15)
|
||||
lookback: Number of bars to look back for prediction (default: 60 = 15 hours)
|
||||
prediction_horizon: Number of bars ahead to predict (default: 1)
|
||||
"""
|
||||
self.symbol = symbol
|
||||
self.timeframe = timeframe
|
||||
self.lookback = lookback
|
||||
self.prediction_horizon = prediction_horizon
|
||||
|
||||
self.scaler = MinMaxScaler()
|
||||
self.model = None
|
||||
|
||||
# Initialize MT5
|
||||
if not mt5.initialize():
|
||||
raise RuntimeError(f"MT5 initialization failed: {mt5.last_error()}")
|
||||
|
||||
print(f"MT5 initialized. Connected to: {mt5.terminal_info().name}")
|
||||
|
||||
# Check if symbol is available
|
||||
symbol_info = mt5.symbol_info(self.symbol)
|
||||
if symbol_info is None:
|
||||
print(f"WARNING: Symbol {self.symbol} not found. Available symbols:")
|
||||
symbols = mt5.symbols_get()
|
||||
if symbols:
|
||||
for i, sym in enumerate(symbols[:10]): # Show first 10
|
||||
print(f" {sym.name}")
|
||||
raise ValueError(f"Symbol {self.symbol} not available in MT5")
|
||||
|
||||
if not symbol_info.visible:
|
||||
print(f"WARNING: Symbol {self.symbol} is not visible. Trying to enable...")
|
||||
if not mt5.symbol_select(self.symbol, True):
|
||||
raise ValueError(f"Failed to enable symbol {self.symbol}")
|
||||
|
||||
def fetch_data(self, start_date: datetime, end_date: datetime) -> pd.DataFrame:
|
||||
"""
|
||||
Fetch historical data from MT5.
|
||||
|
||||
Args:
|
||||
start_date: Start date for data
|
||||
end_date: End date for data
|
||||
|
||||
Returns:
|
||||
DataFrame with OHLCV data
|
||||
"""
|
||||
print(f"\nFetching {self.symbol} 15-minute data from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}...")
|
||||
print("Note: MT5 typically has 15-minute data available for longer periods than 1-minute data.")
|
||||
|
||||
# First, try to find the actual date range with data
|
||||
# Start from end_date and work backwards to find where data starts
|
||||
print("\nFinding available data range...")
|
||||
test_end = end_date
|
||||
test_start = end_date - timedelta(days=365) # Check last year first
|
||||
|
||||
test_rates = mt5.copy_rates_range(self.symbol, self.timeframe, test_start, test_end)
|
||||
if test_rates is None or len(test_rates) == 0:
|
||||
# Try even more recent
|
||||
test_start = end_date - timedelta(days=30)
|
||||
test_rates = mt5.copy_rates_range(self.symbol, self.timeframe, test_start, test_end)
|
||||
|
||||
if test_rates is None or len(test_rates) == 0:
|
||||
raise ValueError(f"No 15-minute data available for {self.symbol}. Make sure:")
|
||||
print(" 1. Historical data is downloaded in MT5 (Tools → History Center)")
|
||||
print(" 2. The symbol is available and enabled")
|
||||
print(" 3. You have 15-minute data for the requested period")
|
||||
|
||||
# Find actual data range by checking from end backwards
|
||||
actual_start = end_date
|
||||
chunk_size_days = 30
|
||||
|
||||
# Work backwards to find where data actually starts
|
||||
print("Scanning backwards to find data availability...")
|
||||
for days_back in range(0, 365*2, chunk_size_days): # Check up to 2 years back
|
||||
check_start = end_date - timedelta(days=days_back + chunk_size_days)
|
||||
check_end = end_date - timedelta(days=days_back)
|
||||
test_rates = mt5.copy_rates_range(self.symbol, self.timeframe, check_start, check_end)
|
||||
if test_rates is not None and len(test_rates) > 10: # More than just 1 bar
|
||||
actual_start = check_start
|
||||
print(f" Found data starting from: {actual_start.strftime('%Y-%m-%d')}")
|
||||
break
|
||||
|
||||
# Now fetch all available data
|
||||
print(f"\nFetching available data from {actual_start.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}...")
|
||||
all_rates = []
|
||||
current_start = actual_start
|
||||
|
||||
# Fetch in chunks (1 month at a time for 15-minute data)
|
||||
chunk_days = 30 # 1 month chunks for 15-minute data
|
||||
|
||||
chunks_with_data = 0
|
||||
chunks_without_data = 0
|
||||
|
||||
while current_start < end_date:
|
||||
chunk_end = min(current_start + timedelta(days=chunk_days), end_date)
|
||||
|
||||
rates = mt5.copy_rates_range(self.symbol, self.timeframe, current_start, chunk_end)
|
||||
|
||||
if rates is None or len(rates) == 0:
|
||||
chunks_without_data += 1
|
||||
current_start = chunk_end
|
||||
continue
|
||||
|
||||
# Skip chunks with only 1 bar (likely invalid/placeholder data)
|
||||
if len(rates) <= 1:
|
||||
chunks_without_data += 1
|
||||
current_start = chunk_end
|
||||
continue
|
||||
|
||||
chunks_with_data += 1
|
||||
|
||||
# MT5 returns structured numpy array - convert properly
|
||||
if isinstance(rates, np.ndarray) and rates.dtype.names:
|
||||
# Structured array - convert each row to dict
|
||||
for row in rates:
|
||||
all_rates.append({name: row[name] for name in rates.dtype.names})
|
||||
else:
|
||||
# Already a list or regular array
|
||||
all_rates.extend(rates if isinstance(rates, list) else rates.tolist())
|
||||
|
||||
if chunks_with_data % 10 == 0:
|
||||
print(f" Progress: {chunks_with_data} chunks with data, {len(all_rates)} total bars")
|
||||
|
||||
current_start = chunk_end
|
||||
|
||||
print(f"\nData fetch complete: {chunks_with_data} chunks with data, {chunks_without_data} chunks skipped")
|
||||
|
||||
if len(all_rates) == 0:
|
||||
raise ValueError(f"No data available for {self.symbol} in the specified date range")
|
||||
|
||||
# Convert to DataFrame
|
||||
df = pd.DataFrame(all_rates)
|
||||
|
||||
# MT5 returns 'time' field - convert from Unix timestamp to datetime
|
||||
if 'time' in df.columns:
|
||||
df['time'] = pd.to_datetime(df['time'], unit='s')
|
||||
df.set_index('time', inplace=True)
|
||||
else:
|
||||
# Debug: print available columns
|
||||
print(f"Available columns: {df.columns.tolist()}")
|
||||
print(f"First row sample: {df.iloc[0] if len(df) > 0 else 'Empty'}")
|
||||
raise ValueError(f"Could not find 'time' column in MT5 data. Available columns: {df.columns.tolist()}")
|
||||
|
||||
# Remove duplicates
|
||||
df = df[~df.index.duplicated(keep='first')]
|
||||
df = df.sort_index()
|
||||
|
||||
print(f"\nTotal 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, sl_atr, tp_atr) - shape (n_samples, 3)
|
||||
|
||||
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])
|
||||
# Target is already aligned with data index
|
||||
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 with multi-output (price change, SL, TP).
|
||||
|
||||
Args:
|
||||
input_shape: Shape of input data (lookback, features)
|
||||
|
||||
Returns:
|
||||
Compiled Keras model
|
||||
"""
|
||||
print(f"\nBuilding multi-output model with input shape: {input_shape}")
|
||||
|
||||
# Shared LSTM layers
|
||||
inputs = layers.Input(shape=input_shape)
|
||||
x = layers.LSTM(128, return_sequences=True)(inputs)
|
||||
x = layers.Dropout(0.3)(x)
|
||||
x = layers.LSTM(64, return_sequences=True)(x)
|
||||
x = layers.Dropout(0.3)(x)
|
||||
x = layers.LSTM(32)(x)
|
||||
x = layers.Dropout(0.3)(x)
|
||||
|
||||
# Shared dense layers
|
||||
shared = layers.Dense(32, activation='relu')(x)
|
||||
shared = layers.Dense(16, activation='relu')(shared)
|
||||
|
||||
# Separate outputs
|
||||
# Output 1: Price change percentage
|
||||
price_change = layers.Dense(8, activation='relu')(shared)
|
||||
price_change = layers.Dense(1, name='price_change')(price_change)
|
||||
|
||||
# Output 2: Stop Loss (ATR multiples)
|
||||
sl_output = layers.Dense(8, activation='relu')(shared)
|
||||
sl_output = layers.Dense(1, activation='relu', name='sl_atr')(sl_output) # ReLU to ensure positive
|
||||
|
||||
# Output 3: Take Profit (ATR multiples)
|
||||
tp_output = layers.Dense(8, activation='relu')(shared)
|
||||
tp_output = layers.Dense(1, activation='relu', name='tp_atr')(tp_output) # ReLU to ensure positive
|
||||
|
||||
model = keras.Model(inputs=inputs, outputs=[price_change, sl_output, tp_output])
|
||||
|
||||
# Compile with separate losses and metrics for each output
|
||||
model.compile(
|
||||
optimizer=keras.optimizers.Adam(learning_rate=0.0005),
|
||||
loss={
|
||||
'price_change': 'mse',
|
||||
'sl_atr': 'mse',
|
||||
'tp_atr': 'mse'
|
||||
},
|
||||
loss_weights={
|
||||
'price_change': 1.0,
|
||||
'sl_atr': 0.5, # Lower weight for SL/TP
|
||||
'tp_atr': 0.5
|
||||
},
|
||||
metrics={
|
||||
'price_change': ['mae'],
|
||||
'sl_atr': ['mae'],
|
||||
'tp_atr': ['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 targets: price change, optimal SL, and optimal TP
|
||||
# Calculate future price change and optimal SL/TP by looking ahead
|
||||
close_prices = feature_df['close'].values
|
||||
high_prices = feature_df['high'].values
|
||||
low_prices = feature_df['low'].values
|
||||
atr_values = feature_df['atr'].values
|
||||
|
||||
# Look ahead window for calculating optimal SL/TP (e.g., 20 bars = 5 hours for M15)
|
||||
look_ahead_bars = 20
|
||||
|
||||
target_price_change = []
|
||||
target_sl = [] # SL in ATR multiples
|
||||
target_tp = [] # TP in ATR multiples
|
||||
|
||||
for i in range(len(close_prices)):
|
||||
if i + self.prediction_horizon < len(close_prices):
|
||||
current_price = close_prices[i]
|
||||
current_atr = atr_values[i] if atr_values[i] > 0 else current_price * 0.001
|
||||
|
||||
# Calculate price change
|
||||
future_price = close_prices[i + self.prediction_horizon]
|
||||
price_change_pct = (future_price - current_price) / current_price if current_price > 0 else 0.0
|
||||
|
||||
# Calculate optimal SL/TP by looking ahead
|
||||
# For BUY signals (positive price change expected)
|
||||
if price_change_pct > 0:
|
||||
# Look ahead to find maximum adverse and favorable excursions
|
||||
max_adverse = 0.0 # Maximum price drop (SL would be hit)
|
||||
max_favorable = 0.0 # Maximum price rise (TP could be set)
|
||||
|
||||
for j in range(i + 1, min(i + look_ahead_bars + 1, len(close_prices))):
|
||||
# Maximum adverse: lowest low below entry
|
||||
adverse_move = (current_price - low_prices[j]) / current_price
|
||||
max_adverse = max(max_adverse, adverse_move)
|
||||
|
||||
# Maximum favorable: highest high above entry
|
||||
favorable_move = (high_prices[j] - current_price) / current_price
|
||||
max_favorable = max(max_favorable, favorable_move)
|
||||
|
||||
# Optimal SL: Use 1.2x of maximum adverse excursion (slightly wider to avoid noise)
|
||||
# Convert to ATR multiples
|
||||
optimal_sl_pct = max_adverse * 1.2 if max_adverse > 0 else 0.005 # Default 0.5% if no adverse move
|
||||
optimal_sl_atr = optimal_sl_pct * current_price / current_atr if current_atr > 0 else 1.5
|
||||
optimal_sl_atr = max(0.5, min(optimal_sl_atr, 5.0)) # Clamp between 0.5 and 5.0 ATR
|
||||
|
||||
# Optimal TP: Use 0.6x of maximum favorable excursion (conservative)
|
||||
# Ensure minimum 1.5x risk/reward ratio
|
||||
optimal_tp_pct = max_favorable * 0.6 if max_favorable > 0 else optimal_sl_pct * 1.5
|
||||
optimal_tp_pct = max(optimal_sl_pct * 1.5, optimal_tp_pct) # At least 1.5x SL
|
||||
optimal_tp_atr = optimal_tp_pct * current_price / current_atr if current_atr > 0 else 2.0
|
||||
optimal_tp_atr = max(1.0, min(optimal_tp_atr, 10.0)) # Clamp between 1.0 and 10.0 ATR
|
||||
|
||||
# For SELL signals (negative price change expected)
|
||||
else:
|
||||
max_adverse = 0.0 # Maximum price rise (SL would be hit for sell)
|
||||
max_favorable = 0.0 # Maximum price drop (TP could be set)
|
||||
|
||||
for j in range(i + 1, min(i + look_ahead_bars + 1, len(close_prices))):
|
||||
# Maximum adverse: highest high above entry
|
||||
adverse_move = (high_prices[j] - current_price) / current_price
|
||||
max_adverse = max(max_adverse, adverse_move)
|
||||
|
||||
# Maximum favorable: lowest low below entry
|
||||
favorable_move = (current_price - low_prices[j]) / current_price
|
||||
max_favorable = max(max_favorable, favorable_move)
|
||||
|
||||
# Optimal SL: Use 1.2x of maximum adverse excursion
|
||||
optimal_sl_pct = max_adverse * 1.2 if max_adverse > 0 else 0.005
|
||||
optimal_sl_atr = optimal_sl_pct * current_price / current_atr if current_atr > 0 else 1.5
|
||||
optimal_sl_atr = max(0.5, min(optimal_sl_atr, 5.0))
|
||||
|
||||
# Optimal TP: Use 0.6x of maximum favorable excursion
|
||||
optimal_tp_pct = max_favorable * 0.6 if max_favorable > 0 else optimal_sl_pct * 1.5
|
||||
optimal_tp_pct = max(optimal_sl_pct * 1.5, optimal_tp_pct)
|
||||
optimal_tp_atr = optimal_tp_pct * current_price / current_atr if current_atr > 0 else 2.0
|
||||
optimal_tp_atr = max(1.0, min(optimal_tp_atr, 10.0))
|
||||
|
||||
target_price_change.append(price_change_pct)
|
||||
target_sl.append(optimal_sl_atr)
|
||||
target_tp.append(optimal_tp_atr)
|
||||
else:
|
||||
target_price_change.append(0.0)
|
||||
target_sl.append(1.5) # Default SL
|
||||
target_tp.append(2.0) # Default TP
|
||||
|
||||
# Create DataFrame with multiple targets
|
||||
target_df = pd.DataFrame({
|
||||
'price_change': target_price_change,
|
||||
'sl_atr': target_sl,
|
||||
'tp_atr': target_tp
|
||||
}, index=feature_df.index)
|
||||
|
||||
# Align targets with features
|
||||
valid_idx = ~(target_df.isna().any(axis=1) | feature_df.isna().any(axis=1))
|
||||
feature_df = feature_df[valid_idx]
|
||||
target_df = target_df[valid_idx]
|
||||
|
||||
print(f"\nValid samples after alignment: {len(feature_df)}")
|
||||
print(f"Target statistics:")
|
||||
print(f" Price Change: mean={target_df['price_change'].mean():.6f}, std={target_df['price_change'].std():.6f}")
|
||||
print(f" SL (ATR): mean={target_df['sl_atr'].mean():.2f}, std={target_df['sl_atr'].std():.2f}")
|
||||
print(f" TP (ATR): mean={target_df['tp_atr'].mean():.2f}, std={target_df['tp_atr'].std():.2f}")
|
||||
|
||||
# Normalize features
|
||||
print("\nNormalizing features...")
|
||||
feature_array = self.scaler.fit_transform(feature_df.values)
|
||||
|
||||
# Prepare multi-output target
|
||||
target_array = target_df[['price_change', 'sl_atr', 'tp_atr']].values
|
||||
|
||||
# Create sequences
|
||||
X, y = self.create_sequences(feature_array, target_array)
|
||||
|
||||
# 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)
|
||||
|
||||
# Prepare multi-output targets for training
|
||||
y_train_dict = {
|
||||
'price_change': y_train[:, 0],
|
||||
'sl_atr': y_train[:, 1],
|
||||
'tp_atr': y_train[:, 2]
|
||||
}
|
||||
y_val_dict = {
|
||||
'price_change': y_val[:, 0],
|
||||
'sl_atr': y_val[:, 1],
|
||||
'tp_atr': y_val[:, 2]
|
||||
}
|
||||
|
||||
# Train model
|
||||
print(f"\nTraining model for {epochs} epochs...")
|
||||
history = self.model.fit(
|
||||
X_train, y_train_dict,
|
||||
batch_size=batch_size,
|
||||
epochs=epochs,
|
||||
validation_data=(X_val, y_val_dict),
|
||||
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_eval = self.model.evaluate(X_train, y_train_dict, verbose=0)
|
||||
val_eval = self.model.evaluate(X_val, y_val_dict, verbose=0)
|
||||
|
||||
# Multi-output model returns list of losses/metrics
|
||||
print(f"Train - Total Loss: {train_eval[0]:.6f}")
|
||||
print(f" Price Change Loss: {train_eval[1]:.6f}, MAE: {train_eval[4]:.6f}")
|
||||
print(f" SL Loss: {train_eval[2]:.6f}, MAE: {train_eval[5]:.6f}")
|
||||
print(f" TP Loss: {train_eval[3]:.6f}, MAE: {train_eval[6]:.6f}")
|
||||
print(f"Val - Total Loss: {val_eval[0]:.6f}")
|
||||
print(f" Price Change Loss: {val_eval[1]:.6f}, MAE: {val_eval[4]:.6f}")
|
||||
print(f" SL Loss: {val_eval[2]:.6f}, MAE: {val_eval[5]:.6f}")
|
||||
print(f" TP Loss: {val_eval[3]:.6f}, MAE: {val_eval[6]:.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 MT5 connection."""
|
||||
mt5.shutdown()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function."""
|
||||
print("="*60)
|
||||
print("EURUSD 15-Minute ONNX Model Training")
|
||||
print("="*60)
|
||||
|
||||
# Training parameters
|
||||
symbol = "EURUSD"
|
||||
timeframe = mt5.TIMEFRAME_M15
|
||||
lookback = 60 # 60 bars = 15 hours of history
|
||||
epochs = 50
|
||||
batch_size = 64
|
||||
|
||||
# Date range: 1990 to 2026
|
||||
start_date = datetime(1990, 1, 1)
|
||||
end_date = datetime(2026, 1, 1)
|
||||
|
||||
# Output paths
|
||||
output_dir = "models"
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
model_path = os.path.join(output_dir, f"{symbol}_M15_model.onnx")
|
||||
|
||||
trainer = None
|
||||
try:
|
||||
# Create trainer
|
||||
trainer = EURUSD15MinTrainer(
|
||||
symbol=symbol,
|
||||
timeframe=timeframe,
|
||||
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())
|
||||
Reference in New Issue
Block a user