2026-10-21 18:04:53 +08:00 | | | //+------------------------------------------------------------------+
|
2026-10-31 17:54:51 +08:00 | | | //| PrgRules.mqh |
|
2026-10-21 18:04:53 +08:00 | | | //| Copyright 2026, Allan Niñal |
|
| | | //| https://www.allanninal.dev/mql5 |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Rule engine of Prop Rules Monitor. It only measures: day start, |
|
| | | //| daily and overall loss floors, trading days. It never trades. |
|
| | | //| |
|
| | | //| Daily floor = day-start reference - daily limit |
|
| | | //| reference: balance, equity or the higher of the two at the |
|
| | | //| reset instant (a time of day in a chosen time zone); |
|
| | | //| limit: % of the initial balance or of that reference. |
|
| | | //| Overall floor = initial - max% (static), or the peak (equity, |
|
| | | //| balance, or end-of-day balance) - max% of the initial, which |
|
| | | //| can stop rising once it reaches the initial balance. |
|
| | | //| Breach = equity at or below a floor (open trades count). |
|
| | | //| Times: deals and resets are compared in broker server time; the |
|
| | | //| reset zone is converted with built-in EU/US daylight-saving |
|
| | | //| rules. Nothing here uses the network. |
|
| | | //| MT4 edition: the same measurements. MT4 keeps orders, not deals: |
|
| | | //| the history scan reads balance operations and closed orders (P&L |
|
| | | //| at the close, commission included; a day is a trading day when a |
|
| | | //| trade was opened in it) and is rebuilt when the orders change; |
|
| | | //| the server clock is GMT + an offset learnt from a fresh tick. |
|
| | | //+------------------------------------------------------------------+
|
| | | // SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
|
2026-10-31 17:54:51 +08:00 | | | #ifndef PRG_RULES_MQH
|
| | | #define PRG_RULES_MQH
|
2026-10-21 18:04:53 +08:00 | | |
|
| | | //--- MT5 shows each enum choice by the comment on its own line, so every member needs one
|
| | | enum ENUM_PRG_REF
|
| | | {
|
| | | PRG_REF_BALANCE=0, // Balance at the reset
|
| | | PRG_REF_EQUITY=1, // Equity at the reset
|
| | | PRG_REF_HIGHER=2 // Higher of balance and equity at the reset
|
| | | };
|
| | | enum ENUM_PRG_OF
|
| | | {
|
| | | PRG_OF_INITIAL=0, // % of the initial balance (account size)
|
| | | PRG_OF_DAYSTART=1 // % of the day-start reference
|
| | | };
|
| | | enum ENUM_PRG_ZONE
|
| | | {
|
| | | PRG_ZONE_BROKER=0, // Broker server time
|
| | | PRG_ZONE_PRAGUE=1, // Central Europe (Prague, CET/CEST)
|
| | | PRG_ZONE_UTC=2, // UTC
|
| | | PRG_ZONE_NEWYORK=3, // New York (EST/EDT)
|
| | | PRG_ZONE_LONDON=4, // London (GMT/BST)
|
| | | PRG_ZONE_UTC3=5 // UTC+3 all year
|
| | | };
|
| | | enum ENUM_PRG_MAX
|
| | | {
|
| | | PRG_MAX_STATIC=0, // Static: initial balance - max %
|
| | | PRG_MAX_TRAIL_EQ=1, // Trailing from the highest equity
|
| | | PRG_MAX_TRAIL_BAL=2, // Trailing from the highest closed balance
|
| | | PRG_MAX_EOD=3, // Trailing from the highest end-of-day balance
|
| | | PRG_MAX_SMART=4 // Two-stage: max % until a gain, then a smaller fixed %
|
| | | };
|
| | | enum ENUM_PRG_CLOCK
|
| | | {
|
| | | PRG_CLOCK_AUTO=0, // Auto: detect from the live server time
|
| | | PRG_CLOCK_NY=1, // New York close: UTC+2/+3 with US daylight saving
|
| | | PRG_CLOCK_EET=2, // UTC+2/+3 with European daylight saving
|
| | | PRG_CLOCK_FIXED=3 // Fixed offset from UTC, no daylight saving
|
| | | };
|
| | |
|
| | | //--- one set of rules (the user's own)
|
2026-10-31 17:54:51 +08:00 | | | struct PrgRules
|
2026-10-21 18:04:53 +08:00 | | | {
|
| | | string name;
|
| | | double daily_pct; // 0 = no daily limit
|
| | | ENUM_PRG_REF daily_ref;
|
| | | ENUM_PRG_OF daily_of;
|
| | | ENUM_PRG_ZONE reset_zone;
|
| | | int reset_hour; // hour of the reset in that zone (0-23)
|
| | | double max_pct; // 0 = no overall limit
|
| | | ENUM_PRG_MAX max_mode;
|
| | | bool max_stop_at_initial; // a trailing floor stops rising at the initial balance
|
| | | double smart_gain_pct; // PRG_MAX_SMART: closed-balance gain that switches the floor ...
|
| | | double smart_after_pct; // ... to account size - this %
|
| | | double target_pct; // profit target, 0 = none
|
| | | int min_days; // minimum trading days, 0 = none
|
| | | };
|
| | |
|
| | | //--- what the engine measured, for the panel
|
| | | struct PrgState
|
| | | {
|
| | | double initial; // account size the % limits use
|
| | | bool initial_auto; // taken from the first deposit
|
| | | datetime day_start; // current reset instant, server time
|
| | | datetime next_reset;
|
| | | double ref_balance; // balance at the reset
|
| | | double ref_equity; // equity at the reset (= balance when not measured)
|
| | | bool ref_measured; // true when the program was running at the reset
|
| | | double daily_floor; // equity at or below this breaks the daily limit (-DBL_MAX = off)
|
| | | double daily_limit; // money
|
| | | double max_floor; // (-DBL_MAX = off)
|
| | | double peak_equity;
|
| | | double peak_balance;
|
| | | double peak_eod;
|
| | | double balance,equity;
|
| | | double today_closed; // closed P&L since the reset
|
| | | int trading_days; // days with at least one new trade
|
| | | double daily_used_pct; // 0..100+ of the daily limit
|
| | | double max_used_pct;
|
| | | };
|
| | |
|
2026-10-31 17:54:51 +08:00 | | | PrgRules g_rules;
|
2026-10-21 18:04:53 +08:00 | | | PrgState g_st;
|
| | |
|
| | | //--- clock of the broker server, resolved once
|
| | | ENUM_PRG_CLOCK g_prg_clock=PRG_CLOCK_NY;
|
| | | int g_prg_fixed=0; // seconds, PRG_CLOCK_FIXED
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Time zones |
|
| | | //+------------------------------------------------------------------+
|
| | | //--- Sunday on or after `day` of `month` in year `y`, at `hour` UTC
|
| | | datetime PrgSundayFrom(const int y,const int month,const int day,const int hour)
|
| | | {
|
| | | MqlDateTime t;
|
| | | ZeroMemory(t);
|
| | | t.year=y;
|
| | | t.mon=month;
|
| | | t.day=day;
|
| | | datetime d=StructToTime(t);
|
| | | TimeToStruct(d,t);
|
| | | return(d+(datetime)(((7-t.day_of_week)%7)*86400+hour*3600));
|
| | | }
|
| | |
|
| | | //--- US daylight saving: second Sunday of March 07:00 UTC to first Sunday of November 06:00 UTC
|
| | | bool PrgUsDst(const datetime utc)
|
| | | {
|
| | | MqlDateTime t;
|
| | | TimeToStruct(utc,t);
|
| | | return(utc>=PrgSundayFrom(t.year,3,8,7) && utc<PrgSundayFrom(t.year,11,1,6));
|
| | | }
|
| | |
|
| | | //--- EU summer time: last Sunday of March to last Sunday of October, 01:00 UTC
|
| | | bool PrgEuDst(const datetime utc)
|
| | | {
|
| | | MqlDateTime t;
|
| | | TimeToStruct(utc,t);
|
| | | return(utc>=PrgSundayFrom(t.year,3,25,1) && utc<PrgSundayFrom(t.year,10,25,1));
|
| | | }
|
| | |
|
| | | //--- offset of a named zone from UTC at UTC time `utc`, seconds
|
| | | int PrgZoneOffset(const ENUM_PRG_ZONE zone,const datetime utc)
|
| | | {
|
| | | switch(zone)
|
| | | {
|
| | | case PRG_ZONE_PRAGUE: return(PrgEuDst(utc) ? 2*3600 : 3600);
|
| | | case PRG_ZONE_LONDON: return(PrgEuDst(utc) ? 3600 : 0);
|
| | | case PRG_ZONE_NEWYORK: return(PrgUsDst(utc) ? -4*3600 : -5*3600);
|
| | | case PRG_ZONE_UTC3: return(3*3600);
|
| | | default: return(0);
|
| | | }
|
| | | }
|
| | |
|
| | | //--- the broker's offset from UTC at server time `srv`
|
| | | int PrgBrokerOffset(const datetime srv)
|
| | | {
|
| | | if(g_prg_clock==PRG_CLOCK_FIXED)
|
| | | return(g_prg_fixed);
|
| | | if(g_prg_clock==PRG_CLOCK_EET)
|
| | | return(PrgEuDst(srv-2*3600) ? 3*3600 : 2*3600);
|
| | | return(PrgUsDst(srv-2*3600) ? 3*3600 : 2*3600);
|
| | | }
|
| | |
|
| | | //--- decide the server clock: the input, or the live server time against UTC (the tester can't tell)
|
| | | void PrgResolveClock(const ENUM_PRG_CLOCK input_clock,const int fixed_hours)
|
| | | {
|
| | | g_prg_clock=input_clock;
|
| | | g_prg_fixed=fixed_hours*3600;
|
| | | if(input_clock!=PRG_CLOCK_AUTO)
|
| | | return;
|
| | | g_prg_clock=PRG_CLOCK_NY;
|
| | | if(IsTesting())
|
| | | return;
|
| | | datetime gmt=TimeGMT();
|
| | | long off=(long)TimeCurrent()-(long)gmt; // the last tick's server time (MQL4 has no TimeTradeServer)
|
| | | off=(long)MathRound(off/1800.0)*1800;
|
| | | if(MathAbs(off)>14*3600) // a frozen weekend server time: keep New York close
|
| | | return;
|
| | | int ny=PrgUsDst(gmt) ? 3*3600 : 2*3600;
|
| | | int eu=PrgEuDst(gmt) ? 3*3600 : 2*3600;
|
| | | if(off==ny)
|
| | | return;
|
| | | if(off==eu)
|
| | | {
|
| | | g_prg_clock=PRG_CLOCK_EET;
|
| | | return;
|
| | | }
|
| | | g_prg_clock=PRG_CLOCK_FIXED;
|
| | | g_prg_fixed=(int)off;
|
| | | }
|
| | |
|
| | | string PrgClockName(void)
|
| | | {
|
| | | switch(g_prg_clock)
|
| | | {
|
| | | case PRG_CLOCK_EET: return("UTC+2/+3 EU");
|
| | | case PRG_CLOCK_FIXED: return(StringFormat("UTC%+.1f",g_prg_fixed/3600.0));
|
| | | default: return("NY close");
|
| | | }
|
| | | }
|
| | |
|
| | | //--- floor `t` to the last instant whose time of day is `hour`:00 (all in one clock)
|
| | | datetime PrgFloorToHour(const datetime t,const int hour)
|
| | | {
|
| | | long k=(long)t-hour*3600;
|
| | | long m=k%86400;
|
| | | if(m<0)
|
| | | m+=86400;
|
| | | return((datetime)((long)t-m));
|
| | | }
|
| | |
|
| | | //--- the reset instant (server time) of the trading day that contains server time `srv`
|
| | | datetime PrgDayStart(const datetime srv)
|
| | | {
|
| | | if(g_rules.reset_zone==PRG_ZONE_BROKER)
|
| | | return(PrgFloorToHour(srv,g_rules.reset_hour));
|
| | | datetime utc=srv-PrgBrokerOffset(srv);
|
| | | datetime loc=utc+PrgZoneOffset(g_rules.reset_zone,utc);
|
| | | datetime rl=PrgFloorToHour(loc,g_rules.reset_hour); // reset, zone local time
|
| | | datetime ru=rl-PrgZoneOffset(g_rules.reset_zone,rl-PrgZoneOffset(g_rules.reset_zone,utc));
|
| | | return(ru+PrgBrokerOffset(ru+2*3600));
|
| | | }
|
| | |
|
| | | string PrgZoneName(const ENUM_PRG_ZONE z)
|
| | | {
|
| | | switch(z)
|
| | | {
|
| | | case PRG_ZONE_PRAGUE: return("Prague");
|
| | | case PRG_ZONE_UTC: return("UTC");
|
| | | case PRG_ZONE_NEWYORK: return("New York");
|
| | | case PRG_ZONE_LONDON: return("London");
|
| | | case PRG_ZONE_UTC3: return("UTC+3");
|
| | | default: return("server");
|
| | | }
|
| | | }
|
| | |
|
| | | //--- current server time: TimeCurrent() in the tester; live, MQL4 has no TimeTradeServer and TimeCurrent stops between
|
| | | //--- ticks (all weekend), so GMT + the server's offset, learnt from a fresh tick (a whole half hour, -12 h .. +14 h)
|
| | | datetime PrgNow(void)
|
| | | {
|
| | | static long off=LONG_MIN;
|
| | | if(IsTesting())
|
| | | return(TimeCurrent());
|
| | | long d=(long)TimeCurrent()-(long)TimeGMT();
|
| | | long r=(long)MathRound(d/1800.0)*1800;
|
| | | if(MathAbs(d-r)<=120 && r>=-12*3600 && r<=14*3600)
|
| | | off=r;
|
| | | return(off==LONG_MIN ? TimeCurrent() : (datetime)((long)TimeGMT()+off));
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Persistence: terminal global variables keyed by the login, so a |
|
| | | //| restart, re-attach or timeframe change keeps peaks and the day |
|
| | | //| start: PRG.<login>.MON.<field>. |
|
| | | //+------------------------------------------------------------------+
|
2026-10-31 17:54:51 +08:00 | | | string g_prg_ns=""; // "MON." (set before PrgInit)
|
| | |
|
2026-10-21 18:04:53 +08:00 | | | string PrgKey(const string field)
|
| | | {
|
2026-10-31 17:54:51 +08:00 | | | return("PRG."+IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN))+"."+g_prg_ns+field);
|
2026-10-21 18:04:53 +08:00 | | | }
|
| | |
|
| | | double PrgGet(const string field,const double def)
|
| | | {
|
| | | string k=PrgKey(field);
|
| | | return(GlobalVariableCheck(k) ? GlobalVariableGet(k) : def);
|
| | | }
|
| | |
|
| | | void PrgSet(const string field,const double v)
|
| | | {
|
| | | if(IsTesting() && IsOptimization())
|
| | | return;
|
| | | GlobalVariableSet(PrgKey(field),v);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| History scan: initial deposit, closed P&L per trading day, peak |
|
| | | //| balance, peak end-of-day balance, trading days. Called at start |
|
| | | //| and after every new deal; prop accounts have short histories. |
|
| | | //+------------------------------------------------------------------+
|
| | | double g_day_pnl[]; // closed P&L per trading day ...
|
| | | datetime g_day_key[]; // ... keyed by its reset instant
|
| | | bool g_day_traded[]; // a new position was opened that day
|
| | | double g_initial_input=0; // 0 = first deposit
|
| | |
|
| | | int PrgDaySlot(const datetime key)
|
| | | {
|
| | | int n=ArraySize(g_day_key);
|
| | | if(n>0 && g_day_key[n-1]==key)
|
| | | return(n-1);
|
| | | for(int i=n-1; i>=0; i--)
|
| | | if(g_day_key[i]==key)
|
| | | return(i);
|
| | | ArrayResize(g_day_key,n+1,64);
|
| | | ArrayResize(g_day_pnl,n+1,64);
|
| | | ArrayResize(g_day_traded,n+1,64);
|
| | | g_day_key[n]=key;
|
| | | g_day_pnl[n]=0;
|
| | | g_day_traded[n]=false;
|
| | | return(n);
|
| | | }
|
| | |
|
| | | //--- running totals of the scan, so each call reads only the deals added since the last one
|
| | | double g_scan_run=0,g_scan_peak=0,g_scan_eod=0,g_scan_day_end=0,g_scan_first=0;
|
| | | datetime g_scan_day=0,g_scan_time=0;
|
| | | double g_scan_base=0; // MT4: added to the running sums when no deposit is in the history
|
| | |
|
| | | void PrgScanReset(void)
|
| | | {
|
| | | ArrayResize(g_day_key,0);
|
| | | ArrayResize(g_day_pnl,0);
|
| | | ArrayResize(g_day_traded,0);
|
| | | g_scan_run=g_scan_peak=g_scan_eod=g_scan_day_end=g_scan_first=0;
|
| | | g_scan_day=g_scan_time=0;
|
| | | g_scan_base=0;
|
| | | }
|
| | |
|
| | | //--- the account's history as events in time order (MT4 keeps orders): a balance operation (kind 0), a closed trade's
|
| | | //--- P&L at its close (kind 1, profit + swap + commission), and the opening of a trade (kind 2, value 0), history and
|
| | | //--- open orders; in one second, balance first, then closes, then opens, then reading order
|
| | | int PrgEvents(datetime &t[],int &kind[],double &v[])
|
| | | {
|
| | | int nh=OrdersHistoryTotal(),no=OrdersTotal(),n=0;
|
| | | ArrayResize(t,2*(nh+no)); ArrayResize(kind,2*(nh+no)); ArrayResize(v,2*(nh+no));
|
| | | for(int pass=0; pass<2; pass++)
|
| | | for(int i=0; i<(pass==0 ? nh : no); i++)
|
| | | {
|
| | | if(!OrderSelect(i,SELECT_BY_POS,pass==0 ? MODE_HISTORY : MODE_TRADES))
|
| | | continue;
|
| | | int ty=OrderType();
|
| | | if(pass==0 && ty==6) // balance operation (deposit, withdrawal); 7 = credit: not balance
|
| | | { t[n]=OrderCloseTime()>0 ? OrderCloseTime() : OrderOpenTime(); kind[n]=0; v[n]=OrderProfit(); n++; continue; }
|
| | | if(ty>OP_SELL)
|
| | | continue;
|
| | | if(StringFind(OrderComment(),"from #")!=0) // the rest of a partly closed order is not a new trade
|
| | | { t[n]=OrderOpenTime(); kind[n]=2; v[n]=0; n++; }
|
| | | if(pass==0)
|
| | | { t[n]=OrderCloseTime(); kind[n]=1; v[n]=OrderProfit()+OrderSwap()+OrderCommission(); n++; }
|
| | | }
|
| | | double key[][2];
|
| | | ArrayResize(key,n);
|
| | | for(int i=0; i<n; i++)
|
| | | { key[i][0]=((double)t[i]*4.0+kind[i])*1048576.0+i; key[i][1]=i; }
|
| | | if(n>1)
|
| | | ArraySort(key);
|
| | | datetime t2[];
|
| | | int k2[];
|
| | | double v2[];
|
| | | ArrayResize(t2,n); ArrayResize(k2,n); ArrayResize(v2,n);
|
| | | for(int i=0; i<n; i++)
|
| | | { int j=(int)key[i][1]; t2[i]=t[j]; k2[i]=kind[j]; v2[i]=v[j]; }
|
| | | ArrayCopy(t,t2); ArrayCopy(kind,k2); ArrayCopy(v,v2);
|
| | | ArrayResize(t,n); ArrayResize(kind,n); ArrayResize(v,n);
|
| | | return(n);
|
| | | }
|
| | |
|
| | | void PrgScanHistory(void)
|
| | | {
|
| | | datetime now=PrgNow();
|
| | | PrgScanReset(); // MT4: no deal list to continue from - read it all again
|
| | | datetime et[];
|
| | | int ek[];
|
| | | double ev[];
|
| | | int n=PrgEvents(et,ek,ev);
|
| | | for(int i=0; i<n; i++)
|
| | | {
|
| | | datetime t=et[i];
|
| | | double v=ev[i];
|
| | | g_scan_time=MathMax(g_scan_time,t);
|
| | | datetime key=PrgDayStart(t);
|
| | | if(ek[i]==2) // an opening: a trading day, no money moved
|
| | | {
|
| | | g_day_traded[PrgDaySlot(key)]=true;
|
| | | continue;
|
| | | }
|
| | | if(g_scan_day!=0 && key!=g_scan_day) // a day ended: its closing balance
|
| | | g_scan_eod=MathMax(g_scan_eod,g_scan_day_end);
|
| | | g_scan_day=key;
|
| | | g_scan_run+=v;
|
| | | g_scan_day_end=g_scan_run;
|
| | | if(ek[i]==0)
|
| | | {
|
| | | if(g_scan_first==0 && v>0)
|
| | | g_scan_first=v;
|
| | | g_scan_peak=MathMax(g_scan_peak,g_scan_run);
|
| | | g_scan_eod=MathMax(g_scan_eod,g_scan_run);
|
| | | continue;
|
| | | }
|
| | | int sl=PrgDaySlot(key);
|
| | | g_day_pnl[sl]+=v;
|
| | | g_scan_peak=MathMax(g_scan_peak,g_scan_run);
|
| | | }
|
| | | //--- no deposit in the history (the Account History tab starts later, or the tester): the running sums are relative;
|
| | | //--- anchor them to the balance now, so balance peaks and end-of-day balances are real balances
|
| | | if(g_scan_first==0 && n>0)
|
| | | {
|
| | | double base=AccountInfoDouble(ACCOUNT_BALANCE)-g_scan_run;
|
| | | g_scan_run+=base; g_scan_peak+=base; g_scan_eod+=base; g_scan_day_end+=base;
|
| | | g_scan_base=base;
|
| | | }
|
| | | double bal=AccountInfoDouble(ACCOUNT_BALANCE);
|
| | | double first_dep=g_scan_first>0 ? g_scan_first : bal-(g_scan_run-g_scan_base); // tester or no deposit in history
|
| | | g_st.initial_auto=(g_initial_input<=0);
|
| | | g_st.initial=g_initial_input>0 ? g_initial_input : (first_dep>0 ? first_dep : bal);
|
| | | g_st.peak_balance=MathMax(MathMax(g_scan_peak,g_st.initial),bal);
|
| | | //--- end-of-day peak: days that closed before today's reset, plus the stored live value
|
| | | datetime today=PrgDayStart(now);
|
| | | double peak_eod=g_scan_eod;
|
| | | if(g_scan_day!=0 && g_scan_day<today)
|
| | | peak_eod=MathMax(peak_eod,g_scan_day_end);
|
| | | g_st.peak_eod=MathMax(MathMax(peak_eod,g_st.initial),MathMax(g_st.peak_eod,PrgGet("peak_eod",0)));
|
| | | PrgTotals(today);
|
| | | }
|
| | |
|
| | | //--- trading-day totals from the per-day table
|
| | | void PrgTotals(const datetime today)
|
| | | {
|
| | | g_st.trading_days=0;
|
| | | g_st.today_closed=0;
|
| | | for(int i=0; i<ArraySize(g_day_key); i++)
|
| | | {
|
| | | if(g_day_traded[i])
|
| | | g_st.trading_days++;
|
| | | if(g_day_key[i]==today)
|
| | | g_st.today_closed=g_day_pnl[i];
|
| | | }
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Day start: measured when the program runs through the reset, |
|
| | | //| otherwise rebuilt as the balance minus every deal since then. |
|
| | | //| Equity at a reset the program missed cannot be known: it then |
|
| | | //| equals the rebuilt balance and the panel says "rebuilt". |
|
| | | //+------------------------------------------------------------------+
|
| | | //--- money moved in the history at or after a time (closed P&L and balance operations)
|
| | | double PrgMovedSince(const datetime start)
|
| | | {
|
| | | double since=0;
|
| | | datetime et[];
|
| | | int ek[];
|
| | | double ev[];
|
| | | int n=PrgEvents(et,ek,ev);
|
| | | for(int i=0; i<n; i++)
|
| | | if(et[i]>=start)
|
| | | since+=ev[i];
|
| | | return(since);
|
| | | }
|
| | |
|
| | | void PrgRebuildDayStart(const datetime start)
|
| | | {
|
| | | g_st.ref_balance=AccountInfoDouble(ACCOUNT_BALANCE)-PrgMovedSince(start);
|
| | | g_st.ref_equity=g_st.ref_balance;
|
| | | g_st.ref_measured=false;
|
| | | }
|
| | |
|
| | | //--- start (or restart) the engine
|
| | | void PrgInit(void)
|
| | | {
|
| | | ZeroMemory(g_st);
|
| | | g_st.day_start=0;
|
| | | PrgScanReset();
|
| | | PrgScanHistory();
|
| | | g_st.peak_equity=MathMax(PrgGet("peak_eq",0),MathMax(g_st.peak_balance,AccountInfoDouble(ACCOUNT_EQUITY)));
|
| | | datetime now=PrgNow();
|
| | | datetime start=PrgDayStart(now);
|
| | | //--- a day start stored by a program that was running at the reset is kept
|
| | | if((datetime)PrgGet("day",0)==start)
|
| | | {
|
| | | g_st.ref_balance=PrgGet("day_bal",0);
|
| | | g_st.ref_equity=PrgGet("day_eq",0);
|
| | | g_st.ref_measured=PrgGet("day_meas",0)>0;
|
| | | }
|
| | | else
|
| | | {
|
| | | PrgRebuildDayStart(start);
|
| | | PrgSet("day",(double)start);
|
| | | PrgSet("day_bal",g_st.ref_balance);
|
| | | PrgSet("day_eq",g_st.ref_equity);
|
| | | PrgSet("day_meas",0);
|
| | | }
|
| | | g_st.day_start=start;
|
| | | g_st.next_reset=PrgDayStart(start+26*3600);
|
| | | if(g_st.next_reset<=start)
|
| | | g_st.next_reset=start+86400;
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Per tick / timer update. Returns true when a new day started. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool PrgUpdate(void)
|
| | | {
|
| | | bool new_day=false;
|
| | | datetime now=PrgNow();
|
| | | g_st.balance=AccountInfoDouble(ACCOUNT_BALANCE);
|
| | | g_st.equity=AccountInfoDouble(ACCOUNT_EQUITY);
|
| | | if(now>=g_st.next_reset)
|
| | | {
|
| | | //--- running through the reset: today's reference is measured, yesterday's close is an end-of-day value;
|
| | | //--- the first tick of the day can come late (a gap) with stops already filled after the reset: those belong
|
| | | //--- to the new day, so they are taken out of both
|
| | | g_st.day_start=PrgDayStart(now);
|
| | | g_st.next_reset=PrgDayStart(g_st.day_start+26*3600);
|
| | | if(g_st.next_reset<=g_st.day_start)
|
| | | g_st.next_reset=g_st.day_start+86400;
|
| | | double since=PrgMovedSince(g_st.day_start);
|
| | | g_st.peak_eod=MathMax(g_st.peak_eod,g_st.balance-since);
|
| | | PrgSet("peak_eod",g_st.peak_eod);
|
| | | g_st.ref_balance=g_st.balance-since;
|
| | | g_st.ref_equity=g_st.equity-since;
|
| | | g_st.ref_measured=true;
|
| | | g_st.today_closed=0;
|
| | | PrgSet("day",(double)g_st.day_start);
|
| | | PrgSet("day_bal",g_st.ref_balance);
|
| | | PrgSet("day_eq",g_st.ref_equity);
|
| | | PrgSet("day_meas",1);
|
| | | PrgTotals(g_st.day_start);
|
| | | new_day=true;
|
| | | }
|
| | | if(g_st.equity>g_st.peak_equity)
|
| | | {
|
| | | g_st.peak_equity=g_st.equity;
|
| | | PrgSet("peak_eq",g_st.peak_equity);
|
| | | }
|
| | | g_st.peak_balance=MathMax(g_st.peak_balance,g_st.balance);
|
| | | //--- daily floor
|
| | | g_st.daily_floor=-DBL_MAX;
|
| | | g_st.daily_limit=0;
|
| | | if(g_rules.daily_pct>0)
|
| | | {
|
| | | double ref=g_st.ref_balance;
|
| | | if(g_rules.daily_ref==PRG_REF_EQUITY)
|
| | | ref=g_st.ref_equity;
|
| | | else
|
| | | if(g_rules.daily_ref==PRG_REF_HIGHER)
|
| | | ref=MathMax(g_st.ref_balance,g_st.ref_equity);
|
| | | g_st.daily_limit=g_rules.daily_pct/100.0*(g_rules.daily_of==PRG_OF_INITIAL ? g_st.initial : ref);
|
| | | g_st.daily_floor=ref-g_st.daily_limit;
|
| | | g_st.daily_used_pct=g_st.daily_limit>0 ? 100.0*(ref-g_st.equity)/g_st.daily_limit : 0;
|
| | | }
|
| | | //--- overall floor
|
| | | g_st.max_floor=-DBL_MAX;
|
| | | if(g_rules.max_pct>0)
|
| | | {
|
| | | double amount=g_rules.max_pct/100.0*g_st.initial;
|
| | | double peak=g_st.initial;
|
| | | switch(g_rules.max_mode)
|
| | | {
|
| | | case PRG_MAX_TRAIL_EQ: peak=g_st.peak_equity; break;
|
| | | case PRG_MAX_TRAIL_BAL: peak=g_st.peak_balance; break;
|
| | | case PRG_MAX_EOD: peak=g_st.peak_eod; break;
|
| | | default: break;
|
| | | }
|
| | | g_st.max_floor=peak-amount;
|
| | | if(g_rules.max_mode==PRG_MAX_SMART && g_rules.smart_gain_pct>0 &&
|
| | | g_st.peak_balance>=g_st.initial*(1.0+g_rules.smart_gain_pct/100.0))
|
| | | {
|
| | | amount=g_rules.smart_after_pct/100.0*g_st.initial;
|
| | | g_st.max_floor=g_st.initial-amount;
|
| | | }
|
| | | if(g_rules.max_mode!=PRG_MAX_STATIC && g_rules.max_mode!=PRG_MAX_SMART && g_rules.max_stop_at_initial)
|
| | | g_st.max_floor=MathMin(g_st.max_floor,g_st.initial);
|
| | | //--- share of the loss budget used: 0 with the full amount of room, 100 at the floor
|
| | | g_st.max_used_pct=amount>0 ? 100.0*(amount-(g_st.equity-g_st.max_floor))/amount : 0;
|
| | | }
|
| | | return(new_day);
|
| | | }
|
| | |
|
| | | //--- equity at or below the daily floor
|
| | | bool PrgDailyBreached(void) { return(g_st.daily_floor>-DBL_MAX && g_st.equity<=g_st.daily_floor); }
|
| | | bool PrgMaxBreached(void) { return(g_st.max_floor>-DBL_MAX && g_st.equity<=g_st.max_floor); }
|
| | |
|
| | | //--- money left before a floor (DBL_MAX when the rule is off)
|
| | | double PrgDailyRoom(void) { return(g_st.daily_floor>-DBL_MAX ? g_st.equity-g_st.daily_floor : DBL_MAX); }
|
| | | double PrgMaxRoom(void) { return(g_st.max_floor>-DBL_MAX ? g_st.equity-g_st.max_floor : DBL_MAX); }
|
| | |
|
2026-10-31 17:54:51 +08:00 | | | //--- the smaller of the two rooms
|
| | | double PrgRoom(void) { return(MathMin(PrgDailyRoom(),PrgMaxRoom())); }
|
| | |
|
2026-10-21 18:04:53 +08:00 | | | #endif
|
| | | //+------------------------------------------------------------------+
|