prop-rules-monitor/tests/mt4/PrgTrader.mq4
Allan Niñal Allan Niñal e3fd6fb07a Prop Rules Monitor: free MT5 and MT4 indicator source, engine tests and README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 18:04:53 +08:00
197 lines
7.1 KiB
MQL4
//+------------------------------------------------------------------+
//| PrgTrader.mq4 |
//| Copyright 2026, Allan Niñal |
//| https://www.allanninal.dev/mql5 |
//+------------------------------------------------------------------+
//| Test EA (not shipped) for the MT4 rule engine (MonEngine.mqh) |
//| of Prop Rules Monitor MT4, run in the MT4 Strategy Tester (tester|
//| trades only). It trades a seeded random mix on new bars (market |
//| orders, stop loss / take profit, partial and full closes) and |
//| runs the engine as the indicator does (scan on a balance or |
//| position change, update on every tick). Every InpSnapBars bars |
//| it writes the engine's history results to |
//| <tag>_<SYM>_<TF>_state.csv; at the end every order to |
//| <tag>_<SYM>_<TF>_orders.csv. tests/mt4/check_mt4.py rebuilds the |
//| results from the orders on its own. The indicator is not run |
//| beside it: both would share the stored values (PRG.*.MON.*). |
//+------------------------------------------------------------------+
// SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
#property strict
#include <PropRulesMonitor\MonEngine.mqh>
input string InpTag = "prg";
input int InpSeed = 1;
input int InpSnapBars = 7; // bars between snapshots (7 = every hour of the day in turn)
input int InpMaxOpen = 6;
input ENUM_PRG_ZONE InpResetZone = PRG_ZONE_PRAGUE;
input int InpResetHour = 0;
input ENUM_PRG_CLOCK InpClock = PRG_CLOCK_AUTO; // AUTO = New York close in the tester
string g_base;
int g_f=INVALID_HANDLE,g_bars=0,g_ops=0,g_fail=0,g_snaps=0;
datetime g_last_bar=0;
double g_last_bal=0;
int g_last_pos=-1;
bool g_done=false;
int LOTS[4]={1,1,2,4}; // in minimum lots
int Rnd(const int n) { return(MathRand()%n); }
int MarketOrders(void)
{
int n=0;
for(int i=OrdersTotal()-1; i>=0; i--)
if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES) && OrderType()<=OP_SELL)
n++;
return(n);
}
int OnInit()
{
MathSrand(InpSeed);
GlobalVariablesDeleteAll(PrgKey("")); // the MT4 tester keeps global variables from the previous run
g_rules.name="Test";
g_rules.daily_pct=5;
g_rules.daily_of=PRG_OF_INITIAL;
g_rules.daily_ref=PRG_REF_BALANCE;
g_rules.reset_zone=InpResetZone;
g_rules.reset_hour=InpResetHour;
g_rules.max_pct=10;
g_rules.max_mode=PRG_MAX_EOD;
PrgResolveClock(InpClock,0);
PrgInit();
g_base=StringFormat("%s_%s_%s",InpTag,_Symbol,TfName());
g_f=FileOpen(g_base+"_state.csv",FILE_WRITE|FILE_TXT|FILE_ANSI);
FileWriteString(g_f,"now,balance,day_start,next_reset,initial,peak_balance,peak_eod,"
"trading_days,today_closed,rebuilt_ref,final,days\n");
return(INIT_SUCCEEDED);
}
string TfName(void)
{
switch(_Period)
{
case PERIOD_M15: return("M15");
case PERIOD_H1: return("H1");
case PERIOD_H4: return("H4");
}
return(IntegerToString(_Period));
}
void Trade(void)
{
int r=Rnd(100);
RefreshRates();
double mn=MarketInfo(_Symbol,MODE_MINLOT),st=MarketInfo(_Symbol,MODE_LOTSTEP);
if(r<30 && MarketOrders()<InpMaxOpen)
{
int type=Rnd(2);
double lots=mn*LOTS[Rnd(4)],price=(type==OP_BUY ? Ask : Bid),sl=0,tp=0;
if(Rnd(10)<5)
{
double d=(100+Rnd(400))*_Point;
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,NormalizeDouble(lots,2),price,50,sl,tp,"prg test",Rnd(3),0,clrNONE)<0)
g_fail++;
return;
}
int n=OrdersTotal();
if(n==0 || !OrderSelect(Rnd(n),SELECT_BY_POS,MODE_TRADES) || OrderType()>OP_SELL)
return;
int tk=OrderTicket();
double lots=OrderLots(),price=(OrderType()==OP_BUY ? Bid : Ask);
double half=MathFloor(lots/2.0/st+1e-9)*st;
if(r<50 && half>=mn-1e-9)
{
g_ops++;
if(!OrderClose(tk,NormalizeDouble(half,2),price,50,clrNONE))
g_fail++;
}
else
if(r<75)
{
g_ops++;
if(!OrderClose(tk,lots,price,50,clrNONE))
g_fail++;
}
}
void Snapshot(const bool last=false)
{
//--- the rebuilt day start (as PrgInit does after a restart), without touching the engine's own reference
PrgState keep=g_st;
PrgRebuildDayStart(PrgDayStart(PrgNow()));
double rebuilt=g_st.ref_balance;
g_st=keep;
string days="";
for(int i=0; i<ArraySize(g_day_key); i++)
days+=StringFormat("%s%I64d:%.2f:%d",i ? ";" : "",(long)g_day_key[i],g_day_pnl[i],g_day_traded[i] ? 1 : 0);
FileWriteString(g_f,StringFormat("%I64d,%.2f,%I64d,%I64d,%.2f,%.2f,%.2f,%d,%.2f,%.2f,%d,",
(long)PrgNow(),AccountBalance(),(long)g_st.day_start,(long)g_st.next_reset,g_st.initial,g_st.peak_balance,
g_st.peak_eod,g_st.trading_days,g_st.today_closed,rebuilt,last ? 1 : 0));
FileWriteString(g_f,days+"\n"); // apart: MQL4's StringFormat stops at about 4 KB
g_snaps++;
}
void OnTick()
{
//--- the engine as the indicator runs it
double bal=AccountBalance();
int pos=MarketOrders();
if(bal!=g_last_bal || pos!=g_last_pos)
{
g_last_bal=bal;
g_last_pos=pos;
PrgScanHistory();
}
PrgUpdate();
datetime bt=iTime(NULL,0,0);
if(bt==g_last_bar)
return;
g_last_bar=bt;
g_bars++;
if(g_bars%InpSnapBars==0)
Snapshot();
Trade();
}
void Finish(void)
{
if(g_done)
return;
g_done=true;
PrgScanHistory();
PrgUpdate();
Snapshot(true); // after the tester closed what was open
FileClose(g_f);
int f=FileOpen(g_base+"_orders.csv",FILE_WRITE|FILE_TXT|FILE_ANSI);
FileWriteString(f,"pool,ticket,type,open_time,close_time,lots,profit,swap,commission,comment\n");
for(int pass=0; pass<2; pass++)
for(int i=0; i<(pass==0 ? OrdersHistoryTotal() : OrdersTotal()); 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,%.2f,%.8f,%.8f,%.8f,%s\n",pass==0 ? "history" : "open",OrderTicket(),
OrderType(),(long)OrderOpenTime(),pass==0 ? (long)OrderCloseTime() : 0,OrderLots(),OrderProfit(),
OrderSwap(),OrderCommission(),c));
}
FileClose(f);
PrintFormat("Dump4 %s.csv: %d bars, %d trade operations (%d refused), %d snapshots, clock %s, reset %s %02d:00, history %d orders",
g_base,g_bars,g_ops,g_fail,g_snaps,PrgClockName(),PrgZoneName(g_rules.reset_zone),g_rules.reset_hour,OrdersHistoryTotal());
}
double OnTester()
{
Finish();
return(0.0);
}
void OnDeinit(const int reason)
{
Finish();
}
//+------------------------------------------------------------------+