//+------------------------------------------------------------------+ //| 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 | //| ___state.csv; at the end every order to | //| ___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 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()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