ea-health-monitor/tests/mt4/HealthTrader.mq4
Allan Niñal Allan Niñal cbccffd5a3 EA Health Monitor: free MT5 and MT4 indicator source, Python reference, tests and README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 19:15:46 +08:00
206 lines
7.3 KiB
MQL4
//+------------------------------------------------------------------+
//| HealthTrader.mq4 |
//| Copyright 2026, Allan Niñal |
//| https://www.allanninal.dev/mql5 |
//+------------------------------------------------------------------+
//| Test EA (not shipped) for EA Health Monitor MT4, run in the MT4 |
//| Strategy Tester (tester trades only). It trades a seeded random |
//| mix on every new bar: several magic numbers and "manual" (magic |
//| 0) orders with comments, stop loss / take profit, partial closes |
//| ("from #" chains), full closes and close-by. Every InpSnapBars |
//| bars and at the end it writes a snapshot: |
//| <tag>_<SYM>_<TF>_<k>_orders.csv the raw orders (history in |
//| its reading order, then the open ones) |
//| <tag>_<SYM>_<TF>_<k>_stats.csv its own book (HealthLive.mqh |
//| EHRefresh: the whole history) |
//| and <tag>_<SYM>_<TF>_snaps.csv (k, now, day start). |
//| tests/mt4/check_mt4.py turns the raw orders into deals on its |
//| own and compares everything with tests/reference.py. |
//| Run it in the MT4 Strategy Tester (open prices), once per symbol |
//| with the inputs in tests/mt4/configs.tsv. |
//+------------------------------------------------------------------+
// SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
#property strict
#include "..\\..\\Indicators\\EAHealth\\HealthLive.mqh"
input string InpTag = "eh";
input int InpSeed = 1;
input int InpSnapBars = 400; // bars between snapshots
input int InpMaxOpen = 12; // most open orders
int g_k=0,g_bars=0,g_ops=0,g_fail=0;
int g_err[8]; // refusals by error code (the most common ones)
int g_errc[8];
datetime g_last_bar=0;
EHLive g_all;
string g_base;
int g_snap=INVALID_HANDLE;
bool g_done=false;
long MAGICS[4]={0,11,22,771500};
string COMMENTS[4]={"","TrendEA","Grid v2","Swing EA"};
int LOTS[5]={1,2,3,5,10}; // order sizes in minimum lots (the symbol's own lot rules)
int Rnd(const int n) { return(MathRand()%n); }
void Refused()
{
g_fail++;
int e=GetLastError();
for(int i=0; i<8; i++)
if(g_errc[i]==0 || g_err[i]==e)
{ g_err[i]=e; g_errc[i]++; return; }
}
int OnInit()
{
MathSrand(InpSeed);
g_base=StringFormat("%s_%s_%s",InpTag,_Symbol,StringSubstr(EnumToString((ENUM_TIMEFRAMES)_Period),7));
EHLiveInit(g_all,"");
g_snap=FileOpen(g_base+"_snaps.csv",FILE_WRITE|FILE_TXT|FILE_ANSI);
FileWriteString(g_snap,"k,now,day\n");
return(INIT_SUCCEEDED);
}
//--- the indicator through iCustom with its default inputs (it has no buffers: the call runs its OnCalculate, a smoke test;
//--- the MT4 tester gives it no chart, so its panel cannot be read)
void RunIndicator()
{
iCustom(NULL,0,"EAHealth\\EAHealthMonitor",0,0);
}
void Trade()
{
int r=Rnd(100);
RefreshRates();
if(r<35 && OrdersTotal()<InpMaxOpen)
{
int m=Rnd(4),type=Rnd(2);
double lots=MarketInfo(_Symbol,MODE_MINLOT)*LOTS[Rnd(5)],price=(type==OP_BUY ? Ask : Bid),sl=0,tp=0;
if(Rnd(10)<4)
{
double d=(100+Rnd(400))*_Point; // 100-500 points: some trades end at the stop or the target
sl=NormalizeDouble(type==OP_BUY ? price-d : price+d,_Digits);
tp=NormalizeDouble(type==OP_BUY ? price+d : price-d,_Digits);
}
g_ops++;
if(OrderSend(_Symbol,type,lots,price,50,sl,tp,COMMENTS[m],(int)MAGICS[m],0,clrNONE)<0)
Refused();
return;
}
int n=OrdersTotal();
if(n==0)
return;
if(!OrderSelect(Rnd(n),SELECT_BY_POS,MODE_TRADES) || OrderType()>OP_SELL)
return;
int tk=OrderTicket(),type=OrderType();
double lots=OrderLots(),price=(type==OP_BUY ? Bid : Ask);
double mn=MarketInfo(_Symbol,MODE_MINLOT),st=MarketInfo(_Symbol,MODE_LOTSTEP);
double half=MathFloor(lots/2.0/st+1e-9)*st;
if(r<60 && half>=mn-1e-9) // partial close: half, rounded down to the lot step
{
g_ops++;
if(!OrderClose(tk,NormalizeDouble(half,2),price,50,clrNONE))
Refused();
}
else
if(r<80)
{
g_ops++;
if(!OrderClose(tk,lots,price,50,clrNONE))
Refused();
}
else
if(r<88) // close by an opposite order
for(int i=0; i<n; i++)
if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES) && OrderType()<=OP_SELL && OrderType()!=type)
{
g_ops++;
if(!OrderCloseBy(tk,OrderTicket(),clrNONE))
Refused();
break;
}
}
void WriteOrders(const string name)
{
int f=FileOpen(name,FILE_WRITE|FILE_TXT|FILE_ANSI);
FileWriteString(f,"pool,ticket,type,open_time,close_time,magic,lots,profit,swap,commission,comment\n");
for(int pass=0; pass<2; pass++)
{
int n=(pass==0 ? OrdersHistoryTotal() : OrdersTotal());
for(int i=0; i<n; i++)
if(OrderSelect(i,SELECT_BY_POS,pass==0 ? MODE_HISTORY : MODE_TRADES))
{
string c=OrderComment();
StringReplace(c,",",";");
FileWriteString(f,StringFormat("%s,%d,%d,%I64d,%I64d,%d,%.2f,%.17g,%.17g,%.17g,%s\n",pass==0 ? "history" : "open",
OrderTicket(),OrderType(),(long)OrderOpenTime(),pass==0 ? (long)OrderCloseTime() : 0,
OrderMagicNumber(),OrderLots(),OrderProfit(),OrderSwap(),OrderCommission(),c));
}
}
FileClose(f);
}
void WriteBook(const int f,const string book,EHLive &L)
{
for(int i=0; i<L.b.nst; i++)
{
EHStat s=L.b.st[i];
string nm=EHName(L,s);
StringReplace(nm,",",";");
FileWriteString(f,StringFormat("%s,%I64d,%d,%d,%d,%d,%.17g,%.17g,%.17g,%.17g,%.17g,%.17g,%.17g,%I64d,%d,%.17g,%s\n",book,
s.magic,s.n,s.w,s.l,s.deals,s.gp,s.gl,s.net,s.curve,s.peak,s.maxdd,s.today,s.last,s.open,s.floating,nm));
}
}
void Snapshot()
{
RunIndicator();
EHRefresh(g_all);
string b=StringFormat("%s_%d",g_base,g_k);
WriteOrders(b+"_orders.csv");
int f=FileOpen(b+"_stats.csv",FILE_WRITE|FILE_TXT|FILE_ANSI);
FileWriteString(f,"book,magic,n,w,l,deals,gp,gl,net,curve,peak,maxdd,today,last,open,floating,name\n");
WriteBook(f,"all",g_all);
FileClose(f);
FileWriteString(g_snap,StringFormat("%d,%I64d,%I64d\n",g_k,(long)EHServerNow(),g_all.day));
g_k++;
}
void OnTick()
{
datetime bt=iTime(NULL,0,0);
if(bt==g_last_bar)
return;
g_last_bar=bt;
g_bars++;
Trade();
if(g_bars%InpSnapBars==0)
Snapshot();
}
void Finish()
{
if(g_done)
return;
g_done=true;
Snapshot();
FileClose(g_snap);
string why="";
for(int i=0; i<8 && g_errc[i]>0; i++)
why+=StringFormat(" %d x%d",g_err[i],g_errc[i]);
PrintFormat("Dump4 %s.csv: %d bars, %d trade operations (%d refused:%s), %d snapshots, history %d orders, open %d",
g_base,g_bars,g_ops,g_fail,why,g_k,OrdersHistoryTotal(),OrdersTotal());
}
double OnTester()
{
Finish();
return(0.0);
}
void OnDeinit(const int reason)
{
Finish();
}
//+------------------------------------------------------------------+