Warrior_EA/Signals/SignalRiskGuard.mqh
AnimateDread 31b2711c8f feat: add daily-loss and max-drawdown risk circuit breakers
Audit turned up a real gap for a prop-firm-portfolio-manager use
case: nothing in this codebase watched for account-level daily-loss
or max-drawdown breaches - the single most common way a prop-firm
evaluation actually gets failed.

New Signals/SignalRiskGuard.mqh (CSignalRiskGuard), wired into the
exact same filter-composition chain as SignalNewsFilter/
SignalSessionFilter (CreateSignalWithRetry/AddFilterToSignal, no new
architecture). Vetoes new entries only (never closes existing
positions - a materially bigger behavior change, left to the
trader/EA's own SL/TP handling) once either MaxDailyLossPct or
MaxDrawdownPct (new RISK_LIMIT_PCT_PRESET inputs, both default
disabled) is breached. Peak equity and the current broker day's
starting balance persist to a small local per-symbol-per-magic state
file - peak equity in particular must survive a restart to mean
anything, otherwise a restart would silently reset drawdown tracking.

New RISK_LIMIT_PCT_PRESET enum (2/3/4/5/8/10/15/20%) rather than
reusing PERCENTAGE_PRESETS, which steps by 10 starting at 10 - too
coarse for prop-firm-style limits (commonly single-digit daily loss,
~8-10% max drawdown). Compiled clean (MetaEditor, 0 errors/0
warnings).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 17:29:38 -04:00

139 lines
6 KiB
MQL5

//+------------------------------------------------------------------+
//| SignalRiskGuard.mqh |
//| AnimateDread |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "AnimateDread"
#property link "https://www.mql5.com"
#include "..\Expert\ExpertSignalCustom.mqh"
//+------------------------------------------------------------------+
//| Class CSignalRiskGuard. |
//| Account-level risk circuit breaker: blocks NEW entries (same |
//| EMPTY_VALUE-veto convention every other filter here uses, via the |
//| composite Direction()) once either the current broker day's |
//| realized+floating loss, or the current drawdown from the account's|
//| all-time peak equity, exceeds its configured threshold. Deliberately|
//| scoped to blocking new entries only - it never closes existing |
//| positions, which is a materially bigger behavior change than a |
//| circuit breaker and is left to the trader/EA's own SL/TP handling. |
//| Peak equity and the current broker day's starting balance persist |
//| to a small local file (StateFileName(), keyed by symbol+magic so |
//| multiple instances never collide) - peak equity in particular must|
//| survive an EA/terminal restart to mean anything; a restart that |
//| reset it to current equity would never detect a real drawdown |
//| afterward. |
//+------------------------------------------------------------------+
class CSignalRiskGuard : public CExpertSignalCustom
{
protected:
double m_maxDailyLossPct;
double m_maxDrawdownPct;
double m_dayStartBalance;
datetime m_dayStartDate; // truncated to midnight - the broker day m_dayStartBalance is for
double m_peakEquity;
bool m_stateLoaded;
string StateFileName(void) const;
void LoadState(void);
void SaveState(void);
public:
CSignalRiskGuard(void);
void SetMaxDailyLossPct(int value) { m_maxDailyLossPct = value; }
void SetMaxDrawdownPct(int value) { m_maxDrawdownPct = value; }
virtual double Direction(void);
};
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
CSignalRiskGuard::CSignalRiskGuard(void) : m_maxDailyLossPct(0),
m_maxDrawdownPct(0),
m_dayStartBalance(0),
m_dayStartDate(0),
m_peakEquity(0),
m_stateLoaded(false)
{
}
//+------------------------------------------------------------------+
//| Local (non-shared) file, keyed by symbol+magic so multiple chart |
//| instances of this EA never read/write each other's risk state. |
//+------------------------------------------------------------------+
string CSignalRiskGuard::StateFileName(void) const
{
return m_symbol.Name() + "_" + IntegerToString(Expert_MagicNumber) + "_riskguard.dat";
}
//+------------------------------------------------------------------+
//| No prior state file just means a first-ever run - the all-zero |
//| defaults are safe: Direction() below seeds real values from the |
//| current account state on its very first call either way. |
//+------------------------------------------------------------------+
void CSignalRiskGuard::LoadState(void)
{
int handle = FileOpen(StateFileName(), FILE_BIN | FILE_READ);
if(handle == INVALID_HANDLE)
return;
m_dayStartBalance = FileReadDouble(handle);
m_dayStartDate = (datetime)FileReadLong(handle);
m_peakEquity = FileReadDouble(handle);
FileClose(handle);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CSignalRiskGuard::SaveState(void)
{
int handle = FileOpen(StateFileName(), FILE_BIN | FILE_WRITE);
if(handle == INVALID_HANDLE)
return;
FileWriteDouble(handle, m_dayStartBalance);
FileWriteLong(handle, (long)m_dayStartDate);
FileWriteDouble(handle, m_peakEquity);
FileClose(handle);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
double CSignalRiskGuard::Direction(void)
{
if(m_maxDailyLossPct <= 0.0 && m_maxDrawdownPct <= 0.0)
return 0.0; // both disabled - permanent no-op, same convention as DOM_SPREADMULT_OFF
if(!m_stateLoaded)
{
LoadState();
m_stateLoaded = true;
}
MqlDateTime now;
TimeToStruct(TimeCurrent(), now);
now.hour = 0;
now.min = 0;
now.sec = 0;
datetime todayMidnight = StructToTime(now);
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
bool stateChanged = false;
if(todayMidnight != m_dayStartDate)
{
m_dayStartDate = todayMidnight;
m_dayStartBalance = balance;
stateChanged = true;
}
if(equity > m_peakEquity)
{
m_peakEquity = equity;
stateChanged = true;
}
if(stateChanged)
SaveState();
if(m_maxDailyLossPct > 0.0 && m_dayStartBalance > 0.0)
{
double dailyLossPct = (m_dayStartBalance - equity) / m_dayStartBalance * 100.0;
if(dailyLossPct >= m_maxDailyLossPct)
return EMPTY_VALUE;
}
if(m_maxDrawdownPct > 0.0 && m_peakEquity > 0.0)
{
double drawdownPct = (m_peakEquity - equity) / m_peakEquity * 100.0;
if(drawdownPct >= m_maxDrawdownPct)
return EMPTY_VALUE;
}
return 0.0;
}
//+------------------------------------------------------------------+