Update
This commit is contained in:
@@ -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, "%");
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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, "%");
|
||||
}
|
||||
@@ -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%)
|
||||
@@ -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.
Reference in New Issue
Block a user