signal-trader/mql5/Experts/SignalTrader/SignalTrader.mqh
Allan Niñal Allan Niñal 0ae6daef7a Signal Trader: free MT5 EA source, Python reference of the engines and rule, tests and README
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 16:08:01 +08:00
479 lines
18 KiB
MQL5
//+------------------------------------------------------------------+
//| SignalTrader.mqh |
//| Copyright 2026, Allan Niñal |
//| https://www.allanninal.dev/mql5 |
//+------------------------------------------------------------------+
//| Signal Trader: trades the closed-bar signals of one of three |
//| engines (UT Bot, SuperTrend, Range Filter), embedded. |
//| Automation, not a strategy with an edge: no profit is promised. |
//| |
//| Rules, identical in tests/reference.py: |
//| * a signal on a closed bar is acted on at the next bar's open; |
//| * an opposite position is closed (and reversed), a same-side |
//| signal is ignored, otherwise a market order is sent; |
//| * stop = stop multiple x ATR(14, Wilder) of the signal bar, |
//| optional target = x times the stop distance. |
//| Fixed lot (0.01 or the broker minimum), one source at a time. |
//| Tester: when the pair is not one of the validated presets the EA |
//| still trades (so the Market validator can run it); live it |
//| refuses new orders there. |
//+------------------------------------------------------------------+
// SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
#property strict
#include <Trade/Trade.mqh>
#include "StEngines.mqh"
#include "StPanel.mqh"
#include "StPresets.mqh"
#define ST_NAME "Signal Trader"
input group "Signal"
input ENUM_ST_SOURCE InpSource = ST_SRC_UT; // Signal source
input double InpUtKey = 2.0; // UT Bot: key value (ATR multiple)
input int InpUtATR = 10; // UT Bot: ATR period
input bool InpUtEMA = false; // UT Bot: only with the EMA(200) trend
input int InpStATR = 10; // SuperTrend: ATR period
input double InpStMult = 3.0; // SuperTrend: ATR multiplier
input bool InpStEMA = false; // SuperTrend: only with the EMA(200) trend
input int InpRfPeriod = 100; // Range Filter: sampling period
input double InpRfMult = 3.0; // Range Filter: range multiplier
input group "Trade"
input double InpStopATR = 2.0; // Stop loss, x ATR(14) of the signal bar
input double InpTpR = 0.0; // Take profit, x the stop distance (0 = none)
input long InpMagic = 26101701;// Magic number
input int InpSlippage = 30; // Maximum slippage, points
input group "Display"
input bool InpPanel = true; // Show the panel
input bool InpDebug = false; // Log every signal and trade (Experts tab)
#define ST_HIST 3000 // closed bars the engines are warmed up with
#define ST_MAX_STOP_PCT 50.0 // never open a trade whose stop would lose more than this % of equity
CTrade g_trade;
CEng *g_eng=NULL;
datetime g_bar0=0; // open time of the current bar
datetime g_last_fed=0; // time of the last closed bar fed to the engines
int g_last_sig=0;
datetime g_last_sig_bar=0;
string g_msg="";
datetime g_msg_time=0;
int g_panel_h=200;
bool g_validated=true;
int g_pend=0; // a signal waiting for the market to open (valid for the current bar only)
datetime g_pend_bar=0;
//+------------------------------------------------------------------+
datetime NowSrv(void) { return(MQLInfoInteger(MQL_TESTER) ? TimeCurrent() : TimeTradeServer()); }
string TfName(const ENUM_TIMEFRAMES tf) { return(StringSubstr(EnumToString(tf==PERIOD_CURRENT ? (ENUM_TIMEFRAMES)_Period : tf),7)); }
string Stamp(const datetime t) { return(TimeToString(t,TIME_DATE|TIME_MINUTES)); }
void Say(const string m,const string full="")
{
g_msg=m;
g_msg_time=TimeLocal();
if(InpDebug || full!="")
Print(ST_NAME+": "+(full!="" ? full : m));
}
bool MarketOpen(void)
{
datetime now=NowSrv();
MqlDateTime dt;
TimeToStruct(now,dt);
int tod=(int)(now%86400);
datetime from,to;
for(int s=0; SymbolInfoSessionTrade(_Symbol,(ENUM_DAY_OF_WEEK)dt.day_of_week,s,from,to); s++)
{
int f=(int)(from%86400),t=(int)(to%86400);
if(t==0)
t=86400;
if(f<t ? (tod>=f && tod<t) : (tod>=f || tod<t))
return(true);
}
return(false);
}
double NormPrice(const double p)
{
double ts=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE);
if(ts>0)
return(NormalizeDouble(MathRound(p/ts)*ts,_Digits));
return(NormalizeDouble(p,_Digits));
}
//--- money lost from `price` to `sl` by one lot
double LossPerLot(const double price,const double sl)
{
double tick=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE);
double tv=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_VALUE_LOSS);
if(tv<=0)
tv=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_VALUE);
double by_tick=(tick>0 ? MathAbs(price-sl)/tick : 0)*tv;
double p=0;
if(price!=sl && OrderCalcProfit(sl<price ? ORDER_TYPE_BUY : ORDER_TYPE_SELL,_Symbol,1.0,price,sl,p))
return(MathMax(by_tick,MathAbs(p)));
return(by_tick);
}
int LotDigits(void)
{
double step=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);
int d=0;
while(d<8 && step>0 && MathAbs(step*MathPow(10,d)-MathRound(step*MathPow(10,d)))>1e-9)
d++;
return(d);
}
//--- the volume for an entry at `price` with stop `sl`; 0 = refuse (message in `why`)
double EntryLots(const bool buy,const double price,const double sl,string &why)
{
double vmin=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN),vmax=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX);
double step=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);
why="";
double lots=MathMax(0.01,vmin); // 0.01, or the broker minimum when that is larger
if(step>0)
lots=MathCeil(lots/step-1e-9)*step;
lots=NormalizeDouble(MathMin(lots,vmax),8);
if(LossPerLot(price,sl)*lots>AccountInfoDouble(ACCOUNT_EQUITY)*ST_MAX_STOP_PCT/100.0)
{ why=StringFormat("the stop would lose more than %.0f%% of equity",ST_MAX_STOP_PCT); return(0); }
double m=0;
if(!OrderCalcMargin(buy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL,_Symbol,lots,price,m) || m>AccountInfoDouble(ACCOUNT_MARGIN_FREE)*0.95)
{ why="not enough free margin"; return(0); }
return(lots);
}
//--- this EA's position on the symbol: ticket (0 = none), direction +1/-1
ulong FindPos(int &dir)
{
dir=0;
for(int i=PositionsTotal()-1; i>=0; i--)
{
ulong t=PositionGetTicket(i);
if(t==0 || PositionGetString(POSITION_SYMBOL)!=_Symbol || PositionGetInteger(POSITION_MAGIC)!=InpMagic)
continue;
dir=(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY ? 1 : -1);
return(t);
}
return(0);
}
//+------------------------------------------------------------------+
//| Engines |
//+------------------------------------------------------------------+
CEng *MakeEngine(const ENUM_ST_SOURCE s)
{
switch(s)
{
case ST_SRC_UT: return(new CUtBot(InpUtKey,InpUtATR,InpUtEMA));
case ST_SRC_ST: return(new CSuperTrend(InpStATR,InpStMult,InpStEMA));
default: return(new CRangeFilter(InpRfPeriod,InpRfMult));
}
}
//--- warm the engine up with the last ST_HIST closed bars
bool WarmUp(void)
{
MqlRates r[];
ArraySetAsSeries(r,false);
int n=CopyRates(_Symbol,_Period,1,ST_HIST,r);
if(n<300)
return(false);
g_eng.Clear();
for(int i=0; i<n; i++)
{
g_eng.Feed(r[i].open,r[i].high,r[i].low,r[i].close);
}
g_last_fed=r[n-1].time;
return(true);
}
//--- feed the closed bars that arrived since the last call; returns the signal of the newest closed bar
int FeedNew(datetime &sig_bar)
{
sig_bar=0;
MqlRates r[];
ArraySetAsSeries(r,false);
int n=CopyRates(_Symbol,_Period,1,40,r);
if(n<1)
return(0);
if(r[n-1].time<=g_last_fed)
return(0);
if(r[0].time>g_last_fed && g_last_fed>0)
{
WarmUp(); // missed more than 40 bars: rebuild, no trade on this one
return(0);
}
int sig=0;
for(int i=0; i<n; i++)
{
if(r[i].time<=g_last_fed)
continue;
sig=g_eng.Feed(r[i].open,r[i].high,r[i].low,r[i].close);
sig_bar=r[i].time;
if(i<n-1)
sig=0; // a catch-up bar: only the newest one is traded
}
g_last_fed=r[n-1].time;
return(sig);
}
//+------------------------------------------------------------------+
//| Entry |
//+------------------------------------------------------------------+
//--- why a new entry is not allowed right now ("" = allowed)
string EntryBlocked(void)
{
if(!MQLInfoInteger(MQL_TESTER) && !TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
return("Algo Trading is off");
if(!MarketOpen())
return("market closed");
if(!g_validated && !MQLInfoInteger(MQL_TESTER))
return("pair is not a validated preset");
return("");
}
bool OpenTrade(const int dir,const double atr)
{
double ask=SymbolInfoDouble(_Symbol,SYMBOL_ASK),bid=SymbolInfoDouble(_Symbol,SYMBOL_BID);
if(ask<=0 || bid<=0)
return(false);
bool buy=(dir>0);
double price=buy ? ask : bid;
double dist=InpStopATR*atr;
//--- never closer than the broker allows: stop level + spread
double minDist=((double)SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)+(double)SymbolInfoInteger(_Symbol,SYMBOL_TRADE_FREEZE_LEVEL))*_Point+(ask-bid)+2*_Point;
if(dist<minDist)
{
PrintFormat("CHK stop widened from %s to %s (broker minimum)",DoubleToString(dist,_Digits),DoubleToString(minDist,_Digits));
dist=minDist;
}
double sl=NormPrice(buy ? price-dist : price+dist);
double tp=(InpTpR>0 ? NormPrice(buy ? price+InpTpR*dist : price-InpTpR*dist) : 0);
string why;
double lots=EntryLots(buy,price,sl,why);
if(lots<=0)
{
Say("skipped: "+why,"entry skipped: "+why);
return(false);
}
bool ok=(buy ? g_trade.Buy(lots,_Symbol,0,sl,tp,ST_NAME) : g_trade.Sell(lots,_Symbol,0,sl,tp,ST_NAME));
if(!ok)
{
Say("refused, code "+IntegerToString(g_trade.ResultRetcode()),"order refused ("+g_trade.ResultRetcodeDescription()+")");
return(false);
}
Say((buy ? "BUY " : "SELL ")+DoubleToString(lots,LotDigits()),
StringFormat("%s %s lots at %s, stop %s%s",buy ? "BUY" : "SELL",DoubleToString(lots,LotDigits()),DoubleToString(price,_Digits),
DoubleToString(sl,_Digits),tp>0 ? ", target "+DoubleToString(tp,_Digits) : ""));
return(true);
}
void HandleSignal(const int sig,const datetime bar)
{
g_last_sig=sig;
g_last_sig_bar=bar;
static datetime dbg_bar=0;
if(InpDebug && bar!=dbg_bar)
Print("DBG ",Stamp(bar),": signal ",sig>0 ? "BUY" : "SELL");
dbg_bar=bar;
if(!MarketOpen())
{
g_pend=sig; // retried on the next ticks of this bar, dropped at the next bar
g_pend_bar=bar;
return;
}
g_pend=0;
int pdir;
ulong pt=FindPos(pdir);
if(pt!=0)
{
if(pdir==sig)
return; // same side: ignored
if(!g_trade.PositionClose(pt))
{
Say("close refused, code "+IntegerToString(g_trade.ResultRetcode()),"close refused ("+g_trade.ResultRetcodeDescription()+")");
return;
}
}
string b=EntryBlocked();
if(b!="")
{
Say("skipped: "+b,InpDebug ? "entry skipped: "+b : "");
return;
}
double atr=g_eng.atr[g_eng.n-1];
if(atr==EMPTY_VALUE || atr<=0)
return;
OpenTrade(sig,atr);
}
//+------------------------------------------------------------------+
//| Panel |
//+------------------------------------------------------------------+
void Draw(void)
{
g_cursor=0;
Box("bg",0,0,UI_W,g_panel_h+UI_PAD,UiBack(),UiRule());
Box("hd",0,0,UI_W,UI_HEAD-4,g_ui_light ? C'240,241,244' : C'32,35,40',UiRule());
Text("title",UI_PAD,5,"Signal Trader",UiText(),g_ui_font+1,"Segoe UI Semibold");
Text("ed",UI_W-UI_PAD-28,7,"FREE",UiDim(),g_ui_font-1);
int y=Section("s_sig",UI_HEAD-6,"SIGNAL");
y=Row("r_src",y,"Source",g_eng.Describe(),UiText());
y=Row("r_tf",y,"Chart",_Symbol+" "+TfName((ENUM_TIMEFRAMES)_Period),UiText());
y=Row("r_pair",y,"Preset",g_validated ? "validated" : "not validated",g_validated ? UiPos() : UiWarn());
string last=(g_last_sig==0 ? "none yet" : (g_last_sig>0 ? "BUY " : "SELL ")+Stamp(g_last_sig_bar));
y=Row("r_last",y,"Last signal",last,g_last_sig>0 ? UiPos() : (g_last_sig<0 ? UiNeg() : UiDim()));
y=Section("s_pos",y,"POSITION");
int dir;
ulong t=FindPos(dir);
if(t!=0)
{
y=Row("p_side",y,"Side",dir>0 ? "BUY" : "SELL",dir>0 ? UiPos() : UiNeg());
y=Row("p_vol",y,"Volume",DoubleToString(PositionGetDouble(POSITION_VOLUME),LotDigits()),UiText());
y=Row("p_sl",y,"Stop loss",DoubleToString(PositionGetDouble(POSITION_SL),_Digits),UiText());
double pl=PositionGetDouble(POSITION_PROFIT)+PositionGetDouble(POSITION_SWAP);
y=Row("p_pl",y,"Open P/L",SMoney(pl)+" "+AccountInfoString(ACCOUNT_CURRENCY),Sign(pl));
}
else
{
y=Row("p_side",y,"Side","flat",UiDim());
y=Row("p_vol",y,"Volume","-",UiDim());
y=Row("p_sl",y,"Stop loss","-",UiDim());
y=Row("p_pl",y,"Open P/L","-",UiDim());
}
y=Section("s_st",y,"STATE");
y=Row("s_lot",y,"Lot","0.01",UiText());
if(g_msg!="" && TimeLocal()-g_msg_time<30)
y=Row("msg2",y,"Last action",g_msg,UiDim());
else
if(ObjectFind(0,SGT_PREFIX+"msg2_l")>=0)
{
ObjectDelete(0,SGT_PREFIX+"msg2_l");
ObjectDelete(0,SGT_PREFIX+"msg2_v");
CacheClear();
}
if(y!=g_panel_h)
{
g_panel_h=y;
CacheClear();
Draw();
return;
}
if(g_panel_dirty)
{
ChartRedraw();
g_panel_dirty=false;
}
}
//+------------------------------------------------------------------+
//| Events |
//+------------------------------------------------------------------+
int OnInit()
{
if(InpStopATR<=0 || InpTpR<0 || InpUtKey<=0 || InpUtATR<1 || InpStATR<1 || InpStMult<=0 || InpRfPeriod<2 || InpRfMult<=0)
{
Print(ST_NAME+": check the inputs (stop, target, periods and multipliers must be positive)");
return(INIT_PARAMETERS_INCORRECT);
}
g_eng=MakeEngine(InpSource);
g_trade.SetExpertMagicNumber(InpMagic);
g_trade.SetDeviationInPoints(InpSlippage);
g_trade.SetTypeFillingBySymbol(_Symbol);
g_trade.LogLevel(LOG_LEVEL_NO);
g_validated=StPresetValidated(_Symbol,(ENUM_TIMEFRAMES)_Period,InpSource);
if(!WarmUp())
Print(ST_NAME+": waiting for history (",_Symbol," ",TfName((ENUM_TIMEFRAMES)_Period),")");
g_bar0=iTime(_Symbol,_Period,0);
PrintFormat("%s: %s on %s %s, %s",ST_NAME,g_eng.Describe(),_Symbol,TfName((ENUM_TIMEFRAMES)_Period),
g_validated ? "validated preset" : "NOT a validated preset");
if(!g_validated && !MQLInfoInteger(MQL_TESTER))
Alert(ST_NAME+": "+_Symbol+" "+TfName((ENUM_TIMEFRAMES)_Period)+" with this source is not a validated preset. No orders will be sent.");
if(InpPanel && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)))
{
g_ui_light=(ChartGetInteger(0,CHART_COLOR_BACKGROUND)==clrWhite);
g_ui_corner=SGT_RIGHT;
EventSetTimer(2);
Draw();
}
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason)
{
EventKillTimer();
if(CheckPointer(g_eng)==POINTER_DYNAMIC)
delete g_eng;
PanelDelete();
}
void OnTick()
{
if(g_eng.n==0 && !WarmUp())
return;
datetime b=iTime(_Symbol,_Period,0);
if(g_pend!=0 && b==g_bar0 && g_pend_bar==iTime(_Symbol,_Period,1) && MarketOpen())
{
int ps=g_pend;
g_pend=0;
HandleSignal(ps,g_pend_bar);
}
if(b==0 || b==g_bar0)
return;
g_pend=0;
g_bar0=b;
datetime sb;
int sig=FeedNew(sb);
if(sig!=0 && sb==iTime(_Symbol,_Period,1))
HandleSignal(sig,sb);
}
void OnTimer()
{
if(InpPanel && g_eng!=NULL && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)))
Draw();
}
//--- debug: one line per closed trade (entry and exit time, side, prices, stop at entry) for the parity check
void OnTradeTransaction(const MqlTradeTransaction &trans,const MqlTradeRequest &request,const MqlTradeResult &result)
{
if(!InpDebug || trans.type!=TRADE_TRANSACTION_DEAL_ADD || trans.symbol!=_Symbol)
return;
if(!HistoryDealSelect(trans.deal) || HistoryDealGetInteger(trans.deal,DEAL_MAGIC)!=InpMagic)
return;
if(HistoryDealGetInteger(trans.deal,DEAL_ENTRY)!=DEAL_ENTRY_OUT)
return;
ulong pid=(ulong)HistoryDealGetInteger(trans.deal,DEAL_POSITION_ID);
if(!HistorySelectByPosition(pid))
return;
double pin=0,sl=0,vol=0,swap=0,profit=0;
datetime tin=0,tout=0;
int side=0;
double pout=0;
for(int i=0; i<HistoryDealsTotal(); i++)
{
ulong d=HistoryDealGetTicket(i);
long e=HistoryDealGetInteger(d,DEAL_ENTRY);
if(e==DEAL_ENTRY_IN)
{
pin=HistoryDealGetDouble(d,DEAL_PRICE);
tin=(datetime)HistoryDealGetInteger(d,DEAL_TIME);
side=(HistoryDealGetInteger(d,DEAL_TYPE)==DEAL_TYPE_BUY ? 1 : -1);
vol=HistoryDealGetDouble(d,DEAL_VOLUME);
ulong o=(ulong)HistoryDealGetInteger(d,DEAL_ORDER);
if(HistoryOrderSelect(o))
sl=HistoryOrderGetDouble(o,ORDER_SL);
}
else
if(e==DEAL_ENTRY_OUT)
{
pout=HistoryDealGetDouble(d,DEAL_PRICE);
tout=(datetime)HistoryDealGetInteger(d,DEAL_TIME);
profit+=HistoryDealGetDouble(d,DEAL_PROFIT);
}
swap+=HistoryDealGetDouble(d,DEAL_SWAP);
}
PrintFormat("TRD %s %s %s %s %.*f %.*f %.*f %.2f %.2f",TimeToString(tin,TIME_DATE|TIME_MINUTES),TimeToString(tout,TIME_DATE|TIME_MINUTES),
side>0 ? "BUY" : "SELL",DoubleToString(vol,2),_Digits,pin,_Digits,pout,_Digits,sl,profit,swap);
}
//+------------------------------------------------------------------+