| //+------------------------------------------------------------------+
|
| //| PropRulesMonitor.mq5 |
|
| //| Copyright 2026, Allan Niñal |
|
| //| https://www.allanninal.dev/mql5 |
|
| //+------------------------------------------------------------------+
|
| //| Free indicator: shows how close the account is to its prop-firm |
|
| //| daily loss limit and max loss, live, with alerts at a warning |
|
| //| level and at a breach. It only watches: it never opens, closes |
|
| //| or changes a trade, so it works without Algo Trading. The rules |
|
| //| are typed in by the user. Engine: MonEngine.mqh. |
|
| //| |
|
| //+------------------------------------------------------------------+
|
| // SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
|
| #property copyright "Copyright 2026, Allan Niñal <https://www.allanninal.dev/mql5>"
|
| #property link "https://www.allanninal.dev/mql5"
|
| #property version "1.2"
|
| #property description "Live prop-firm rule monitor: daily loss limit and max loss (static or trailing) measured on equity, with the firm's reset time and zone."
|
| #property description "Warning and breach alerts. Watches only - never trades."
|
| #property indicator_chart_window
|
| #property indicator_buffers 0
|
| #property indicator_plots 0
|
|
|
| #include <PropRulesMonitor/MonView.mqh>
|
| #include <ChartKit/CkDebug.mqh>
|
|
|
| input group "DAILY LOSS LIMIT"
|
| input double InpDailyPct = 5.0; // Daily loss limit, % (0 = off)
|
| input ENUM_PRG_OF InpDailyOf = PRG_OF_INITIAL; // Limit is
|
| input ENUM_PRG_REF InpDailyRef = PRG_REF_BALANCE; // Measured from
|
| input ENUM_PRG_ZONE InpResetZone = PRG_ZONE_PRAGUE; // Day resets in time zone
|
| input int InpResetHour = 0; // Day resets at hour (0-23)
|
|
|
| input group "MAX LOSS"
|
| input double InpMaxPct = 10.0; // Max loss, % of account size (0 = off)
|
| input ENUM_PRG_MAX InpMaxMode = PRG_MAX_STATIC; // Max loss type
|
| input bool InpMaxStop = false; // Trailing floor stops at the account size
|
| input double InpSmartGain = 5.0; // Two-stage: after a closed gain of %
|
| input double InpSmartAfter = 5.0; // Two-stage: max loss becomes %
|
|
|
| input group "ACCOUNT"
|
| input double InpAccount = 0; // Account size (0 = first deposit)
|
| input double InpTargetPct = 10.0; // Profit target, % (0 = none)
|
| input int InpMinDays = 4; // Minimum trading days (0 = none)
|
|
|
| input group "ALERTS (MetaTrader push settings)"
|
| input double InpWarnAt = 80; // Warn at % of a limit used
|
| input bool InpPopup = true; // Pop-up alert
|
| input bool InpPush = true; // Push notification
|
|
|
| input group "DISPLAY"
|
| input ENUM_PRG_CORNER InpCorner = PRG_LEFT; // Panel position
|
| input int InpOffsetX = 10; // Panel margin from the side, px
|
| input int InpOffsetY = 26; // Panel margin from the top, px
|
| input int InpFontSize = 9; // Panel font size
|
| input bool InpLight = false; // Light colours (for white charts)
|
|
|
| input group "SERVER CLOCK"
|
| input ENUM_PRG_CLOCK InpClock = PRG_CLOCK_AUTO; // Broker server clock
|
| input int InpFixedUTC = 0; // Broker offset from UTC, hours (Fixed only)
|
|
|
| input group "Support"
|
| input bool InpDebug = false; // Debug log for bug reports (Experts tab)
|
|
|
| //--- alert state for the current day (daily) and ever (max)
|
| datetime g_warned_day=0,g_breached_day=0;
|
| bool g_warned_max=false,g_breached_max=false;
|
| double g_last_bal=0;
|
| int g_last_pos=-1;
|
| int g_panel_h=200;
|
|
|
| void Notify(const string msg)
|
| {
|
| string m="Prop Rules Monitor: "+msg;
|
| if(InpPopup && !MQLInfoInteger(MQL_TESTER))
|
| Alert(m);
|
| else
|
| Print(m);
|
| if(InpPush && !MQLInfoInteger(MQL_TESTER))
|
| SendNotification(m);
|
| }
|
|
|
| void CheckAlerts(void)
|
| {
|
| if(g_rules.daily_pct>0)
|
| {
|
| if(PrgDailyBreached() && g_breached_day!=g_st.day_start)
|
| {
|
| g_breached_day=g_st.day_start;
|
| g_warned_day=g_st.day_start;
|
| CkDbg("daily limit breached");
|
| Notify(StringFormat("DAILY LIMIT BREACHED - equity %s at or below the floor %s",Money(g_st.equity),Money(g_st.daily_floor)));
|
| }
|
| else
|
| if(g_st.daily_used_pct>=InpWarnAt && g_warned_day!=g_st.day_start)
|
| {
|
| g_warned_day=g_st.day_start;
|
| Notify(StringFormat("%.0f%% of today's loss limit used - %s left",g_st.daily_used_pct,Money(PrgDailyRoom())));
|
| }
|
| }
|
| if(g_rules.max_pct>0)
|
| {
|
| if(PrgMaxBreached() && !g_breached_max)
|
| {
|
| g_breached_max=true;
|
| CkDbg("max loss breached");
|
| Notify(StringFormat("MAX LOSS BREACHED - equity %s at or below the floor %s",Money(g_st.equity),Money(g_st.max_floor)));
|
| }
|
| else
|
| if(g_st.max_used_pct>=InpWarnAt && !g_warned_max)
|
| {
|
| g_warned_max=true;
|
| Notify(StringFormat("%.0f%% of the max loss used - %s left",g_st.max_used_pct,Money(PrgMaxRoom())));
|
| }
|
| if(g_st.max_used_pct<InpWarnAt*0.9) // re-arm after a clear recovery
|
| g_warned_max=g_breached_max=false;
|
| }
|
| }
|
|
|
| void Draw(void)
|
| {
|
| string status="OK - within the limits";
|
| color clr=UiPos();
|
| if(PrgMaxBreached())
|
| {
|
| status="MAX LOSS BREACHED";
|
| clr=UiNeg();
|
| }
|
| else
|
| if(PrgDailyBreached())
|
| {
|
| status="DAILY LIMIT BREACHED";
|
| clr=UiNeg();
|
| }
|
| else
|
| if(g_st.daily_used_pct>=InpWarnAt || g_st.max_used_pct>=InpWarnAt)
|
| {
|
| status="WARNING - near a limit";
|
| clr=UiWarn();
|
| }
|
| g_cursor=0;
|
| DrawBack(g_panel_h);
|
| int y=DrawBody("Prop Rules Monitor","MONITOR",status,clr);
|
| y=Row("clock",y,"Server clock",PrgClockName(),UiDim());
|
| if(y!=g_panel_h)
|
| {
|
| g_panel_h=y;
|
| g_cursor=0;
|
| DrawBack(g_panel_h);
|
| }
|
| if(g_panel_dirty)
|
| {
|
| ChartRedraw();
|
| g_panel_dirty=false;
|
| }
|
| }
|
|
|
| datetime g_drawn=0; // tester time of the last panel repaint
|
| void Refresh(const bool draw=true)
|
| {
|
| double bal=AccountInfoDouble(ACCOUNT_BALANCE);
|
| int pos=PositionsTotal();
|
| if(bal!=g_last_bal || pos!=g_last_pos) // a deal happened: closed P&L and trading days change
|
| {
|
| g_last_bal=bal;
|
| g_last_pos=pos;
|
| PrgScanHistory();
|
| }
|
| PrgUpdate();
|
| CheckAlerts();
|
| if(draw)
|
| Draw();
|
| }
|
|
|
| int OnInit(void)
|
| {
|
| if(InpDailyPct<0 || InpDailyPct>100 || InpMaxPct<0 || InpMaxPct>100 || InpTargetPct<0 || InpWarnAt<=0 || InpWarnAt>100 || InpAccount<0)
|
| {
|
| Print("Prop Rules Monitor: a percentage input is outside 0-100 (warn level 1-100) or the account size is negative");
|
| return(INIT_PARAMETERS_INCORRECT);
|
| }
|
| g_prg_ns="MON."; // its own stored values
|
| g_rules.name="Custom";
|
| g_rules.daily_pct=MathMax(0,InpDailyPct);
|
| g_rules.daily_of=InpDailyOf;
|
| g_rules.daily_ref=InpDailyRef;
|
| g_rules.reset_zone=InpResetZone;
|
| g_rules.reset_hour=MathMax(0,MathMin(23,InpResetHour));
|
| g_rules.max_pct=MathMax(0,InpMaxPct);
|
| g_rules.max_mode=InpMaxMode;
|
| g_rules.max_stop_at_initial=InpMaxStop;
|
| g_rules.smart_gain_pct=MathMax(0,InpSmartGain);
|
| g_rules.smart_after_pct=MathMax(0,InpSmartAfter);
|
| g_rules.target_pct=MathMax(0,InpTargetPct);
|
| g_rules.min_days=MathMax(0,InpMinDays);
|
| g_initial_input=MathMax(0,InpAccount);
|
| g_warn_at=MathMax(1,MathMin(99,InpWarnAt));
|
| g_ui_corner=InpCorner;
|
| g_ui_x=InpOffsetX;
|
| g_ui_y=InpOffsetY;
|
| g_ui_font=MathMax(7,MathMin(14,InpFontSize));
|
| g_ui_light=InpLight;
|
| PrgResolveClock(InpClock,InpFixedUTC);
|
| PrgInit();
|
| CkDebugStart("Prop Rules Monitor","1.2",InpDebug,StringFormat("daily %.2f%%, max %.2f%% mode %d, warn %.0f%%, clock %d",InpDailyPct,InpMaxPct,(int)InpMaxMode,InpWarnAt,(int)InpClock));
|
| g_last_bal=AccountInfoDouble(ACCOUNT_BALANCE);
|
| g_last_pos=PositionsTotal();
|
| EventSetTimer(1);
|
| Refresh();
|
| return(INIT_SUCCEEDED);
|
| }
|
|
|
| void OnDeinit(const int reason)
|
| {
|
| EventKillTimer();
|
| PanelDelete();
|
| ChartRedraw();
|
| }
|
|
|
| void OnTimer(void)
|
| {
|
| Refresh(!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE));
|
| }
|
|
|
| int OnCalculate(const int rates_total,const int prev_calculated,const int begin,const double &price[])
|
| {
|
| //--- in the tester the limits are checked on every tick; the panel (about 20 formatted rows) is repainted only in the
|
| //--- visual tester and at most once per second of tester time: drawing on every tick made long tests 10-20x slower
|
| if(MQLInfoInteger(MQL_TESTER))
|
| {
|
| bool draw=false;
|
| if(MQLInfoInteger(MQL_VISUAL_MODE) && TimeCurrent()!=g_drawn)
|
| {
|
| g_drawn=TimeCurrent();
|
| draw=true;
|
| }
|
| Refresh(draw);
|
| }
|
| return(rates_total);
|
| }
|
| //+------------------------------------------------------------------+
|