//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+