//+------------------------------------------------------------------+ //| 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 #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)); } //+------------------------------------------------------------------+