This commit is contained in:
zhutoutoutousan
2026-04-01 05:40:17 +02:00
parent 28e7daf1e3
commit 842a2f8fac
53 changed files with 9222 additions and 962 deletions
+620
View File
@@ -0,0 +1,620 @@
//+------------------------------------------------------------------+
//| BTCUSD_M1_EA.mq5 |
//| Copyright 2025, MetaQuotes Ltd. |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link "https://www.mql5.com"
#property version "1.00"
#property description "Expert Advisor using ONNX model for BTCUSD 1-minute price prediction"
#property description "Based on: https://www.mql5.com/en/docs/onnx/onnx_test"
#include <Trade\Trade.mqh>
//--- Resource: Embed ONNX model in EA
// Based on: https://www.mql5.com/en/docs/onnx/onnx_test
// Path is relative to MQL5 directory (not starting with \\Files\\)
#resource "Files\\BTCUSD_M1_model.onnx" as uchar ExtModel[]
//--- Input parameters
input group "ONNX Model Settings"
input string InpModelPath = ""; // ONNX Model Path (leave empty to use embedded resource)
input int InpLookback = 60; // Lookback Period (bars)
input bool InpUsePrediction = true; // Use Model Prediction
input group "Trading Settings"
input double InpLotSize = 0.01; // Lot Size
input int InpMagicNumber = 123456; // Magic Number
input int InpSlippage = 3; // Slippage (points)
input bool InpUsePredictedSLTP = true; // Use Predicted SL/TP (based on prediction & volatility)
input int InpStopLoss = 50; // Stop Loss (pips) - used if InpUsePredictedSLTP=false
input int InpTakeProfit = 100; // Take Profit (pips) - used if InpUsePredictedSLTP=false
input double InpSLMultiplier = 1.5; // SL Multiplier (ATR-based, e.g., 1.5 = 1.5x ATR)
input double InpTPMultiplier = 2.0; // TP Multiplier (ATR-based, e.g., 2.0 = 2x ATR)
input double InpMinSLATR = 0.5; // Minimum SL (ATR multiplier)
input double InpMinTPATR = 1.0; // Minimum TP (ATR multiplier)
input group "Prediction Settings"
input double InpPredictionThreshold = 0.00005; // Min Prediction Change (0.005% as decimal, e.g., 0.00005 = 0.005%)
input bool InpUseConfidence = true; // Use Confidence Filter
input double InpMinConfidence = 0.1; // Minimum Confidence (0.1 = 10%)
//--- Global variables
CTrade trade;
long onnx_handle = INVALID_HANDLE;
datetime last_bar_time = 0;
double last_prediction = 0.0;
double last_confidence = 0.0;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
// Set trade parameters
trade.SetExpertMagicNumber(InpMagicNumber);
trade.SetDeviationInPoints(InpSlippage);
trade.SetTypeFilling(ORDER_FILLING_FOK);
// Check symbol
if(_Symbol != "BTCUSD" && _Symbol != "BTCUSD#")
{
Print("WARNING: This EA is designed for BTCUSD. Current symbol: ", _Symbol);
}
// Check timeframe
if(_Period != PERIOD_M1)
{
Print("WARNING: This EA is designed for M1 timeframe. Current timeframe: ", EnumToString(_Period));
}
// Load ONNX model
// Based on: https://www.mql5.com/en/docs/onnx/onnx_test
Print("Loading ONNX model from embedded resource...");
// Create model from resource buffer
onnx_handle = OnnxCreateFromBuffer(ExtModel, ONNX_DEBUG_LOGS);
if(onnx_handle == INVALID_HANDLE)
{
int error = GetLastError();
Print("ERROR: Failed to create ONNX model from resource. Error: ", error);
Print("Make sure the model file exists at: MQL5\\Files\\BTCUSD_M1_model.onnx");
Print("Then recompile the EA to embed it as a resource.");
return(INIT_FAILED);
}
// Set input shape - per MQL5 documentation
const long ExtInputShape[] = {1, InpLookback, 13}; // batch=1, lookback bars, 13 features
if(!OnnxSetInputShape(onnx_handle, 0, ExtInputShape))
{
Print("OnnxSetInputShape failed, error ", GetLastError());
OnnxRelease(onnx_handle);
return(INIT_FAILED);
}
// Set output shape - per MQL5 documentation
const long ExtOutputShape[] = {1, 1}; // batch=1, single output value
if(!OnnxSetOutputShape(onnx_handle, 0, ExtOutputShape))
{
Print("OnnxSetOutputShape failed, error ", GetLastError());
OnnxRelease(onnx_handle);
return(INIT_FAILED);
}
// Get model info
long input_count = OnnxGetInputCount(onnx_handle);
long output_count = OnnxGetOutputCount(onnx_handle);
Print("ONNX Model loaded successfully");
Print(" Inputs: ", input_count);
Print(" Outputs: ", output_count);
if(input_count > 0)
{
string input_name = OnnxGetInputName(onnx_handle, 0);
Print(" Input name: ", input_name);
}
if(output_count > 0)
{
string output_name = OnnxGetOutputName(onnx_handle, 0);
Print(" Output name: ", output_name);
}
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
// Release ONNX model
if(onnx_handle != INVALID_HANDLE)
{
OnnxRelease(onnx_handle);
Print("ONNX model released");
}
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// Check if new bar
datetime current_bar_time = iTime(_Symbol, PERIOD_CURRENT, 0);
if(current_bar_time == last_bar_time)
{
return; // Still the same bar
}
last_bar_time = current_bar_time;
// Check if we should use prediction
if(!InpUsePrediction)
{
return;
}
// Prepare input data
float input_data[];
if(!PrepareInputData(input_data))
{
Print("ERROR: Failed to prepare input data");
return;
}
// Check if input data is valid
if(ArraySize(input_data) != InpLookback * 13)
{
Print("ERROR: Input data size mismatch. Expected: ", InpLookback * 13, ", Got: ", ArraySize(input_data));
return;
}
// Convert flat array to matrixf for OnnxRun
// Shape: [lookback, features] = [60, 13] - batch dimension is added automatically
matrixf input_matrix;
input_matrix.Resize(InpLookback, 13);
// Fill matrix from flat array
int idx = 0;
for(int i = 0; i < InpLookback; i++)
{
for(int j = 0; j < 13; j++)
{
if(idx >= ArraySize(input_data))
{
Print("ERROR: Index out of bounds when filling matrix. idx=", idx, ", array size=", ArraySize(input_data));
return;
}
input_matrix[i][j] = input_data[idx++];
}
}
// Verify matrix is not empty
if(input_matrix.Rows() == 0 || input_matrix.Cols() == 0)
{
Print("ERROR: Input matrix is empty. Rows: ", input_matrix.Rows(), ", Cols: ", input_matrix.Cols());
return;
}
// Run ONNX model - use matrixf and vectorf per MQL5 documentation
vectorf output_vector(1);
if(!RunONNXModel(input_matrix, output_vector))
{
Print("ERROR: Failed to run ONNX model");
return;
}
// Get prediction
if(output_vector.Size() == 0)
{
Print("ERROR: Empty output from ONNX model");
return;
}
// Model now predicts price change percentage directly (e.g., -0.003 = -0.3%)
double predicted_change_pct = output_vector[0];
double current_price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
// Check if prediction is percentage (between -1 and 1) or absolute price (old format)
double price_change_pct;
double predicted_price;
if(MathAbs(predicted_change_pct) < 1.0)
{
// New format: percentage (e.g., -0.003 = -0.3%)
price_change_pct = predicted_change_pct * 100.0; // Convert to percentage
predicted_price = current_price * (1.0 + predicted_change_pct); // Calculate predicted price
}
else
{
// Old format: absolute price
predicted_price = predicted_change_pct;
double price_change = predicted_price - current_price;
price_change_pct = (price_change / current_price) * 100.0;
}
// Calculate confidence (for percentage predictions: 0.001 = 0.1% = 10% confidence)
double confidence;
if(MathAbs(price_change_pct) < 1.0)
{
// It's a decimal percentage (e.g., 0.001 = 0.1%)
confidence = MathMin(MathAbs(predicted_change_pct) / 0.01, 1.0); // 0.01 = 1% = 100% confidence
}
else
{
// It's already in percentage form
confidence = MathMin(MathAbs(price_change_pct) / 1.0, 1.0);
}
last_prediction = predicted_price;
last_confidence = confidence;
// Calculate ATR for dynamic SL/TP
double atr_value = 0.0;
double atr_array[];
ArraySetAsSeries(atr_array, true);
int atr_handle = iATR(_Symbol, PERIOD_CURRENT, 14);
if(atr_handle != INVALID_HANDLE)
{
if(CopyBuffer(atr_handle, 0, 0, 1, atr_array) > 0)
{
atr_value = atr_array[0];
}
IndicatorRelease(atr_handle);
}
// Calculate predicted SL/TP based on prediction, confidence, and volatility
double predicted_sl = 0.0;
double predicted_tp = 0.0;
if(InpUsePredictedSLTP && atr_value > 0)
{
// Calculate SL/TP based on ATR, prediction, and confidence
double predicted_move = MathAbs(predicted_price - current_price);
// SL: Based on ATR and confidence
// Higher confidence = tighter SL, lower confidence = wider SL
double sl_atr_mult = InpSLMultiplier / MathMax(confidence, 0.1);
sl_atr_mult = MathMax(sl_atr_mult, InpMinSLATR);
predicted_sl = atr_value * sl_atr_mult;
// TP: Use a fraction of predicted move (not the full move)
// Take 30-50% of predicted move as TP, but ensure minimum
double tp_fraction = 0.3 + (confidence * 0.2); // 30-50% based on confidence
double tp_from_prediction = predicted_move * tp_fraction;
// Also calculate TP from ATR multiplier
double tp_from_atr = atr_value * InpTPMultiplier;
// Use the smaller of the two (more conservative)
predicted_tp = MathMin(tp_from_prediction, tp_from_atr);
predicted_tp = MathMax(predicted_tp, atr_value * InpMinTPATR); // Minimum TP
// Ensure TP is at least 1.5x SL for risk/reward
if(predicted_tp < predicted_sl * 1.5)
{
predicted_tp = predicted_sl * 1.5;
}
// Cap TP at maximum 80% of predicted move (don't be too greedy)
double max_tp = predicted_move * 0.8;
if(predicted_tp > max_tp)
{
predicted_tp = max_tp;
}
}
// Log prediction
Print("Prediction: Current=", current_price,
" Predicted Change=", price_change_pct, "%",
" Predicted Price=", predicted_price,
" Confidence=", confidence,
" ATR=", atr_value);
if(InpUsePredictedSLTP && predicted_sl > 0 && predicted_tp > 0)
{
Print(" Predicted SL=", predicted_sl, " (", predicted_sl/current_price*100, "%)",
" Predicted TP=", predicted_tp, " (", predicted_tp/current_price*100, "%)");
}
// Check if we should trade
if(!InpUseConfidence || confidence >= InpMinConfidence)
{
// Check if prediction is significant
// price_change_pct is in percentage (e.g., 5.72 = 5.72%)
// InpPredictionThreshold is in decimal (e.g., 0.00005 = 0.005%)
// Convert threshold to percentage for comparison
double threshold_pct = InpPredictionThreshold * 100.0;
double abs_change_pct = MathAbs(price_change_pct); // Already in percentage
Print("Trade Check: Change=", price_change_pct, "% Threshold=", threshold_pct, "% Confidence=", confidence);
if(abs_change_pct >= threshold_pct)
{
// Check existing position
if(PositionSelect(_Symbol))
{
// Manage existing position
ManagePosition(predicted_price, price_change_pct);
}
else
{
// Open new position based on prediction
if(price_change_pct > threshold_pct)
{
Print(">>> Opening BUY position: Change=", price_change_pct, "% Threshold=", threshold_pct, "%");
OpenBuyPosition(predicted_sl, predicted_tp);
}
else if(price_change_pct < -threshold_pct)
{
Print(">>> Opening SELL position: Change=", price_change_pct, "% Threshold=", threshold_pct, "%");
OpenSellPosition(predicted_sl, predicted_tp);
}
}
}
else
{
Print("Prediction below threshold: Change=", price_change_pct, "% < Threshold=", threshold_pct, "%");
}
}
else
{
Print("Confidence too low: ", confidence, " < ", InpMinConfidence);
}
}
//+------------------------------------------------------------------+
//| Prepare input data for ONNX model |
//+------------------------------------------------------------------+
bool PrepareInputData(float &input_array[])
{
int lookback = InpLookback;
int features = 13; // OHLC(4) + volume(1) + RSI(1) + EMA20(1) + EMA50(1) + ATR(1) + price_change(1) + high_low_ratio(1) + volume_ma(1) + volume_ratio(1) = 13
ArrayResize(input_array, lookback * features);
ArrayInitialize(input_array, 0.0);
// Get historical data
double open[], high[], low[], close[];
long volume[]; // CopyTickVolume requires long[] not double[]
ArraySetAsSeries(open, true);
ArraySetAsSeries(high, true);
ArraySetAsSeries(low, true);
ArraySetAsSeries(close, true);
ArraySetAsSeries(volume, true);
int copied_open = CopyOpen(_Symbol, PERIOD_CURRENT, 0, lookback + 50, open);
if(copied_open < lookback)
{
Print("ERROR: CopyOpen failed. Got ", copied_open, " bars, need ", lookback);
return false;
}
int copied_high = CopyHigh(_Symbol, PERIOD_CURRENT, 0, lookback + 50, high);
if(copied_high < lookback)
{
Print("ERROR: CopyHigh failed. Got ", copied_high, " bars, need ", lookback);
return false;
}
int copied_low = CopyLow(_Symbol, PERIOD_CURRENT, 0, lookback + 50, low);
if(copied_low < lookback)
{
Print("ERROR: CopyLow failed. Got ", copied_low, " bars, need ", lookback);
return false;
}
int copied_close = CopyClose(_Symbol, PERIOD_CURRENT, 0, lookback + 50, close);
if(copied_close < lookback)
{
Print("ERROR: CopyClose failed. Got ", copied_close, " bars, need ", lookback);
return false;
}
int copied_volume = CopyTickVolume(_Symbol, PERIOD_CURRENT, 0, lookback + 50, volume);
if(copied_volume < lookback)
{
Print("ERROR: CopyTickVolume failed. Got ", copied_volume, " bars, need ", lookback);
return false;
}
// Calculate indicators
double rsi[], ema20[], ema50[], atr[];
ArraySetAsSeries(rsi, true);
ArraySetAsSeries(ema20, true);
ArraySetAsSeries(ema50, true);
ArraySetAsSeries(atr, true);
// Calculate RSI
int rsi_handle = iRSI(_Symbol, PERIOD_CURRENT, 14, PRICE_CLOSE);
if(rsi_handle == INVALID_HANDLE) return false;
if(CopyBuffer(rsi_handle, 0, 0, lookback + 50, rsi) < lookback)
{
IndicatorRelease(rsi_handle);
return false;
}
IndicatorRelease(rsi_handle);
// Calculate EMAs
int ema20_handle = iMA(_Symbol, PERIOD_CURRENT, 20, 0, MODE_EMA, PRICE_CLOSE);
int ema50_handle = iMA(_Symbol, PERIOD_CURRENT, 50, 0, MODE_EMA, PRICE_CLOSE);
if(ema20_handle == INVALID_HANDLE || ema50_handle == INVALID_HANDLE) return false;
if(CopyBuffer(ema20_handle, 0, 0, lookback + 50, ema20) < lookback ||
CopyBuffer(ema50_handle, 0, 0, lookback + 50, ema50) < lookback)
{
IndicatorRelease(ema20_handle);
IndicatorRelease(ema50_handle);
return false;
}
IndicatorRelease(ema20_handle);
IndicatorRelease(ema50_handle);
// Calculate ATR
int atr_handle = iATR(_Symbol, PERIOD_CURRENT, 14);
if(atr_handle == INVALID_HANDLE) return false;
if(CopyBuffer(atr_handle, 0, 0, lookback + 50, atr) < lookback)
{
IndicatorRelease(atr_handle);
return false;
}
IndicatorRelease(atr_handle);
// Calculate volume MA for normalization
double volume_ma[];
ArraySetAsSeries(volume_ma, true);
ArrayResize(volume_ma, lookback);
ArrayInitialize(volume_ma, 0.0);
// Calculate volume MA (20-period rolling average)
for(int j = 0; j < lookback; j++)
{
double sum = 0.0;
int count = 0;
for(int k = j; k < j + 20 && k < ArraySize(volume); k++)
{
sum += (double)volume[k];
count++;
}
volume_ma[j] = count > 0 ? sum / count : (double)volume[j];
}
// Prepare features - MUST match Python training exactly (13 features)
int idx = 0;
for(int i = 0; i < lookback; i++)
{
// Feature 1-4: OHLC
input_array[idx++] = (float)open[i];
input_array[idx++] = (float)high[i];
input_array[idx++] = (float)low[i];
input_array[idx++] = (float)close[i];
// Feature 5: Volume (normalized by 1,000,000)
input_array[idx++] = (float)((double)volume[i] / 1000000.0);
// Feature 6: RSI (normalized by 100)
input_array[idx++] = (float)(rsi[i] / 100.0);
// Feature 7: EMA20 normalized difference
input_array[idx++] = (float)((ema20[i] - close[i]) / close[i]);
// Feature 8: EMA50 normalized difference
input_array[idx++] = (float)((ema50[i] - close[i]) / close[i]);
// Feature 9: ATR normalized
input_array[idx++] = (float)(atr[i] / close[i]);
// Feature 10: Price change (percentage)
double price_change = i > 0 ? (close[i] - close[i+1]) / close[i+1] : 0.0;
input_array[idx++] = (float)price_change;
// Feature 11: High/Low ratio
input_array[idx++] = (float)(high[i] / low[i]);
// Feature 12: Volume MA (normalized by 1,000,000)
input_array[idx++] = (float)(volume_ma[i] / 1000000.0);
// Feature 13: Volume ratio
double vol_ratio = volume_ma[i] > 0 ? (double)volume[i] / volume_ma[i] : 1.0;
input_array[idx++] = (float)vol_ratio;
}
return true;
}
//+------------------------------------------------------------------+
//| Run ONNX model |
//+------------------------------------------------------------------+
bool RunONNXModel(matrixf &input_matrix, vectorf &output_vector)
{
if(onnx_handle == INVALID_HANDLE)
return false;
// Run model - shapes are already set in OnInit per MQL5 documentation
// Based on: https://www.mql5.com/en/docs/onnx/onnx_test
// OnnxRun expects matrixf and vectorf, not flat arrays
if(!OnnxRun(onnx_handle, ONNX_DEBUG_LOGS | ONNX_NO_CONVERSION, input_matrix, output_vector))
{
Print("ERROR: Failed to run ONNX model. Error: ", GetLastError());
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Open buy position |
//+------------------------------------------------------------------+
void OpenBuyPosition(double predicted_sl = 0.0, double predicted_tp = 0.0)
{
double price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double sl = 0.0;
double tp = 0.0;
if(InpUsePredictedSLTP && predicted_sl > 0 && predicted_tp > 0)
{
// Use predicted SL/TP
sl = price - predicted_sl;
tp = price + predicted_tp;
Print("Using predicted SL/TP: SL=", sl, " TP=", tp);
}
else
{
// Use fixed SL/TP from input parameters
sl = InpStopLoss > 0 ? price - InpStopLoss * _Point * 10 : 0;
tp = InpTakeProfit > 0 ? price + InpTakeProfit * _Point * 10 : 0;
}
if(trade.Buy(InpLotSize, _Symbol, price, sl, tp, "ONNX Buy Signal"))
{
Print("Buy order opened. Ticket: ", trade.ResultOrder(), " Price: ", price, " SL: ", sl, " TP: ", tp);
}
else
{
Print("Failed to open buy order. Error: ", trade.ResultRetcodeDescription());
}
}
//+------------------------------------------------------------------+
//| Open sell position |
//+------------------------------------------------------------------+
void OpenSellPosition(double predicted_sl = 0.0, double predicted_tp = 0.0)
{
double price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double sl = 0.0;
double tp = 0.0;
if(InpUsePredictedSLTP && predicted_sl > 0 && predicted_tp > 0)
{
// Use predicted SL/TP
sl = price + predicted_sl;
tp = price - predicted_tp;
Print("Using predicted SL/TP: SL=", sl, " TP=", tp);
}
else
{
// Use fixed SL/TP from input parameters
sl = InpStopLoss > 0 ? price + InpStopLoss * _Point * 10 : 0;
tp = InpTakeProfit > 0 ? price - InpTakeProfit * _Point * 10 : 0;
}
if(trade.Sell(InpLotSize, _Symbol, price, sl, tp, "ONNX Sell Signal"))
{
Print("Sell order opened. Ticket: ", trade.ResultOrder(), " Price: ", price, " SL: ", sl, " TP: ", tp);
}
else
{
Print("Failed to open sell order. Error: ", trade.ResultRetcodeDescription());
}
}
//+------------------------------------------------------------------+
//| Manage existing position |
//+------------------------------------------------------------------+
void ManagePosition(double predicted_price, double price_change_pct)
{
if(!PositionSelect(_Symbol))
return;
// Simple position management - can be enhanced
// For now, just log the position status
double position_profit = PositionGetDouble(POSITION_PROFIT);
Print("Position exists. Profit: ", position_profit, " Predicted change: ", price_change_pct, "%");
}
+68
View File
@@ -0,0 +1,68 @@
# BTCUSD 1-Minute ONNX Model Training
This directory contains the training script for a BTCUSD price prediction model using 1-minute timeframe data from 2017 to 2026.
## Requirements
```bash
pip install yfinance tensorflow scikit-learn pandas numpy tf2onnx onnx tqdm
```
## Usage
1. **Run the training script:**
```bash
cd ai/btcusd1min
python main.py
```
**Note:** yfinance 1-minute data is limited to the last 7 days. For longer historical training, the script will use the most recent available data.
## Configuration
The script is configured with:
- **Symbol**: BTCUSD
- **Timeframe**: M1 (1 minute)
- **Lookback**: 60 bars (60 minutes of history)
- **Date Range**: 2017-01-01 to 2026-01-01
- **Model Architecture**: LSTM with 3 layers (128, 64, 32 units)
- **Epochs**: 50 (with early stopping)
- **Batch Size**: 64
## Output
The script will create:
- `models/BTCUSD_M1_model.onnx` - The trained ONNX model
- `models/BTCUSD_M1_model_scaler.pkl` - The MinMaxScaler used for normalization
## Model Features
The model uses 13 features:
1. Open
2. High
3. Low
4. Close
5. Tick Volume
6. RSI (14 period)
7. EMA 20
8. EMA 50
9. ATR (14 period)
10. Price Change (percentage)
11. High/Low Ratio
12. Volume MA (20 period)
13. Volume Ratio
## Model Output
The model predicts the **price change percentage** for the next bar (1 minute ahead).
## Notes
- Training on 9 years of 1-minute data will take significant time and memory
- The script fetches data in 3-month chunks to manage memory
- Early stopping and learning rate reduction are enabled to prevent overfitting
- The model uses dropout (0.3) for regularization
## Using the Model in MQL5
After training, copy the ONNX model to your MT5 `MQL5/Files/` directory and use it in an Expert Advisor similar to the XAUUSD H1 EA.
+455
View File
@@ -0,0 +1,455 @@
"""
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())
Binary file not shown.
Binary file not shown.
+231 -62
View File
@@ -11,9 +11,14 @@
#include <Trade\Trade.mqh>
//--- Resource: Embed ONNX model in EA
// Based on: https://www.mql5.com/en/docs/onnx/onnx_test
// Path is relative to MQL5 directory (not starting with \\Files\\)
#resource "Files\\XAUUSD_H1_model.onnx" as uchar ExtModel[]
//--- Input parameters
input group "ONNX Model Settings"
input string InpModelPath = "models\\XAUUSD_H1_model.onnx"; // ONNX Model Path
input string InpModelPath = ""; // ONNX Model Path (leave empty to use embedded resource)
input int InpLookback = 60; // Lookback Period (bars)
input bool InpUsePrediction = true; // Use Model Prediction
@@ -21,8 +26,13 @@ input group "Trading Settings"
input double InpLotSize = 0.01; // Lot Size
input int InpMagicNumber = 123456; // Magic Number
input int InpSlippage = 3; // Slippage (points)
input int InpStopLoss = 50; // Stop Loss (pips)
input int InpTakeProfit = 100; // Take Profit (pips)
input bool InpUsePredictedSLTP = true; // Use Predicted SL/TP (based on prediction & volatility)
input int InpStopLoss = 50; // Stop Loss (pips) - used if InpUsePredictedSLTP=false
input int InpTakeProfit = 100; // Take Profit (pips) - used if InpUsePredictedSLTP=false
input double InpSLMultiplier = 1.5; // SL Multiplier (ATR-based, e.g., 1.5 = 1.5x ATR)
input double InpTPMultiplier = 2.0; // TP Multiplier (ATR-based, e.g., 2.0 = 2x ATR)
input double InpMinSLATR = 0.5; // Minimum SL (ATR multiplier)
input double InpMinTPATR = 1.0; // Minimum TP (ATR multiplier)
input group "Prediction Settings"
input double InpPredictionThreshold = 0.00005; // Min Prediction Change (0.005% as decimal, e.g., 0.00005 = 0.005%)
@@ -47,33 +57,40 @@ int OnInit()
trade.SetTypeFilling(ORDER_FILLING_FOK);
// Load ONNX model
string model_path = InpModelPath;
// Based on: https://www.mql5.com/en/docs/onnx/onnx_test
Print("Loading ONNX model from embedded resource...");
// Convert relative path to full path
if(StringFind(model_path, "\\") == 0 || StringFind(model_path, "/") == 0)
{
// Already absolute path
}
else
{
// Relative path - prepend terminal data folder
model_path = TerminalInfoString(TERMINAL_DATA_PATH) + "\\MQL5\\Files\\" + model_path;
}
// Replace forward slashes with backslashes for Windows
StringReplace(model_path, "/", "\\");
Print("Loading ONNX model from: ", model_path);
onnx_handle = OnnxCreate(model_path, ONNX_DEFAULT);
// Create model from resource buffer
onnx_handle = OnnxCreateFromBuffer(ExtModel, ONNX_DEBUG_LOGS);
if(onnx_handle == INVALID_HANDLE)
{
Print("ERROR: Failed to load ONNX model. Error: ", GetLastError());
Print("Make sure the model file exists at: ", model_path);
int error = GetLastError();
Print("ERROR: Failed to create ONNX model from resource. Error: ", error);
Print("Make sure the model file exists at: MQL5\\Files\\XAUUSD_H1_model.onnx");
Print("Then recompile the EA to embed it as a resource.");
return(INIT_FAILED);
}
// Set input shape - per MQL5 documentation
const long ExtInputShape[] = {1, InpLookback, 13}; // batch=1, lookback bars, 13 features
if(!OnnxSetInputShape(onnx_handle, 0, ExtInputShape))
{
Print("OnnxSetInputShape failed, error ", GetLastError());
OnnxRelease(onnx_handle);
return(INIT_FAILED);
}
// Set output shape - per MQL5 documentation
const long ExtOutputShape[] = {1, 1}; // batch=1, single output value
if(!OnnxSetOutputShape(onnx_handle, 0, ExtOutputShape))
{
Print("OnnxSetOutputShape failed, error ", GetLastError());
OnnxRelease(onnx_handle);
return(INIT_FAILED);
}
// Get model info
long input_count = OnnxGetInputCount(onnx_handle);
long output_count = OnnxGetOutputCount(onnx_handle);
@@ -137,23 +154,59 @@ void OnTick()
return;
}
// Run ONNX model
float output_data[];
if(!RunONNXModel(input_data, output_data))
// Check if input data is valid
if(ArraySize(input_data) != InpLookback * 13)
{
Print("ERROR: Input data size mismatch. Expected: ", InpLookback * 13, ", Got: ", ArraySize(input_data));
return;
}
// Convert flat array to matrixf for OnnxRun
// Shape: [lookback, features] = [60, 13] - batch dimension is added automatically
matrixf input_matrix;
input_matrix.Resize(InpLookback, 13);
// Fill matrix from flat array
int idx = 0;
for(int i = 0; i < InpLookback; i++)
{
for(int j = 0; j < 13; j++)
{
if(idx >= ArraySize(input_data))
{
Print("ERROR: Index out of bounds when filling matrix. idx=", idx, ", array size=", ArraySize(input_data));
return;
}
input_matrix[i][j] = input_data[idx++];
}
}
// Verify matrix is not empty
if(input_matrix.Rows() == 0 || input_matrix.Cols() == 0)
{
Print("ERROR: Input matrix is empty. Rows: ", input_matrix.Rows(), ", Cols: ", input_matrix.Cols());
return;
}
Print("Input matrix prepared: Rows=", input_matrix.Rows(), ", Cols=", input_matrix.Cols());
// Run ONNX model - use matrixf and vectorf per MQL5 documentation
vectorf output_vector(1);
if(!RunONNXModel(input_matrix, output_vector))
{
Print("ERROR: Failed to run ONNX model");
return;
}
// Get prediction
if(ArraySize(output_data) == 0)
if(output_vector.Size() == 0)
{
Print("ERROR: Empty output from ONNX model");
return;
}
// Model now predicts price change percentage directly (e.g., -0.003 = -0.3%)
double predicted_change_pct = output_data[0];
double predicted_change_pct = output_vector[0];
double current_price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
// Check if prediction is percentage (between -1 and 1) or absolute price (old format)
@@ -190,20 +243,85 @@ void OnTick()
last_prediction = predicted_price;
last_confidence = confidence;
// Calculate ATR for dynamic SL/TP
double atr_value = 0.0;
double atr_array[];
ArraySetAsSeries(atr_array, true);
int atr_handle = iATR(_Symbol, PERIOD_CURRENT, 14);
if(atr_handle != INVALID_HANDLE)
{
if(CopyBuffer(atr_handle, 0, 0, 1, atr_array) > 0)
{
atr_value = atr_array[0];
}
IndicatorRelease(atr_handle);
}
// Calculate predicted SL/TP based on prediction, confidence, and volatility
double predicted_sl = 0.0;
double predicted_tp = 0.0;
if(InpUsePredictedSLTP && atr_value > 0)
{
// Calculate SL/TP based on ATR, prediction, and confidence
double predicted_move = MathAbs(predicted_price - current_price);
// SL: Based on ATR and confidence
// Higher confidence = tighter SL, lower confidence = wider SL
double sl_atr_mult = InpSLMultiplier / MathMax(confidence, 0.1);
sl_atr_mult = MathMax(sl_atr_mult, InpMinSLATR);
predicted_sl = atr_value * sl_atr_mult;
// TP: Use a fraction of predicted move (not the full move)
// Take 30-50% of predicted move as TP, but ensure minimum
double tp_fraction = 0.3 + (confidence * 0.2); // 30-50% based on confidence
double tp_from_prediction = predicted_move * tp_fraction;
// Also calculate TP from ATR multiplier
double tp_from_atr = atr_value * InpTPMultiplier;
// Use the smaller of the two (more conservative)
predicted_tp = MathMin(tp_from_prediction, tp_from_atr);
predicted_tp = MathMax(predicted_tp, atr_value * InpMinTPATR); // Minimum TP
// Ensure TP is at least 1.5x SL for risk/reward
if(predicted_tp < predicted_sl * 1.5)
{
predicted_tp = predicted_sl * 1.5;
}
// Cap TP at maximum 80% of predicted move (don't be too greedy)
double max_tp = predicted_move * 0.8;
if(predicted_tp > max_tp)
{
predicted_tp = max_tp;
}
}
// Log prediction
Print("Prediction: Current=", current_price,
" Predicted Change=", price_change_pct, "%",
" Predicted Price=", predicted_price,
" Confidence=", confidence);
" Confidence=", confidence,
" ATR=", atr_value);
if(InpUsePredictedSLTP && predicted_sl > 0 && predicted_tp > 0)
{
Print(" Predicted SL=", predicted_sl, " (", predicted_sl/current_price*100, "%)",
" Predicted TP=", predicted_tp, " (", predicted_tp/current_price*100, "%)");
}
// Check if we should trade
if(!InpUseConfidence || confidence >= InpMinConfidence)
{
// Check if prediction is significant
// price_change_pct is now in percentage (e.g., 0.1 = 0.1%), so compare with threshold * 100
// OR: if model outputs decimal (0.001), compare directly
double abs_change_decimal = MathAbs(predicted_change_pct); // Use raw prediction for threshold check
if(abs_change_decimal >= InpPredictionThreshold)
// price_change_pct is in percentage (e.g., 5.72 = 5.72%)
// InpPredictionThreshold is in decimal (e.g., 0.00005 = 0.005%)
// Convert threshold to percentage for comparison
double threshold_pct = InpPredictionThreshold * 100.0;
double abs_change_pct = MathAbs(price_change_pct); // Already in percentage
Print("Trade Check: Change=", price_change_pct, "% Threshold=", threshold_pct, "% Confidence=", confidence);
if(abs_change_pct >= threshold_pct)
{
// Check existing position
if(PositionSelect(_Symbol))
@@ -214,17 +332,26 @@ void OnTick()
else
{
// Open new position based on prediction
// Use raw prediction (decimal format) for threshold comparison
if(predicted_change_pct > InpPredictionThreshold)
if(price_change_pct > threshold_pct)
{
OpenBuyPosition();
Print(">>> Opening BUY position: Change=", price_change_pct, "% Threshold=", threshold_pct, "%");
OpenBuyPosition(predicted_sl, predicted_tp);
}
else if(predicted_change_pct < -InpPredictionThreshold)
else if(price_change_pct < -threshold_pct)
{
OpenSellPosition();
Print(">>> Opening SELL position: Change=", price_change_pct, "% Threshold=", threshold_pct, "%");
OpenSellPosition(predicted_sl, predicted_tp);
}
}
}
else
{
Print("Prediction below threshold: Change=", price_change_pct, "% < Threshold=", threshold_pct, "%");
}
}
else
{
Print("Confidence too low: ", confidence, " < ", InpMinConfidence);
}
}
@@ -251,16 +378,36 @@ bool PrepareInputData(float &input_array[])
ArraySetAsSeries(close, true);
ArraySetAsSeries(volume, true);
if(CopyOpen(_Symbol, PERIOD_CURRENT, 0, lookback + 50, open) < lookback)
int copied_open = CopyOpen(_Symbol, PERIOD_CURRENT, 0, lookback + 50, open);
if(copied_open < lookback)
{
Print("ERROR: CopyOpen failed. Got ", copied_open, " bars, need ", lookback);
return false;
if(CopyHigh(_Symbol, PERIOD_CURRENT, 0, lookback + 50, high) < lookback)
}
int copied_high = CopyHigh(_Symbol, PERIOD_CURRENT, 0, lookback + 50, high);
if(copied_high < lookback)
{
Print("ERROR: CopyHigh failed. Got ", copied_high, " bars, need ", lookback);
return false;
if(CopyLow(_Symbol, PERIOD_CURRENT, 0, lookback + 50, low) < lookback)
}
int copied_low = CopyLow(_Symbol, PERIOD_CURRENT, 0, lookback + 50, low);
if(copied_low < lookback)
{
Print("ERROR: CopyLow failed. Got ", copied_low, " bars, need ", lookback);
return false;
if(CopyClose(_Symbol, PERIOD_CURRENT, 0, lookback + 50, close) < lookback)
}
int copied_close = CopyClose(_Symbol, PERIOD_CURRENT, 0, lookback + 50, close);
if(copied_close < lookback)
{
Print("ERROR: CopyClose failed. Got ", copied_close, " bars, need ", lookback);
return false;
if(CopyTickVolume(_Symbol, PERIOD_CURRENT, 0, lookback + 50, volume) < lookback)
}
int copied_volume = CopyTickVolume(_Symbol, PERIOD_CURRENT, 0, lookback + 50, volume);
if(copied_volume < lookback)
{
Print("ERROR: CopyTickVolume failed. Got ", copied_volume, " bars, need ", lookback);
return false;
}
// Calculate indicators (simplified - you may need to match training exactly)
double rsi[], ema20[], ema50[], atr[];
@@ -379,21 +526,15 @@ bool PrepareInputData(float &input_array[])
//+------------------------------------------------------------------+
//| Run ONNX model |
//+------------------------------------------------------------------+
bool RunONNXModel(float &input_data[], float &output_data[])
bool RunONNXModel(matrixf &input_matrix, vectorf &output_vector)
{
if(onnx_handle == INVALID_HANDLE)
return false;
// Set input shape
long input_shape[] = {1, InpLookback, 13}; // 13 features
if(!OnnxSetInputShape(onnx_handle, 0, input_shape))
{
Print("ERROR: Failed to set input shape. Error: ", GetLastError());
return false;
}
// Run model
if(!OnnxRun(onnx_handle, ONNX_NO_CONVERSION, input_data, output_data))
// Run model - shapes are already set in OnInit per MQL5 documentation
// Based on: https://www.mql5.com/en/docs/onnx/onnx_test
// OnnxRun expects matrixf and vectorf, not flat arrays
if(!OnnxRun(onnx_handle, ONNX_DEBUG_LOGS | ONNX_NO_CONVERSION, input_matrix, output_vector))
{
Print("ERROR: Failed to run ONNX model. Error: ", GetLastError());
return false;
@@ -405,15 +546,29 @@ bool RunONNXModel(float &input_data[], float &output_data[])
//+------------------------------------------------------------------+
//| Open buy position |
//+------------------------------------------------------------------+
void OpenBuyPosition()
void OpenBuyPosition(double predicted_sl = 0.0, double predicted_tp = 0.0)
{
double price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double sl = InpStopLoss > 0 ? price - InpStopLoss * _Point * 10 : 0;
double tp = InpTakeProfit > 0 ? price + InpTakeProfit * _Point * 10 : 0;
double sl = 0.0;
double tp = 0.0;
if(InpUsePredictedSLTP && predicted_sl > 0 && predicted_tp > 0)
{
// Use predicted SL/TP
sl = price - predicted_sl;
tp = price + predicted_tp;
Print("Using predicted SL/TP: SL=", sl, " TP=", tp);
}
else
{
// Use fixed SL/TP from input parameters
sl = InpStopLoss > 0 ? price - InpStopLoss * _Point * 10 : 0;
tp = InpTakeProfit > 0 ? price + InpTakeProfit * _Point * 10 : 0;
}
if(trade.Buy(InpLotSize, _Symbol, price, sl, tp, "ONNX Buy Signal"))
{
Print("Buy order opened. Ticket: ", trade.ResultOrder());
Print("Buy order opened. Ticket: ", trade.ResultOrder(), " Price: ", price, " SL: ", sl, " TP: ", tp);
}
else
{
@@ -424,15 +579,29 @@ void OpenBuyPosition()
//+------------------------------------------------------------------+
//| Open sell position |
//+------------------------------------------------------------------+
void OpenSellPosition()
void OpenSellPosition(double predicted_sl = 0.0, double predicted_tp = 0.0)
{
double price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double sl = InpStopLoss > 0 ? price + InpStopLoss * _Point * 10 : 0;
double tp = InpTakeProfit > 0 ? price - InpTakeProfit * _Point * 10 : 0;
double sl = 0.0;
double tp = 0.0;
if(InpUsePredictedSLTP && predicted_sl > 0 && predicted_tp > 0)
{
// Use predicted SL/TP
sl = price + predicted_sl;
tp = price - predicted_tp;
Print("Using predicted SL/TP: SL=", sl, " TP=", tp);
}
else
{
// Use fixed SL/TP from input parameters
sl = InpStopLoss > 0 ? price + InpStopLoss * _Point * 10 : 0;
tp = InpTakeProfit > 0 ? price - InpTakeProfit * _Point * 10 : 0;
}
if(trade.Sell(InpLotSize, _Symbol, price, sl, tp, "ONNX Sell Signal"))
{
Print("Sell order opened. Ticket: ", trade.ResultOrder());
Print("Sell order opened. Ticket: ", trade.ResultOrder(), " Price: ", price, " SL: ", sl, " TP: ", tp);
}
else
{
+34 -5
View File
@@ -71,16 +71,45 @@ python predict_with_onnx.py --model models/XAUUSD_H1_model.onnx --symbol XAUUSD
### Step 3: Use in Expert Advisor
#### For Strategy Tester:
1. Copy the ONNX model to Tester Files folder:
```
<MT5 Data Folder>\Tester\Files\XAUUSD_H1_model.onnx
```
Or use the full path shown in error messages if file not found.
2. Compile `ONNX_EA.mq5` in MetaEditor (F7)
3. Open Strategy Tester (View → Strategy Tester or Ctrl+R)
4. Configure:
- Expert Advisor: `ONNX_EA`
- Symbol: `XAUUSD` (or your symbol)
- Period: `H1` (or your timeframe)
- Inputs:
- `InpModelPath`: `XAUUSD_H1_model.onnx` (just filename)
- Adjust other parameters as needed
5. Click Start
#### For Live/Demo Trading:
1. Copy the ONNX model to MT5's Files folder:
```
<MT5 Data Folder>\MQL5\Files\models\XAUUSD_H1_model.onnx
<MT5 Data Folder>\MQL5\Files\XAUUSD_H1_model.onnx
```
To find your Data Folder: Tools → Options → Expert Advisors → Data Folder
2. Compile `ONNX_EA.mq5` in MetaEditor
2. Compile `ONNX_EA.mq5` in MetaEditor (F7)
3. Attach the EA to a chart with:
- Model path: `models\XAUUSD_H1_model.onnx`
- Your trading parameters
3. Open a chart (e.g., XAUUSD H1)
4. Drag `ONNX_EA` from Navigator (Ctrl+N) onto the chart
5. Configure inputs:
- `InpModelPath`: `XAUUSD_H1_model.onnx` (just filename)
- Adjust trading parameters
6. Click OK and enable AutoTrading if needed
## Detailed Usage
+681
View File
@@ -0,0 +1,681 @@
//+------------------------------------------------------------------+
//| EURUSD_M15_EA.mq5 |
//| Copyright 2025, MetaQuotes Ltd. |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link "https://www.mql5.com"
#property version "1.00"
#property description "Expert Advisor using ONNX model for EURUSD 15-minute price prediction"
#property description "Based on: https://www.mql5.com/en/docs/onnx/onnx_test"
#include <Trade\Trade.mqh>
//--- Resource: Embed ONNX model in EA
// Based on: https://www.mql5.com/en/docs/onnx/onnx_test
// Path is relative to MQL5 directory (not starting with \\Files\\)
#resource "Files\\EURUSD_M15_model.onnx" as uchar ExtModel[]
//--- Input parameters
input group "ONNX Model Settings"
input string InpModelPath = ""; // ONNX Model Path (leave empty to use embedded resource)
input int InpLookback = 60; // Lookback Period (bars = 15 hours)
input bool InpUsePrediction = true; // Use Model Prediction
input group "Trading Settings"
input double InpLotSize = 0.01; // Lot Size
input int InpMagicNumber = 123456; // Magic Number
input int InpSlippage = 3; // Slippage (points)
input bool InpUsePredictedSLTP = true; // Use Predicted SL/TP (based on prediction & volatility)
input int InpStopLoss = 50; // Stop Loss (pips) - used if InpUsePredictedSLTP=false
input int InpTakeProfit = 100; // Take Profit (pips) - used if InpUsePredictedSLTP=false
input double InpSLMultiplier = 1.5; // SL Multiplier (ATR-based, e.g., 1.5 = 1.5x ATR)
input double InpTPMultiplier = 2.0; // TP Multiplier (ATR-based, e.g., 2.0 = 2x ATR)
input double InpMinSLATR = 0.5; // Minimum SL (ATR multiplier)
input double InpMinTPATR = 1.0; // Minimum TP (ATR multiplier)
input group "Prediction Settings"
input double InpPredictionThreshold = 0.00005; // Min Prediction Change (0.005% as decimal, e.g., 0.00005 = 0.005%)
input bool InpUseConfidence = true; // Use Confidence Filter
input double InpMinConfidence = 0.1; // Minimum Confidence (0.1 = 10%)
//--- Global variables
CTrade trade;
long onnx_handle = INVALID_HANDLE;
datetime last_bar_time = 0;
double last_prediction = 0.0;
double last_confidence = 0.0;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
// Set trade parameters
trade.SetExpertMagicNumber(InpMagicNumber);
trade.SetDeviationInPoints(InpSlippage);
trade.SetTypeFilling(ORDER_FILLING_FOK);
// Check symbol
if(_Symbol != "EURUSD" && _Symbol != "EURUSD#")
{
Print("WARNING: This EA is designed for EURUSD. Current symbol: ", _Symbol);
}
// Check timeframe
if(_Period != PERIOD_M15)
{
Print("WARNING: This EA is designed for M15 timeframe. Current timeframe: ", EnumToString(_Period));
}
// Load ONNX model
// Based on: https://www.mql5.com/en/docs/onnx/onnx_test
Print("Loading ONNX model from embedded resource...");
// Create model from resource buffer
onnx_handle = OnnxCreateFromBuffer(ExtModel, ONNX_DEBUG_LOGS);
if(onnx_handle == INVALID_HANDLE)
{
int error = GetLastError();
Print("ERROR: Failed to create ONNX model from resource. Error: ", error);
Print("Make sure the model file exists at: MQL5\\Files\\EURUSD_M15_model.onnx");
Print("Then recompile the EA to embed it as a resource.");
return(INIT_FAILED);
}
// Set input shape - per MQL5 documentation
const long ExtInputShape[] = {1, InpLookback, 13}; // batch=1, lookback bars, 13 features
if(!OnnxSetInputShape(onnx_handle, 0, ExtInputShape))
{
Print("OnnxSetInputShape failed, error ", GetLastError());
OnnxRelease(onnx_handle);
return(INIT_FAILED);
}
// Set output shapes - per MQL5 documentation (multi-output: price_change, sl_atr, tp_atr)
const long ExtOutputShape0[] = {1, 1}; // batch=1, price change output
const long ExtOutputShape1[] = {1, 1}; // batch=1, SL (ATR) output
const long ExtOutputShape2[] = {1, 1}; // batch=1, TP (ATR) output
if(!OnnxSetOutputShape(onnx_handle, 0, ExtOutputShape0))
{
Print("OnnxSetOutputShape[0] failed, error ", GetLastError());
OnnxRelease(onnx_handle);
return(INIT_FAILED);
}
if(!OnnxSetOutputShape(onnx_handle, 1, ExtOutputShape1))
{
Print("OnnxSetOutputShape[1] failed, error ", GetLastError());
OnnxRelease(onnx_handle);
return(INIT_FAILED);
}
if(!OnnxSetOutputShape(onnx_handle, 2, ExtOutputShape2))
{
Print("OnnxSetOutputShape[2] failed, error ", GetLastError());
OnnxRelease(onnx_handle);
return(INIT_FAILED);
}
// Get model info
long input_count = OnnxGetInputCount(onnx_handle);
long output_count = OnnxGetOutputCount(onnx_handle);
Print("ONNX Model loaded successfully");
Print(" Inputs: ", input_count);
Print(" Outputs: ", output_count);
if(input_count > 0)
{
string input_name = OnnxGetInputName(onnx_handle, 0);
Print(" Input name: ", input_name);
}
if(output_count > 0)
{
string output_name = OnnxGetOutputName(onnx_handle, 0);
Print(" Output name: ", output_name);
}
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
// Release ONNX model
if(onnx_handle != INVALID_HANDLE)
{
OnnxRelease(onnx_handle);
Print("ONNX model released");
}
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// Check if new bar
datetime current_bar_time = iTime(_Symbol, PERIOD_CURRENT, 0);
if(current_bar_time == last_bar_time)
{
return; // Still the same bar
}
last_bar_time = current_bar_time;
// Check if we should use prediction
if(!InpUsePrediction)
{
return;
}
// Prepare input data
float input_data[];
if(!PrepareInputData(input_data))
{
Print("ERROR: Failed to prepare input data");
return;
}
// Check if input data is valid
if(ArraySize(input_data) != InpLookback * 13)
{
Print("ERROR: Input data size mismatch. Expected: ", InpLookback * 13, ", Got: ", ArraySize(input_data));
return;
}
// Convert flat array to matrixf for OnnxRun
// Shape: [lookback, features] = [60, 13] - batch dimension is added automatically
matrixf input_matrix;
input_matrix.Resize(InpLookback, 13);
// Fill matrix from flat array
int idx = 0;
for(int i = 0; i < InpLookback; i++)
{
for(int j = 0; j < 13; j++)
{
if(idx >= ArraySize(input_data))
{
Print("ERROR: Index out of bounds when filling matrix. idx=", idx, ", array size=", ArraySize(input_data));
return;
}
input_matrix[i][j] = input_data[idx++];
}
}
// Verify matrix is not empty
if(input_matrix.Rows() == 0 || input_matrix.Cols() == 0)
{
Print("ERROR: Input matrix is empty. Rows: ", input_matrix.Rows(), ", Cols: ", input_matrix.Cols());
return;
}
// Run ONNX model - multi-output: price_change, sl_atr, tp_atr
vectorf output_price_change(1);
vectorf output_sl_atr(1);
vectorf output_tp_atr(1);
if(!RunONNXModel(input_matrix, output_price_change, output_sl_atr, output_tp_atr))
{
Print("ERROR: Failed to run ONNX model");
return;
}
// Get predictions
if(output_price_change.Size() == 0 || output_sl_atr.Size() == 0 || output_tp_atr.Size() == 0)
{
Print("ERROR: Empty output from ONNX model");
return;
}
// Model outputs: [price_change_pct, sl_atr_multiple, tp_atr_multiple]
double predicted_change_pct = output_price_change[0];
double predicted_sl_atr = output_sl_atr[0];
double predicted_tp_atr = output_tp_atr[0];
// Ensure positive values for SL/TP
predicted_sl_atr = MathMax(0.5, predicted_sl_atr); // Minimum 0.5 ATR
predicted_tp_atr = MathMax(1.0, predicted_tp_atr); // Minimum 1.0 ATR
double current_price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
// Check if prediction is percentage (between -1 and 1) or absolute price (old format)
double price_change_pct;
double predicted_price;
if(MathAbs(predicted_change_pct) < 1.0)
{
// New format: percentage (e.g., -0.003 = -0.3%)
price_change_pct = predicted_change_pct * 100.0; // Convert to percentage
predicted_price = current_price * (1.0 + predicted_change_pct); // Calculate predicted price
}
else
{
// Old format: absolute price
predicted_price = predicted_change_pct;
double price_change = predicted_price - current_price;
price_change_pct = (price_change / current_price) * 100.0;
}
// Calculate confidence (for percentage predictions: 0.001 = 0.1% = 10% confidence)
double confidence;
if(MathAbs(price_change_pct) < 1.0)
{
// It's a decimal percentage (e.g., 0.001 = 0.1%)
confidence = MathMin(MathAbs(predicted_change_pct) / 0.01, 1.0); // 0.01 = 1% = 100% confidence
}
else
{
// It's already in percentage form
confidence = MathMin(MathAbs(price_change_pct) / 1.0, 1.0);
}
last_prediction = predicted_price;
last_confidence = confidence;
// Calculate ATR for converting model's ATR multiples to actual price distances
double atr_value = 0.0;
double atr_array[];
ArraySetAsSeries(atr_array, true);
int atr_handle = iATR(_Symbol, PERIOD_CURRENT, 14);
if(atr_handle != INVALID_HANDLE)
{
if(CopyBuffer(atr_handle, 0, 0, 1, atr_array) > 0)
{
atr_value = atr_array[0];
}
IndicatorRelease(atr_handle);
}
// Use model's predicted SL/TP (in ATR multiples) directly
double predicted_sl = 0.0;
double predicted_tp = 0.0;
if(InpUsePredictedSLTP && atr_value > 0)
{
// Model predicts SL and TP in ATR multiples - convert to price distance
predicted_sl = atr_value * predicted_sl_atr;
predicted_tp = atr_value * predicted_tp_atr;
// Ensure minimums from input parameters
predicted_sl = MathMax(predicted_sl, atr_value * InpMinSLATR);
predicted_tp = MathMax(predicted_tp, atr_value * InpMinTPATR);
// Ensure TP is at least 1.5x SL for risk/reward
if(predicted_tp < predicted_sl * 1.5)
{
predicted_tp = predicted_sl * 1.5;
}
// Get broker's stop level requirements
int stop_level = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
double min_stop_distance = stop_level * point;
// Ensure SL/TP meet broker's minimum stop level
if(predicted_sl < min_stop_distance)
{
predicted_sl = min_stop_distance;
}
if(predicted_tp < min_stop_distance)
{
predicted_tp = min_stop_distance;
}
}
// Log prediction
Print("Prediction: Current=", current_price,
" Predicted Change=", price_change_pct, "%",
" Predicted Price=", predicted_price,
" Confidence=", confidence,
" ATR=", atr_value);
if(InpUsePredictedSLTP && predicted_sl > 0 && predicted_tp > 0)
{
Print(" Model SL=", predicted_sl_atr, "x ATR (", predicted_sl, " points, ", predicted_sl/current_price*100, "%)",
" Model TP=", predicted_tp_atr, "x ATR (", predicted_tp, " points, ", predicted_tp/current_price*100, "%)");
}
// Check if we should trade
if(!InpUseConfidence || confidence >= InpMinConfidence)
{
// Check if prediction is significant
// price_change_pct is in percentage (e.g., 5.72 = 5.72%)
// InpPredictionThreshold is in decimal (e.g., 0.00005 = 0.005%)
// Convert threshold to percentage for comparison
double threshold_pct = InpPredictionThreshold * 100.0;
double abs_change_pct = MathAbs(price_change_pct); // Already in percentage
Print("Trade Check: Change=", price_change_pct, "% Threshold=", threshold_pct, "% Confidence=", confidence);
if(abs_change_pct >= threshold_pct)
{
// Check existing position
if(PositionSelect(_Symbol))
{
// Manage existing position
ManagePosition(predicted_price, price_change_pct);
}
else
{
// Open new position based on prediction
if(price_change_pct > threshold_pct)
{
Print(">>> Opening BUY position: Change=", price_change_pct, "% Threshold=", threshold_pct, "%");
OpenBuyPosition(predicted_sl, predicted_tp);
}
else if(price_change_pct < -threshold_pct)
{
Print(">>> Opening SELL position: Change=", price_change_pct, "% Threshold=", threshold_pct, "%");
OpenSellPosition(predicted_sl, predicted_tp);
}
}
}
else
{
Print("Prediction below threshold: Change=", price_change_pct, "% < Threshold=", threshold_pct, "%");
}
}
else
{
Print("Confidence too low: ", confidence, " < ", InpMinConfidence);
}
}
//+------------------------------------------------------------------+
//| Prepare input data for ONNX model |
//+------------------------------------------------------------------+
bool PrepareInputData(float &input_array[])
{
int lookback = InpLookback;
int features = 13; // OHLC(4) + volume(1) + RSI(1) + EMA20(1) + EMA50(1) + ATR(1) + price_change(1) + high_low_ratio(1) + volume_ma(1) + volume_ratio(1) = 13
ArrayResize(input_array, lookback * features);
ArrayInitialize(input_array, 0.0);
// Get historical data
double open[], high[], low[], close[];
long volume[]; // CopyTickVolume requires long[] not double[]
ArraySetAsSeries(open, true);
ArraySetAsSeries(high, true);
ArraySetAsSeries(low, true);
ArraySetAsSeries(close, true);
ArraySetAsSeries(volume, true);
int copied_open = CopyOpen(_Symbol, PERIOD_CURRENT, 0, lookback + 50, open);
if(copied_open < lookback)
{
Print("ERROR: CopyOpen failed. Got ", copied_open, " bars, need ", lookback);
return false;
}
int copied_high = CopyHigh(_Symbol, PERIOD_CURRENT, 0, lookback + 50, high);
if(copied_high < lookback)
{
Print("ERROR: CopyHigh failed. Got ", copied_high, " bars, need ", lookback);
return false;
}
int copied_low = CopyLow(_Symbol, PERIOD_CURRENT, 0, lookback + 50, low);
if(copied_low < lookback)
{
Print("ERROR: CopyLow failed. Got ", copied_low, " bars, need ", lookback);
return false;
}
int copied_close = CopyClose(_Symbol, PERIOD_CURRENT, 0, lookback + 50, close);
if(copied_close < lookback)
{
Print("ERROR: CopyClose failed. Got ", copied_close, " bars, need ", lookback);
return false;
}
int copied_volume = CopyTickVolume(_Symbol, PERIOD_CURRENT, 0, lookback + 50, volume);
if(copied_volume < lookback)
{
Print("ERROR: CopyTickVolume failed. Got ", copied_volume, " bars, need ", lookback);
return false;
}
// Calculate indicators
double rsi[], ema20[], ema50[], atr[];
ArraySetAsSeries(rsi, true);
ArraySetAsSeries(ema20, true);
ArraySetAsSeries(ema50, true);
ArraySetAsSeries(atr, true);
// Calculate RSI
int rsi_handle = iRSI(_Symbol, PERIOD_CURRENT, 14, PRICE_CLOSE);
if(rsi_handle == INVALID_HANDLE) return false;
if(CopyBuffer(rsi_handle, 0, 0, lookback + 50, rsi) < lookback)
{
IndicatorRelease(rsi_handle);
return false;
}
IndicatorRelease(rsi_handle);
// Calculate EMAs
int ema20_handle = iMA(_Symbol, PERIOD_CURRENT, 20, 0, MODE_EMA, PRICE_CLOSE);
int ema50_handle = iMA(_Symbol, PERIOD_CURRENT, 50, 0, MODE_EMA, PRICE_CLOSE);
if(ema20_handle == INVALID_HANDLE || ema50_handle == INVALID_HANDLE) return false;
if(CopyBuffer(ema20_handle, 0, 0, lookback + 50, ema20) < lookback ||
CopyBuffer(ema50_handle, 0, 0, lookback + 50, ema50) < lookback)
{
IndicatorRelease(ema20_handle);
IndicatorRelease(ema50_handle);
return false;
}
IndicatorRelease(ema20_handle);
IndicatorRelease(ema50_handle);
// Calculate ATR
int atr_handle = iATR(_Symbol, PERIOD_CURRENT, 14);
if(atr_handle == INVALID_HANDLE) return false;
if(CopyBuffer(atr_handle, 0, 0, lookback + 50, atr) < lookback)
{
IndicatorRelease(atr_handle);
return false;
}
IndicatorRelease(atr_handle);
// Calculate volume MA for normalization
double volume_ma[];
ArraySetAsSeries(volume_ma, true);
ArrayResize(volume_ma, lookback);
ArrayInitialize(volume_ma, 0.0);
// Calculate volume MA (20-period rolling average)
for(int j = 0; j < lookback; j++)
{
double sum = 0.0;
int count = 0;
for(int k = j; k < j + 20 && k < ArraySize(volume); k++)
{
sum += (double)volume[k];
count++;
}
volume_ma[j] = count > 0 ? sum / count : (double)volume[j];
}
// Prepare features - MUST match Python training exactly (13 features)
int idx = 0;
for(int i = 0; i < lookback; i++)
{
// Feature 1-4: OHLC
input_array[idx++] = (float)open[i];
input_array[idx++] = (float)high[i];
input_array[idx++] = (float)low[i];
input_array[idx++] = (float)close[i];
// Feature 5: Volume (normalized by 1,000,000)
input_array[idx++] = (float)((double)volume[i] / 1000000.0);
// Feature 6: RSI (normalized by 100)
input_array[idx++] = (float)(rsi[i] / 100.0);
// Feature 7: EMA20 normalized difference
input_array[idx++] = (float)((ema20[i] - close[i]) / close[i]);
// Feature 8: EMA50 normalized difference
input_array[idx++] = (float)((ema50[i] - close[i]) / close[i]);
// Feature 9: ATR normalized
input_array[idx++] = (float)(atr[i] / close[i]);
// Feature 10: Price change (percentage)
double price_change = i > 0 ? (close[i] - close[i+1]) / close[i+1] : 0.0;
input_array[idx++] = (float)price_change;
// Feature 11: High/Low ratio
input_array[idx++] = (float)(high[i] / low[i]);
// Feature 12: Volume MA (normalized by 1,000,000)
input_array[idx++] = (float)(volume_ma[i] / 1000000.0);
// Feature 13: Volume ratio
double vol_ratio = volume_ma[i] > 0 ? (double)volume[i] / volume_ma[i] : 1.0;
input_array[idx++] = (float)vol_ratio;
}
return true;
}
//+------------------------------------------------------------------+
//| Run ONNX model |
//+------------------------------------------------------------------+
bool RunONNXModel(matrixf &input_matrix, vectorf &output_price_change, vectorf &output_sl_atr, vectorf &output_tp_atr)
{
if(onnx_handle == INVALID_HANDLE)
return false;
// For multi-output ONNX models in MQL5, OnnxRun expects all outputs as separate parameters
// Function signature: OnnxRun(handle, flags, input, output1, output2, output3, ...)
// This is 4 parameters total: handle, flags, input, and then all outputs
if(!OnnxRun(onnx_handle, ONNX_DEBUG_LOGS | ONNX_NO_CONVERSION, input_matrix,
output_price_change, output_sl_atr, output_tp_atr))
{
Print("ERROR: Failed to run ONNX model. Error: ", GetLastError());
Print("Model has ", OnnxGetOutputCount(onnx_handle), " outputs");
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Open buy position |
//+------------------------------------------------------------------+
void OpenBuyPosition(double predicted_sl = 0.0, double predicted_tp = 0.0)
{
double price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double sl = 0.0;
double tp = 0.0;
// Get broker's stop level requirements
int stop_level = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
double min_stop_distance = stop_level * point;
if(InpUsePredictedSLTP && predicted_sl > 0 && predicted_tp > 0)
{
// Use predicted SL/TP
// For BUY: SL below entry, TP above entry
sl = price - predicted_sl;
tp = price + predicted_tp;
// Validate stops meet broker requirements
if(sl > 0 && (price - sl) < min_stop_distance)
{
sl = price - min_stop_distance;
}
if(tp > 0 && (tp - price) < min_stop_distance)
{
tp = price + min_stop_distance;
}
Print("Using predicted SL/TP: Entry=", price, " SL=", sl, " TP=", tp, " (SL distance: ", price - sl, ", TP distance: ", tp - price, ")");
}
else
{
// Use fixed SL/TP from input parameters
sl = InpStopLoss > 0 ? price - InpStopLoss * _Point * 10 : 0;
tp = InpTakeProfit > 0 ? price + InpTakeProfit * _Point * 10 : 0;
}
if(trade.Buy(InpLotSize, _Symbol, price, sl, tp, "ONNX Buy Signal"))
{
Print("Buy order opened. Ticket: ", trade.ResultOrder(), " Price: ", price, " SL: ", sl, " TP: ", tp);
}
else
{
Print("Failed to open buy order. Error: ", trade.ResultRetcodeDescription());
Print(" Entry: ", price, " SL: ", sl, " TP: ", tp);
Print(" Stop Level: ", stop_level, " Min Distance: ", min_stop_distance);
}
}
//+------------------------------------------------------------------+
//| Open sell position |
//+------------------------------------------------------------------+
void OpenSellPosition(double predicted_sl = 0.0, double predicted_tp = 0.0)
{
double price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double sl = 0.0;
double tp = 0.0;
// Get broker's stop level requirements
int stop_level = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
double min_stop_distance = stop_level * point;
if(InpUsePredictedSLTP && predicted_sl > 0 && predicted_tp > 0)
{
// Use predicted SL/TP
// For SELL: SL above entry, TP below entry
sl = price + predicted_sl;
tp = price - predicted_tp;
// Validate stops meet broker requirements
if(sl > 0 && (sl - price) < min_stop_distance)
{
sl = price + min_stop_distance;
}
if(tp > 0 && (price - tp) < min_stop_distance)
{
tp = price - min_stop_distance;
}
Print("Using predicted SL/TP: Entry=", price, " SL=", sl, " TP=", tp, " (SL distance: ", sl - price, ", TP distance: ", price - tp, ")");
}
else
{
// Use fixed SL/TP from input parameters
sl = InpStopLoss > 0 ? price + InpStopLoss * _Point * 10 : 0;
tp = InpTakeProfit > 0 ? price - InpTakeProfit * _Point * 10 : 0;
}
if(trade.Sell(InpLotSize, _Symbol, price, sl, tp, "ONNX Sell Signal"))
{
Print("Sell order opened. Ticket: ", trade.ResultOrder(), " Price: ", price, " SL: ", sl, " TP: ", tp);
}
else
{
Print("Failed to open sell order. Error: ", trade.ResultRetcodeDescription());
Print(" Entry: ", price, " SL: ", sl, " TP: ", tp);
Print(" Stop Level: ", stop_level, " Min Distance: ", min_stop_distance);
}
}
//+------------------------------------------------------------------+
//| Manage existing position |
//+------------------------------------------------------------------+
void ManagePosition(double predicted_price, double price_change_pct)
{
if(!PositionSelect(_Symbol))
return;
// Simple position management - can be enhanced
// For now, just log the position status
double position_profit = PositionGetDouble(POSITION_PROFIT);
Print("Position exists. Profit: ", position_profit, " Predicted change: ", price_change_pct, "%");
}
+100
View File
@@ -0,0 +1,100 @@
# EURUSD 15-Minute ONNX Model Training
This directory contains the training script for a EURUSD price prediction model using 15-minute timeframe data from 1990 to 2026 using MetaTrader 5 as the data source.
## Requirements
```bash
pip install MetaTrader5 tensorflow scikit-learn pandas numpy tf2onnx onnx tqdm
```
## Usage
1. **Make sure MetaTrader 5 is running and logged in**
2. **Ensure EURUSD symbol is available in your broker**
3. **Make sure you have historical data downloaded in MT5** (Tools → History Center → Download)
4. **Run the training script:**
```bash
cd ai/eurusd1min
python main.py
```
## Configuration
The script is configured with:
- **Symbol**: EURUSD
- **Timeframe**: M15 (15 minutes)
- **Lookback**: 60 bars (15 hours of history)
- **Date Range**: 1990-01-01 to 2026-01-01
- **Model Architecture**: LSTM with 3 layers (128, 64, 32 units)
- **Epochs**: 50 (with early stopping)
- **Batch Size**: 64
## Data Fetching
- The script fetches data in **1-month chunks** to manage memory
- 15-minute data is more likely to be available for longer historical periods than 1-minute data
- The script automatically skips chunks with only 1 bar (invalid/placeholder data)
- Progress is shown for each chunk
- Make sure you have sufficient historical data in MT5
## Output
The script will create:
- `models/EURUSD_M15_model.onnx` - The trained ONNX model
- `models/EURUSD_M15_model_scaler.pkl` - The MinMaxScaler used for normalization
## Model Features
The model uses 13 features:
1. Open
2. High
3. Low
4. Close
5. Tick Volume
6. RSI (14 period)
7. EMA 20
8. EMA 50
9. ATR (14 period)
10. Price Change (percentage)
11. High/Low Ratio
12. Volume MA (20 period)
13. Volume Ratio
## Model Output
The model predicts the **price change percentage** for the next bar (15 minutes ahead).
## Notes
- Training on 36 years of 15-minute data will take significant time and memory
- The script fetches data in 1-month chunks to manage memory
- Chunks with only 1 bar are automatically skipped (invalid/placeholder data)
- Early stopping and learning rate reduction are enabled to prevent overfitting
- The model uses dropout (0.3) for regularization
- 15-minute data is more manageable than 1-minute data for long historical periods
## Using the Model in MQL5
### Expert Advisor
An Expert Advisor (`EURUSD_M15_EA.mq5`) is provided in this directory. It uses the trained ONNX model for automated trading.
**Setup:**
1. Copy `EURUSD_M15_model.onnx` to `MQL5/Files/` directory
2. Compile `EURUSD_M15_EA.mq5` in MetaEditor
3. The model will be embedded as a resource during compilation
4. Attach the EA to a EURUSD chart with M15 timeframe
**Features:**
- Embedded ONNX model (no file path issues)
- Dynamic SL/TP based on prediction, confidence, and ATR
- Configurable prediction threshold and confidence filter
- Automatic position management
**Input Parameters:**
- `InpLookback`: 60 bars (15 hours of history)
- `InpUsePredictedSLTP`: Use dynamic SL/TP based on prediction
- `InpPredictionThreshold`: Minimum prediction change to trade (default: 0.00005 = 0.005%)
- `InpMinConfidence`: Minimum confidence to trade (default: 0.1 = 10%)
+655
View File
@@ -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())
Binary file not shown.
Binary file not shown.