Article-23177-AutoML-Pipeli.../AutoML Pipeline.mq5

467 lines
17 KiB
MQL5
Raw Permalink Normal View History

2026-07-16 21:49:07 +02:00
//+------------------------------------------------------------------+
//| AutoML Pipeline.mq5 |
//| Copyright 2025, MetaQuotes Ltd. |
//| https://www.mql5.com/en/users/johnhlomohang/ |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link "https://www.mql5.com/en/users/johnhlomohang/"
#property version "1.00"
#include <Trade\Trade.mqh>
#resource "\\Files\\AutoML\\ema_rsi_model.onnx" as uchar ExtModelBuffer[]
//+------------------------------------------------------------------+
//| Inputs |
//+------------------------------------------------------------------+
input group "=== Strategy (must match Python training config) ==="
input int InpEmaFast = 12; // Fast EMA period
input int InpEmaSlow = 26; // Slow EMA period
input int InpRsiPeriod = 14; // RSI period
input int InpAtrPeriod = 14; // ATR period
input int InpVolFast = 123; // StdDev short window
input int InpVolSlow = 864; // StdDev long window
input int InpMaxHoldBars = 254; // Time-stop (bars) — matches labeling
input group "=== AutoML Gate ==="
input bool InpUseModelGate = false; // false = raw EMA+RSI baseline (for A/B tests)
input double InpConfidence = 0.55; // Min P(profit) to take a signal (optimize 0.50–0.75)
input group "=== Risk & Trade Management ==="
input double InpLots = 0.36; // Fixed lot size
input bool InpUseSL = true; // Protective stop-loss
input double InpSLxATR = 12.4; // SL distance = ATR * this
input bool InpUseTrailing = true; // ATR trailing stop (locks in profit)
input double InpTrailxATR = 3.0; // Trail distance = ATR * this
input ulong InpMagic = 100010; // Magic number
input int InpSlippage = 7; // Max deviation (points)
input group "=== Display ==="
input bool InpShowDashboard = true; // On-chart status panel
//+------------------------------------------------------------------+
//| Globals |
//+------------------------------------------------------------------+
#define N_FEATURES 9
CTrade g_trade;
long g_onnx = INVALID_HANDLE; // ONNX session handle
int g_hEmaFast = INVALID_HANDLE;
int g_hEmaSlow = INVALID_HANDLE;
int g_hRsi = INVALID_HANDLE;
int g_hAtr = INVALID_HANDLE;
datetime g_lastBarTime = 0;
//--- Dashboard state
double g_lastConfidence = 0.0;
string g_lastSignal = "none";
string g_lastDecision = "-";
int g_signalsSeen = 0;
int g_signalsTaken = 0;
//+------------------------------------------------------------------+
//| Expert initialization |
//+------------------------------------------------------------------+
int OnInit()
{
//--- Indicator handles
g_hEmaFast = iMA(_Symbol, _Period, InpEmaFast, 0, MODE_EMA, PRICE_CLOSE);
g_hEmaSlow = iMA(_Symbol, _Period, InpEmaSlow, 0, MODE_EMA, PRICE_CLOSE);
g_hRsi = iRSI(_Symbol, _Period, InpRsiPeriod, PRICE_CLOSE);
g_hAtr = iATR(_Symbol, _Period, InpAtrPeriod);
if(g_hEmaFast==INVALID_HANDLE || g_hEmaSlow==INVALID_HANDLE ||
g_hRsi==INVALID_HANDLE || g_hAtr==INVALID_HANDLE)
{
Print("[INIT] Failed to create indicator handles");
return INIT_FAILED;
}
//--- ONNX session from the embedded resource
g_onnx = OnnxCreateFromBuffer(ExtModelBuffer, ONNX_DEFAULT);
if(g_onnx == INVALID_HANDLE)
{
PrintFormat("[INIT] OnnxCreateFromBuffer failed, error %d", GetLastError());
return INIT_FAILED;
}
//--- Pin the shapes. The model was exported with a dynamic batch
//--- dimension (None, 9); MT5 requires it fixed before running.
const long inShape[] = {1, N_FEATURES};
const long outLblShape[] = {1};
const long outProbShape[] = {1, 2};
if(!OnnxSetInputShape(g_onnx, 0, inShape))
{
PrintFormat("[INIT] OnnxSetInputShape failed, error %d", GetLastError());
return INIT_FAILED;
}
if(!OnnxSetOutputShape(g_onnx, 0, outLblShape) ||
!OnnxSetOutputShape(g_onnx, 1, outProbShape))
{
PrintFormat("[INIT] OnnxSetOutputShape failed, error %d", GetLastError());
return INIT_FAILED;
}
//--- Trade object
g_trade.SetExpertMagicNumber(InpMagic);
g_trade.SetDeviationInPoints(InpSlippage);
g_trade.SetTypeFillingBySymbol(_Symbol);
PrintFormat("[INIT] OK — model %d bytes embedded, gate=%s, threshold=%.2f",
ArraySize(ExtModelBuffer),
InpUseModelGate ? "ON" : "OFF (baseline)",
InpConfidence);
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Expert deinitialization |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
if(g_onnx != INVALID_HANDLE)
OnnxRelease(g_onnx);
IndicatorRelease(g_hEmaFast);
IndicatorRelease(g_hEmaSlow);
IndicatorRelease(g_hRsi);
IndicatorRelease(g_hAtr);
Comment("");
}
//+------------------------------------------------------------------+
//| Expert tick |
//+------------------------------------------------------------------+
void OnTick()
{
//--- Trailing runs on every tick so profit gets locked intrabar
if(InpUseTrailing)
ManageTrailingStop();
//--- Everything else is bar-close logic (mirrors the labeling)
if(!IsNewBar())
return;
ProcessClosedBar();
if(InpShowDashboard)
UpdateDashboard();
}
//+------------------------------------------------------------------+
//| New-bar detector |
//+------------------------------------------------------------------+
bool IsNewBar()
{
datetime t = iTime(_Symbol, _Period, 0);
if(t == g_lastBarTime)
return false;
g_lastBarTime = t;
return true;
}
//+------------------------------------------------------------------+
//| Core logic — runs once per bar, on the freshly CLOSED bar |
//+------------------------------------------------------------------+
void ProcessClosedBar()
{
//--- 1) Read EMAs on bars 1 (closed) and 2 (prior)
double emaF[], emaS[];
if(CopyBuffer(g_hEmaFast, 0, 1, 2, emaF) < 2)
return; // [0]=bar1 [1]=bar2? No:
if(CopyBuffer(g_hEmaSlow, 0, 1, 2, emaS) < 2)
return;
//--- CopyBuffer fills as-series=false by default: emaF[0]=bar2, emaF[1]=bar1
double emaFast1 = emaF[1], emaFast2 = emaF[0];
double emaSlow1 = emaS[1], emaSlow2 = emaS[0];
//--- 2) Crossover on the closed bar (same rule as Python)
//--- Python: above[i] != above[i-1]
bool crossUp = (emaFast1 > emaSlow1) && (emaFast2 <= emaSlow2);
bool crossDn = (emaFast1 <= emaSlow1) && (emaFast2 > emaSlow2);
int direction = crossUp ? 1 : (crossDn ? -1 : 0);
//--- 3) Exit management first (mirrors the label simulation)
//--- Exit rule in training: opposite crossover OR time stop
if(PositionSelectByMagic())
{
long posType = PositionGetInteger(POSITION_TYPE);
bool opposite = (posType==POSITION_TYPE_BUY && crossDn) ||
(posType==POSITION_TYPE_SELL && crossUp);
datetime openTime = (datetime)PositionGetInteger(POSITION_TIME);
int barsHeld = iBarShift(_Symbol, _Period, openTime);
if(opposite)
{
g_trade.PositionClose(_Symbol);
PrintFormat("[EXIT] Opposite crossover after %d bars", barsHeld);
}
else
if(barsHeld >= InpMaxHoldBars)
{
g_trade.PositionClose(_Symbol);
PrintFormat("[EXIT] Time stop hit (%d bars)", barsHeld);
}
}
//--- 4) Entry evaluation
if(direction == 0)
return; // no signal this bar
g_signalsSeen++;
g_lastSignal = (direction > 0) ? "BUY cross" : "SELL cross";
if(PositionSelectByMagic()) // still holding (same-direction cross)
{
g_lastDecision = "skipped (position open)";
return;
}
//--- 5) Build the feature vector — THE CONTRACT
float features[N_FEATURES];
if(!ComputeFeatures(direction, features))
{
g_lastDecision = "skipped (feature error)";
return;
}
//--- 6) Query the model
double pProfit = 1.0; // gate off => always pass
if(InpUseModelGate)
{
if(!RunModel(features, pProfit))
{
g_lastDecision = "skipped (inference error)";
return;
}
}
g_lastConfidence = pProfit;
PrintFormat("[SIGNAL] %s | P(profit)=%.3f | threshold=%.2f",
g_lastSignal, pProfit, InpConfidence);
if(pProfit <= InpConfidence)
{
g_lastDecision = StringFormat("REJECTED (%.3f <= %.2f)", pProfit, InpConfidence);
return;
}
//--- 7) Execute
ExecuteEntry(direction);
}
//+------------------------------------------------------------------+
//| Feature vector — order and math must match the Python notebook |
//+------------------------------------------------------------------+
bool ComputeFeatures(const int direction, float &f[])
{
//--- Prices of the closed bar
double close1 = iClose(_Symbol, _Period, 1);
double high1 = iHigh(_Symbol, _Period, 1);
double low1 = iLow(_Symbol, _Period, 1);
if(close1 <= 0.0)
return false;
//--- Indicator values
double emaF[], emaS[], rsi[], atr[];
if(CopyBuffer(g_hEmaFast, 0, 1, 1, emaF) < 1)
return false;
if(CopyBuffer(g_hEmaSlow, 0, 1, 1, emaS) < 1)
return false;
if(CopyBuffer(g_hRsi, 0, 1, 2, rsi) < 2)
return false; // rsi[0]=bar2 rsi[1]=bar1
if(CopyBuffer(g_hAtr, 0, 1, 1, atr) < 1)
return false;
//--- Sample standard deviations (pandas-compatible, ddof=1).
//--- iStdDev uses the POPULATION formula (ddof=0), which is
//--- systematically ~1–2%% smaller — a silent feature-drift bug.
double sdFast = StdDevSample(InpVolFast, 1);
double sdSlow = StdDevSample(InpVolSlow, 1);
if(sdFast <= 0.0 || sdSlow <= 0.0)
return false;
double range1 = high1 - low1;
double rangePct = (range1 > 0.0) ? (close1 - low1) / range1 : 0.5;
f[0] = (float)(emaF[0] / close1 - 1.0); // ema_fast_rel
f[1] = (float)(emaS[0] / close1 - 1.0); // ema_slow_rel
f[2] = (float)((emaF[0] - emaS[0]) / close1); // ema_distance
f[3] = (float)(rsi[1]); // rsi (bar 1)
f[4] = (float)(rsi[1] - rsi[0]); // rsi_momentum
f[5] = (float)(atr[0] / close1); // atr_rel
f[6] = (float)(sdFast / sdSlow); // volatility_ratio
f[7] = (float)(rangePct); // close_range_pct
f[8] = (float)(direction); // signal_direction
return true;
}
//+------------------------------------------------------------------+
//| Sample std-dev |
//+------------------------------------------------------------------+
double StdDevSample(const int period, const int shift)
{
double closes[];
if(CopyClose(_Symbol, _Period, shift, period, closes) < period)
return 0.0;
double mean = 0.0;
for(int i = 0; i < period; i++)
mean += closes[i];
mean /= period;
double ss = 0.0;
for(int i = 0; i < period; i++)
{
double d = closes[i] - mean;
ss += d * d;
}
return MathSqrt(ss / (period - 1)); // ddof = 1
}
//+------------------------------------------------------------------+
//| ONNX inference — returns P(trade closes in profit) |
//+------------------------------------------------------------------+
bool RunModel(const float &features[], double &pProfit)
{
long outLabel[1]; // output 0: predicted class (int64)
float outProbs[1][2]; // output 1: [P(loss), P(profit)]
if(!OnnxRun(g_onnx, ONNX_NO_CONVERSION, features, outLabel, outProbs))
{
PrintFormat("[ONNX] OnnxRun failed, error %d", GetLastError());
return false;
}
pProfit = (double)outProbs[0][1];
return true;
}
//+------------------------------------------------------------------+
//| Entry execution with optional ATR stop-loss |
//+------------------------------------------------------------------+
void ExecuteEntry(const int direction)
{
double atr[];
if(CopyBuffer(g_hAtr, 0, 1, 1, atr) < 1)
return;
double sl = 0.0;
bool ok = false;
if(direction > 0)
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
if(InpUseSL)
sl = NormalizeDouble(ask - InpSLxATR * atr[0], _Digits);
ok = g_trade.Buy(InpLots, _Symbol, 0.0, sl, 0.0, "AutoML gate");
}
else
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
if(InpUseSL)
sl = NormalizeDouble(bid + InpSLxATR * atr[0], _Digits);
ok = g_trade.Sell(InpLots, _Symbol, 0.0, sl, 0.0, "AutoML gate");
}
if(ok)
{
g_signalsTaken++;
g_lastDecision = StringFormat("TAKEN (%.3f > %.2f)", g_lastConfidence, InpConfidence);
}
else
{
g_lastDecision = StringFormat("order failed (%d)", (int)g_trade.ResultRetcode());
PrintFormat("[TRADE] Order failed: retcode=%d", (int)g_trade.ResultRetcode());
}
}
//+------------------------------------------------------------------+
//| ATR trailing stop — the "guarantee the profit" layer |
//+------------------------------------------------------------------+
void ManageTrailingStop()
{
if(!PositionSelectByMagic())
return;
double atr[];
if(CopyBuffer(g_hAtr, 0, 1, 1, atr) < 1)
return;
double trail = InpTrailxATR * atr[0];
long type = PositionGetInteger(POSITION_TYPE);
double sl = PositionGetDouble(POSITION_SL);
double openPx = PositionGetDouble(POSITION_PRICE_OPEN);
if(type == POSITION_TYPE_BUY)
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double newSL = NormalizeDouble(bid - trail, _Digits);
//--- Only trail once in profit, only ever move the stop UP
if(newSL > openPx && (sl == 0.0 || newSL > sl))
g_trade.PositionModify(_Symbol, newSL, PositionGetDouble(POSITION_TP));
}
else
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double newSL = NormalizeDouble(ask + trail, _Digits);
//--- Only trail once in profit, only ever move the stop DOWN
if(newSL < openPx && (sl == 0.0 || newSL < sl))
g_trade.PositionModify(_Symbol, newSL, PositionGetDouble(POSITION_TP));
}
}
//+------------------------------------------------------------------+
//| Select the EA's own position on this symbol |
//+------------------------------------------------------------------+
bool PositionSelectByMagic()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0)
continue;
if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
PositionGetInteger(POSITION_MAGIC) == (long)InpMagic)
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| On-chart dashboard |
//+------------------------------------------------------------------+
void UpdateDashboard()
{
string gate = InpUseModelGate
? StringFormat("ON (threshold %.2f)", InpConfidence)
: "OFF — raw EMA+RSI baseline";
string pos = "flat";
if(PositionSelectByMagic())
{
long type = PositionGetInteger(POSITION_TYPE);
int held = iBarShift(_Symbol, _Period,
(datetime)PositionGetInteger(POSITION_TIME));
pos = StringFormat("%s | %d/%d bars | P/L %.2f",
type==POSITION_TYPE_BUY ? "LONG" : "SHORT",
held, InpMaxHoldBars,
PositionGetDouble(POSITION_PROFIT));
}
Comment(StringFormat(
"\n EMA+RSI AutoML EA (Part 10)"
"\n --------------------------------------"
"\n Model gate : %s"
"\n Last signal : %s"
"\n Last P(profit) : %.3f"
"\n Last decision : %s"
"\n Signals seen : %d taken: %d (%.0f%%)"
"\n Position : %s",
gate, g_lastSignal, g_lastConfidence, g_lastDecision,
g_signalsSeen, g_signalsTaken,
g_signalsSeen > 0 ? 100.0 * g_signalsTaken / g_signalsSeen : 0.0,
pos));
}
//+------------------------------------------------------------------+