Engineering Trading Discipline into Code (Part 7): Automating Equity Protection Through Governance Logic
Contents
- Introduction
- Why Governance Matters in Automated Trading
- System Architecture
- Governance Logic Design
- Implementation in MQL5
- Backtesting Results
- Conclusion
Introduction
A profitable trading strategy is not necessarily a resilient trading system. Many Expert Advisors implement sound entry and exit logic yet still suffer severe equity deterioration because they lack mechanisms that control behavior during adverse conditions. During drawdowns or rapid equity declines, an EA may continue trading aggressively, re-enter too quickly after losses, or fail to reduce exposure when account pressure increases.
Human traders often respond to such situations through discipline: slowing down, reducing risk, and waiting for recovery before resuming normal activity. Automated systems have no such instinctive safeguards. If discipline is to exist in algorithmic trading, it must be explicitly implemented in code.
This article introduces an equity governance framework for MQL5 that acts as an independent supervisory layer above the trading strategy. The framework monitors peak equity, drawdown percentage, daily loss percentage, and rapid equity deterioration, then maps these signals into four execution states: NORMAL, CAUTION, RESTRICTED, and LOCKDOWN.
As account pressure increases, the system progressively applies trading restrictions, activates recovery cooldowns, and determines whether new orders should be authorized. To keep integration simple, the framework exposes a single authorization interface that can be called immediately before trade execution.
The goal is straightforward: enforce disciplined, state-driven risk control through code, allowing automated trading systems to reduce exposure during periods of elevated equity stress without modifying their underlying entry and exit logic.
Why Governance Matters in Automated Trading
One of the biggest misconceptions in algorithmic trading is that profitability alone is enough to keep a system alive. In reality, many automated systems fail not because the strategy logic is weak, but because there is no internal governance controlling how the system behaves under pressure.
In most cases, problems begin to appear when the system encounters unstable market conditions or prolonged equity stress. Without protective control layers, an EA may continue operating with the same level of aggression regardless of what is happening to the account. This often leads to behaviors such as:
- Increasing exposure during drawdown periods
- Opening trades too frequently after consecutive losses
- Continuing execution under unstable volatility conditions
- Failing to slow down after rapid equity decline
- Accumulating correlated exposure across positions
- Remaining active when account conditions already require defensive behavior
Over time, these behaviors can compound losses much faster than the strategy itself can recover from them.
Financial markets have already shown what happens when automation operates without sufficient control layers. A well-known example is the 2012 Knight Capital Group trading incident, where a software deployment failure triggered uncontrolled automated executions in live markets. Within roughly 45 minutes, the firm lost over $440 million. The losses were driven by runaway trading and the lack of immediate protective controls.
This incident became one of the clearest demonstrations that automated systems require more than signal generation alone. They also require mechanisms capable of supervising exposure, restricting unstable execution behavior, and protecting capital during abnormal operating conditions.
System Architecture
The framework is organized as a modular control structure. Each module handles a specific responsibility, allowing the system to monitor account conditions, interpret equity pressure, and apply execution control in a structured way.
The architecture is divided into several core layers, as shown in the diagram below.

This separation keeps the framework clean, scalable, and easier to maintain as additional governance features are introduced later.
Governance Logic Design
The governance logic defines how the framework reacts when account pressure begins to increase. Instead of allowing the trading system to operate with the same behavior under all market conditions, the framework adjusts execution based on equity stability.
The model is built around four operating states:

State transitions are driven by:
- Current drawdown percentage
- Rapid equity decline detection
- Recovery threshold validation
- Cooldown timing conditions
- Stability confirmation logic
The framework also controls how recovery takes place. The system does not immediately return to normal operation after a small improvement in equity conditions. Recovery is handled gradually through cooldown timing and stability checks to avoid rapid state flipping.
This allows the governance layer to behave more like a structured risk supervisor rather than a simple trade blocker. The objective is to maintain controlled execution behavior during unstable conditions while protecting account stability over time.
Implementation in MQL5
The Equity Governor has two parts: a core library with governance logic and an application layer that assembles and runs the components. This separation keeps business rules cleanly isolated from execution flow, making the code easier to understand, test, and extend.
The library file (EquityGovernor_Classes.mqh) defines nine specialized classes that handle equity tracking, drawdown analysis, state transitions, cooldown timers, trade restrictions, authorization, logging, and visualization. The Expert Advisor file ( EquityGovernor.mq5 ) wires the classes together, exposes user inputs, and updates the system on ticks and timers. Both files work together, but they remain independent in role.

1. EquityGovernor_Classes.mqh — The Governance Library
This file is the brain of the system. Each class focuses on a single aspect of risk management, and all classes communicate through clearly defined interfaces. The modular design allows any component to be replaced or upgraded without touching the others.

Class Overview
| Class | Responsibility |
|---|---|
| CEquityMonitor | Tracks current equity, peak, drawdown low, and daily starting balance. Returns drawdown and daily loss as percentages. |
| CDrawdownAnalyzer | Compares drawdown against user thresholds and returns a pressure level (NORMAL, CAUTION, RESTRICTED, LOCKDOWN). Also detects rapid equity collapses over short intervals. |
| CRecoveryCooldown | Manages timed lockdown and restricted cooldowns, and checks whether equity has recovered enough to allow a state downgrade. |
| CGovernanceStateEngine | The core state machine that transitions between NORMAL → CAUTION → RESTRICTED → LOCKDOWN using drawdown pressure and rapid‑collapse flags. |
| CRestrictionEnforcer | Enforces trading limits in CAUTION mode: lot multiplier, maximum open positions, and minimum time between trades. |
| CTradeAuthorizer | Central trade gatekeeper. Combines state permission, restriction rules, and hard limits (max drawdown, max daily loss) to grant or deny a trade. |
| CEventLogger | Logs governance events to the Experts tab. |
| CVisualizer | Paints a fixed panel dashboard on the chart showing state, drawdown, equity, peak, daily loss, trade permission, and remaining cooldown. |
| CTestTradingEngine | A built‑in SMA crossover strategy that demonstrates how the governor authorizes or blocks trades in real time. |
CEquityMonitor — Equity & Daily Tracking
This class is the data source for the entire system. It reads ACCOUNT_EQUITY on every update and maintains three running values: peak equity, drawdown low, and daily starting balance. Peak equity is the highest value observed, drawdown trough is the lowest equity observed since the last peak, and daily starting balance is recorded at midnight. No buffers or history are stored; only the essential extremes are kept.
//+------------------------------------------------------------------+ //| CEquityMonitor | //+------------------------------------------------------------------+ class CEquityMonitor { private: double m_equity; double m_peak; double m_drawdownLow; double m_dailyStart; int m_lastDay; public: CEquityMonitor(); void Init(); void Update(); //--- Returns current equity double GetEquity() const { return m_equity; } //--- Returns peak equity reached since start double GetPeak() const { return m_peak; } double GetDrawdownPercent() const; double GetDailyLossPercent() const; }; //+------------------------------------------------------------------+ //| CEquityMonitor::Constructor | //+------------------------------------------------------------------+ CEquityMonitor::CEquityMonitor() { m_equity = 0; m_peak = 0; m_drawdownLow = 0; m_dailyStart = 0; m_lastDay = -1; } //+------------------------------------------------------------------+ //| CEquityMonitor::Init | //+------------------------------------------------------------------+ void CEquityMonitor::Init() { Update(); m_peak = m_equity; m_drawdownLow = m_equity; m_dailyStart = m_equity; } //+------------------------------------------------------------------+ //| CEquityMonitor::Update | //+------------------------------------------------------------------+ void CEquityMonitor::Update() { m_equity = AccountInfoDouble(ACCOUNT_EQUITY); if(m_equity > m_peak) m_peak = m_equity; if(m_equity < m_drawdownLow) m_drawdownLow = m_equity; MqlDateTime tm; TimeCurrent(tm); if(m_lastDay != tm.day) { m_lastDay = tm.day; m_dailyStart = m_equity; } } //+------------------------------------------------------------------+ //| CEquityMonitor::GetDrawdownPercent | //+------------------------------------------------------------------+ double CEquityMonitor::GetDrawdownPercent() const { if(m_peak <= 0) return 0; return (m_peak - m_equity) / m_peak * 100.0; } //+------------------------------------------------------------------+ //| CEquityMonitor::GetDailyLossPercent | //+------------------------------------------------------------------+ double CEquityMonitor::GetDailyLossPercent() const { if(m_dailyStart <= 0 || m_equity >= m_dailyStart) return 0; return (m_dailyStart - m_equity) / m_dailyStart * 100.0; }
GetDrawdownPercent() calculates (peak - equity) / peak * 100, and GetDailyLossPercent() returns the loss since the daily start, or 0 if equity is above it. These two percentages are consumed by every other class — the whole governor reacts to nothing else.
CDrawdownAnalyzer — Pressure & Rapid Collapse
The analyzer takes the drawdown percentage from the monitor and maps it to a four‑level pressure scale using three user‑configurable thresholds. It also contains a rapid‑collapse detector that detects flash crashes before the static thresholds react.
//+------------------------------------------------------------------+ //| CDrawdownAnalyzer::PressureToString | //+------------------------------------------------------------------+ string CDrawdownAnalyzer::PressureToString(ENUM_DD_PRESSURE p) { switch(p) { case DD_NORMAL: return "NORMAL"; case DD_CAUTION: return "CAUTION"; case DD_RESTRICTED: return "RESTRICTED"; case DD_LOCKDOWN: return "LOCKDOWN"; default: return "UNKNOWN"; } }
IsRapidCollapse() records the equity once every m_rapidSec seconds (default 10). If within that window the equity drops by m_rapidPct% (default 3%), the method returns true – signaling an immediate state upgrade, regardless of the current drawdown percentage. This makes the system sensitive to sharp moves that can wipe out an account before a deeper DD% is reached.
CRecoveryCooldown — Timers & Recovery Checks
Two independent cooldowns (lockdown and restricted) prevent the state from bouncing back too soon. The recovery check adds an extra condition: equity must be within a user‑defined percentage of the peak before leaving RESTRICTED.
//+------------------------------------------------------------------+ //| CRecoveryCooldown::CanDowngradeFromRestricted | //+------------------------------------------------------------------+ bool CRecoveryCooldown::CanDowngradeFromRestricted() { if(TimeCurrent() < m_restrictedEnd) return false; if(m_mon == NULL) return true; double peak = m_mon->GetPeak(); double cur = m_mon->GetEquity(); if(peak <= 0) return true; return (cur / peak) >= m_recoveryThreshold; }
When lockdown starts, m_lockdownEnd is set to TimeCurrent() + m_lockdownSec. GetLockdownRem() returns the remaining seconds, used by the dashboard. The cooldown object is passed to the state engine; the engine calls StartLockdown() when entering LOCKDOWN and StartRestricted() when entering RESTRICTED.
CGovernanceStateEngine — The State Machine
This is the central decision‑maker. It receives a pressure level and a rapid‑collapse flag, then moves between four states using a hysteresis pattern. A stable‑count mechanism (default 3 ticks) prevents rapid toggling between NORMAL and CAUTION.
//+------------------------------------------------------------------+ //| CGovernanceStateEngine::Update | //+------------------------------------------------------------------+ void CGovernanceStateEngine::Update(ENUM_DD_PRESSURE pressure, bool rapid) { ENUM_GOV_STATE newState = m_state; switch(m_state) { case GOV_NORMAL: if(pressure == DD_LOCKDOWN) newState = GOV_LOCKDOWN; else if(pressure >= DD_RESTRICTED) newState = GOV_RESTRICTED; else if(pressure >= DD_CAUTION || rapid) newState = GOV_CAUTION; break; case GOV_CAUTION: if(pressure == DD_NORMAL) { m_stableCount++; if(m_stableCount >= 3) newState = GOV_NORMAL; } else { m_stableCount = 0; if(pressure >= DD_RESTRICTED || rapid) newState = GOV_RESTRICTED; if(pressure == DD_LOCKDOWN) newState = GOV_LOCKDOWN; } break; case GOV_RESTRICTED: if(rapid || pressure == DD_LOCKDOWN) { newState = GOV_LOCKDOWN; if(m_cd != NULL) m_cd->StartLockdown(); } else if(pressure == DD_NORMAL && m_cd != NULL && m_cd->CanDowngradeFromRestricted()) { newState = GOV_CAUTION; m_stableCount = 0; if(m_cd != NULL) m_cd->ResetRestricted(); } break; case GOV_LOCKDOWN: if(m_cd != NULL && m_cd->LockdownExpired() && pressure <= DD_NORMAL) { newState = GOV_RESTRICTED; if(m_cd != NULL) m_cd->StartRestricted(); } break; } if(newState != m_state) { m_prev = m_state; m_state = newState; m_stableCount = 0; } }
Notice that RESTRICTED upgrades to LOCKDOWN if a rapid collapse is detected, even if the static drawdown hasn’t reached the lockdown threshold. LOCKDOWN releases to RESTRICTED only after the timer expires and pressure is back to NORMAL — the governor requires a calm market before easing restrictions.
CRestrictionEnforcer — Trading Limits in CAUTION
When the state is CAUTION, this class applies a lot multiplier, limits open positions, and enforces a minimum time between trades.
//+------------------------------------------------------------------+ //| CRestrictionEnforcer::IsTradeAllowed | //+------------------------------------------------------------------+ bool CRestrictionEnforcer::IsTradeAllowed(ENUM_GOV_STATE state, int currentPositions) { if(state == GOV_NORMAL) return true; if(state == GOV_CAUTION) { if(currentPositions >= m_maxPositions) return false; if(m_lastTrade != 0 && (TimeCurrent() - m_lastTrade) < m_minSec) return false; return true; } return false; }
GetLotMult() returns 1.0 for NORMAL, the configured multiplier (default 0.5) for CAUTION, and 0.0 for anything stricter. This ensures the trader can never increase exposure during drawdowns.
CTradeAuthorizer — Central Gatekeeper
The authorizer combines the state engine, restriction enforcer, and equity monitor into a single permission check. It also enforces hard limits (max drawdown, max daily loss) that block all trades regardless of state.
//+------------------------------------------------------------------+ //| CTradeAuthorizer::Authorize | //+------------------------------------------------------------------+ ENUM_AUTH_RESULT CTradeAuthorizer::Authorize(double lot, string &reason, int positions) { if(m_state == NULL || m_enforcer == NULL || m_mon == NULL) { reason = "Init fail"; return AUTH_DENIED_STATE; } ENUM_GOV_STATE state = m_state->Get(); if(!m_enforcer->IsTradeAllowed(state, positions)) { reason = "State " + CGovernanceStateEngine::ToString(state); return AUTH_DENIED_STATE; } if(m_mon->GetDrawdownPercent() >= m_maxDD) { reason = "DD limit"; return AUTH_DENIED_DRAWDOWN; } if(m_mon->GetDailyLossPercent() >= m_maxDailyLoss) { reason = "Daily loss limit"; return AUTH_DENIED_DAILY_LOSS; } reason = ""; return AUTH_GRANTED; }
The hard limits are an absolute backstop — they fire instantly even if the state machine hasn’t yet reached LOCKDOWN, protecting against a single extreme candle.
CEventLogger & CVisualizer — Output & Dashboard
CEventLogger is a thin wrapper around Print() that prepends [GOV] to every message. CVisualizer draws a fixed‑position panel using chart objects (OBJ_RECTANGLE_LABEL and OBJ_LABEL). The panel shows the current state (color‑coded), drawdown, equity, peak, daily loss, trade permission, and remaining lockdown time.
//+------------------------------------------------------------------+ //| CVisualizer::Refresh | //+------------------------------------------------------------------+ void CVisualizer::Refresh() { if(m_state == NULL || m_mon == NULL) return; UpdatePanel(); ENUM_GOV_STATE s = m_state->Get(); color col = StateColor(s); double dd = m_mon->GetDrawdownPercent(); double eq = m_mon->GetEquity(); double peak = m_mon->GetPeak(); double daily = m_mon->GetDailyLossPercent(); bool newTrades; if(m_enf != NULL) newTrades = m_enf->IsTradeAllowed(s, PositionsTotal()); else newTrades = true; int cd = 0; if(m_cd != NULL) cd = m_cd->GetLockdownRem(); string cdStr = "NONE"; if(cd > 0) cdStr = StringFormat("%02d:%02d", cd / 60, cd % 60); int y = m_y; Label("head", "===============================", m_x, y, clrLightGray, 8); y += VIS_LINE_HEIGHT; Label("st", "STATE : " + CGovernanceStateEngine::ToString(s), m_x, y, col, 10); y += VIS_LINE_HEIGHT; Label("dd", "DD : " + DoubleToString(dd, 2) + "%", m_x, y, clrWhite, 9); y += VIS_LINE_HEIGHT; Label("eq", "Equity: " + DoubleToString(eq, 2), m_x, y, clrWhite, 9); y += VIS_LINE_HEIGHT; Label("pk", "Peak : " + DoubleToString(peak, 2), m_x, y, clrLightGray, 9); y += VIS_LINE_HEIGHT; Label("dl", "DailyL: " + DoubleToString(daily, 2) + "%", m_x, y, (daily > 3) ? clrOrange : clrWhite, 9); y += VIS_LINE_HEIGHT; Label("tr", "Trades: " + (newTrades ? "ALLOWED" : "BLOCKED"), m_x, y, newTrades ? clrLightGreen : clrRed, 9); y += VIS_LINE_HEIGHT; Label("cd", "Cooldown: " + cdStr, m_x, y, clrYellow, 9); y += VIS_LINE_HEIGHT; Label("foot", "===============================", m_x, y, clrLightGray, 8); }
All objects are prefixed with "GOV_" so they can be deleted in one call on deinitialization. The panel works independently of the test engine, giving the trader a real‑time health monitor.
CTestTradingEngine — Built‑In Strategy
The test engine uses a 20/50 SMA crossover with an optional 200‑SMA trend filter. Before placing any trade, it calls m_auth.Authorize(). If permission is granted, it calculates lot size (adjusted by the enforcer’s multiplier), sets SL and TP, and sends a market order.
//+------------------------------------------------------------------+ //| CTestTradingEngine::OnTick | //+------------------------------------------------------------------+ void CTestTradingEngine::OnTick() { if(m_auth == NULL || !m_indicatorsValid) return; ManageTrailingStop(); datetime bar = iTime(_Symbol, _Period, 0); if(bar == m_lastBar) return; m_lastBar = bar; double fast[2], slow[2]; if(CopyBuffer(m_fastHandle, 0, 1, 2, fast) != 2) return; if(CopyBuffer(m_slowHandle, 0, 1, 2, slow) != 2) return; bool trendUp = true, trendDown = false; if(m_useTrendFilter) { double trend[1]; if(CopyBuffer(m_trendHandle, 0, 0, 1, trend) != 1) return; double currentPrice = (SymbolInfoDouble(_Symbol, SYMBOL_ASK) + SymbolInfoDouble(_Symbol, SYMBOL_BID)) / 2.0; trendUp = (currentPrice > trend[0]); trendDown = (currentPrice < trend[0]); } bool wasAbove = (fast[1] > slow[1]); bool nowAbove = (fast[0] > slow[0]); int signal = 0; if(!wasAbove && nowAbove && trendUp) signal = 1; else if(wasAbove && !nowAbove && trendDown) signal = -1; if(signal == 0) return; string reason; int positions = PositionsTotal(); ENUM_AUTH_RESULT ar = m_auth->Authorize(m_lot, reason, positions); if(ar != AUTH_GRANTED) { Print("[GOV] Trade blocked: ", reason); return; } double mult = m_auth->GetLotMultiplier(); double rawLot = m_lot * mult; if(rawLot <= 0) return; double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); double finalLot = MathFloor(rawLot / lotStep) * lotStep; finalLot = MathMax(minLot, MathMin(maxLot, finalLot)); if(finalLot <= 0) return; int orderType = (signal > 0) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL; double price = (orderType == ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID); double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); double slDist = m_slPips * point * 10; double tpDist = m_tpPips * point * 10; double sl = 0, tp = 0; if(orderType == ORDER_TYPE_BUY) { sl = price - slDist; tp = price + tpDist; } else { sl = price + slDist; tp = price - tpDist; } long fillFlags = SymbolInfoInteger(_Symbol, SYMBOL_FILLING_MODE); ENUM_ORDER_TYPE_FILLING fill = ORDER_FILLING_RETURN; if((fillFlags & SYMBOL_FILLING_FOK) == SYMBOL_FILLING_FOK) fill = ORDER_FILLING_FOK; else if((fillFlags & SYMBOL_FILLING_IOC) == SYMBOL_FILLING_IOC) fill = ORDER_FILLING_IOC; MqlTradeRequest req = {0}; MqlTradeResult res = {0}; req.action = TRADE_ACTION_DEAL; req.symbol = _Symbol; req.volume = finalLot; req.type = (ENUM_ORDER_TYPE)orderType; req.price = price; req.sl = sl; req.tp = tp; req.deviation = 10; req.type_filling = fill; if(OrderSend(req, res)) { if(m_enf != NULL) m_enf->RecordTrade(); } else { Print("[GOV] Order failed: retcode=", res.retcode, " comment=", res.comment); } }
The engine also manages trailing stops on open positions using CTrade::PositionModify(). This keeps the test EA realistic, demonstrating that the governor only blocks new entries, not position management.
2. EquityGovernor.mq5 — The Application Layer
The EA file acts as the integration layer. It starts by including the governance library, defines all user inputs, creates the library objects, and connects them. Its four event handlers orchestrate the whole system. Below we walk through the EA from top to bottom.

Header, Properties, and Include
The file begins with the standard MQL5 header block that identifies the EA, sets the copyright, version, and compilation strictness. Immediately after, we include the governance library — this single line imports all the classes described above into the EA’s scope.
//+------------------------------------------------------------------+ //| EquityGovernor.mq5 | //| Copyright 2026, Christian Benjamin. | //| https://www.mql5.com/en/users/lynnchris | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Christian Benjamin." #property link "https://www.mql5.com/en/users/lynnchris" #property version "1.0" #property strict #include <GOVERNOR/EquityGovernor_Classes.mqh>
By placing the classes in a separate .mqh file, the EA stays clean and focused only on configuration and event handling.
Input Parameters
All thresholds, limits, and options are exposed as input variables so the trader can adjust them without touching the code. The inputs are grouped logically: master switch, drawdown thresholds, cooldown & recovery, restrictions, hard limits, test engine, stress mode, dashboard & logging, and exit parameters. Each input includes a short inline comment explaining its purpose.
// ---- Master ON/OFF switch input bool InpEnableGoverning = true; // true = Governor active, false = always NORMAL // ---- Realistic governor thresholds input double InpDD_NormalMax = 2.0; // 2% drawdown triggers CAUTION input double InpDD_CautionMax = 5.0; // 5% drawdown triggers RESTRICTED input double InpDD_RestrictedMax = 7.0; // 7% drawdown triggers LOCKDOWN input int InpLockdownSec = 300; // seconds to stay in LOCKDOWN input int InpRestrictedSec = 180; // seconds before downgrade from RESTRICTED input double InpRecoveryThr = 0.99; // equity must recover to 99% of peak to downgrade // ---- Restrictions in CAUTION mode input double InpCautionLotMult = 0.5; // lot multiplier when CAUTION input int InpCautionMaxPos = 1; // max positions allowed in CAUTION input int InpCautionMinSec = 60; // min seconds between trades in CAUTION // ---- Hard limits (absolute blocks) input double InpMaxDDLimit = 10.0; // hard DD limit (blocks all trades) input double InpMaxDailyLoss = 5.0; // hard daily loss limit (blocks all trades) // ---- Test engine & stress mode input double InpTestLot = 0.1; // base lot size (normal mode) input bool InpEnableTest = true; input bool InpStressMode = false; // larger lot + no trend filter // ---- Dashboard & logging input bool InpEnableLog = true; input bool InpEnableDash = false; // ---- Exit parameters (pips) input double InpSLPips = 40.0; input double InpTPPips = 80.0; input double InpTrailPips = 30.0; input int InpTimerSeconds = 5;
The master switch is the most powerful input. When InpEnableGoverning = false, the EA will set all drawdown thresholds and hard limits to 100%, effectively disabling the governor while keeping all other components running. Stress mode (InpStressMode = true) increases the test engine lot size to 0.5 and removes the trend filter, causing more trades and deeper drawdowns — a one‑click way to see the governor’s protective response.
Global Objects
Below the inputs, all nine library objects are instantiated as global variables. Because they are declared outside any function, they persist for the EA’s entire lifetime and are accessible from OnInit, OnTick, and OnTimer.
// --- Global Variables
CEquityMonitor g_Mon;
CDrawdownAnalyzer g_Anal;
CRecoveryCooldown g_Cooldown;
CGovernanceStateEngine g_State;
CRestrictionEnforcer g_Enforcer;
CTradeAuthorizer g_Auth;
CEventLogger g_Log;
CVisualizer g_Vis;
CTestTradingEngine g_Test; These objects are initialized in OnInit() and linked together via their Init() methods. The EA itself holds no governance logic — it only passes data between objects.
The UpdateGovernor() Helper
A small helper function centralizes the governor update logic. It reads the current drawdown, asks the analyzer for pressure, checks for rapid collapse, and feeds all of that into the state engine. If the state changes and logging is enabled, a message is printed to the Experts tab.
//+------------------------------------------------------------------+ //| Updates governor state on every tick when enabled | //+------------------------------------------------------------------+ void UpdateGovernor() { double dd = g_Mon.GetDrawdownPercent(); ENUM_DD_PRESSURE pressure = g_Anal.GetPressure(dd); bool rapid = g_Anal.IsRapidCollapse(g_Mon); ENUM_GOV_STATE old = g_State.Get(); g_State.Update(pressure, rapid); if(old != g_State.Get() && InpEnableLog) g_Log.LogStateChange(old, g_State.Get(), dd); }
This function is called from OnTick() (if governing is enabled) and from OnTimer() (if the dashboard is active). Keeping it separate avoids code duplication and makes the update sequence easy to follow.
Initialization — OnInit()
OnInit() is the wiring center. It first records the starting equity and then checks the master switch.
If governing is disabled, all drawdown and hard limits are raised to 100%. As a result, the governor never leaves the NORMAL state, although all objects remain active.
If governing is enabled, the configured input values are used. The objects are then initialized in a logical sequence. Thresholds are configured first, followed by the cooldown manager, state engine, restriction enforcer, and trade authorizer.
Finally, the test engine and dashboard are activated if requested.
//+------------------------------------------------------------------+ //| initialization | //+------------------------------------------------------------------+ int OnInit() { g_Mon.Init(); // --- Set thresholds to 100% if governing is disabled (never triggers) double normalMax = InpEnableGoverning ? InpDD_NormalMax : 100.0; double cautionMax = InpEnableGoverning ? InpDD_CautionMax : 100.0; double restrictedMax= InpEnableGoverning ? InpDD_RestrictedMax: 100.0; double maxDD = InpEnableGoverning ? InpMaxDDLimit : 100.0; double maxDaily = InpEnableGoverning ? InpMaxDailyLoss : 100.0; g_Anal.SetThresholds(normalMax, cautionMax, restrictedMax); g_Cooldown.Init(&g_Mon); g_Cooldown.SetParams(InpLockdownSec, InpRestrictedSec, InpRecoveryThr); g_State.Init(&g_Cooldown); g_Enforcer.SetParams(InpCautionLotMult, InpCautionMaxPos, InpCautionMinSec); g_Auth.Init(&g_State, &g_Enforcer, &g_Mon); g_Auth.SetLimits(maxDD, maxDaily); if(InpEnableLog) g_Log.SetToExperts(true); string mode = InpEnableGoverning ? "GOVERNING ACTIVE" : "GOVERNING DISABLED"; if(InpStressMode) mode += " (STRESS MODE)"; g_Log.Log("Equity Governor v1.0 – " + mode); if(InpEnableDash) { g_Vis.Init(&g_State, &g_Mon, &g_Cooldown, &g_Enforcer); g_Vis.SetPos(CORNER_LEFT_UPPER, 10, 30); } if(InpEnableTest) { double lot = InpStressMode ? 0.5 : InpTestLot; bool useTrend = !InpStressMode; g_Test.Init(&g_Auth, &g_Enforcer, lot, useTrend); g_Test.SetRiskParams(InpSLPips, InpTPPips, InpTrailPips); g_Log.Log("Test engine: lot=" + DoubleToString(lot,2) + ", trend filter=" + (useTrend ? "ON" : "OFF")); } EventSetTimer(InpTimerSeconds); return INIT_SUCCEEDED; }
Notice how the master switch is implemented — it raises the thresholds to impossible values. This keeps all code paths intact, making it easy to toggle governing on/off without restarting the EA.
Cleanup — OnDeinit()
When the EA is removed, OnDeinit() kills the timer, hides the dashboard (deleting all GOV_* objects), and clears the chart comment.
//+------------------------------------------------------------------+ //| deinitialization | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); g_Log.Log("Stopped"); g_Vis.Hide(); Comment(""); }
Because all objects are global/static, no manual deletion is needed — the visualizer handles its chart objects, and the test engine releases its indicator handles in its destructor.
OnTimer() — Dashboard Refresh
The timer runs every InpTimerSeconds. If the dashboard is active, it updates the equity monitor, runs the governor update, and refreshes the visual panel. This keeps the chart display accurate even when there are no new ticks.
//+------------------------------------------------------------------+ //| OnTimer | //+------------------------------------------------------------------+ void OnTimer() { if(InpEnableDash) { g_Mon.Update(); UpdateGovernor(); g_Vis.Refresh(); } }
OnTick() — Main Event Loop
This is the heart of the EA. On every tick, the equity monitor is updated. If governing is active, the governor state is also updated. Then the test engine is called, which asks the authorizer for permission before placing any trade. If the dashboard is disabled, a simple Comment() shows the current state and DD% on the chart.
//+------------------------------------------------------------------+ //| OnTick | //+------------------------------------------------------------------+ void OnTick() { g_Mon.Update(); if(InpEnableGoverning) UpdateGovernor(); if(InpEnableTest) g_Test.OnTick(); if(!InpEnableDash) { Comment(StringFormat("Gov: %s | DD: %.2f%% | Eq: %.2f", CGovernanceStateEngine::ToString(g_State.Get()), g_Mon.GetDrawdownPercent(), g_Mon.GetEquity())); } else Comment(""); }
Stress Mode and the Final Execution Flow
Stress mode turns the test engine into an aggressive trader: lot size increases from 0.1 to 0.5, and the 200‑SMA trend filter is removed. This causes more frequent entries, often against the dominant trend, which rapidly creates drawdown. Within minutes, the governor moves from CAUTION to RESTRICTED and typically into LOCKDOWN. New trades remain blocked until the cooldown expires and equity recovers. This makes stress mode the quickest way to demonstrate the system’s protective behavior.
The complete execution flow can be summarized as:
- OnInit() → initialize equity → set thresholds (master switch logic) → wire all objects → optionally activate dashboard and test engine → start timer.
- OnTick() → update equity → optionally update governor state → test engine requests authorization → if granted, place trade and record trade time.
- OnTimer() → if dashboard active, update equity, state, and panel.
- OnDeinit() → kill timer, hide dashboard, clear comment.
Every new trade passes through CTradeAuthorizer::Authorize(), which checks the current state, the restriction rules (lot multiplier, max positions, min time), and the hard limits.
Backtesting Results
To validate the role of the governance layer, the system was backtested in two modes: first with governance disabled, and then with governance enabled. The objective was not to optimize profitability. Instead, the goal was to observe how the control framework reacted under identical trading conditions and how it influenced drawdown behavior, exposure management, and execution stability.

Without Governance
input bool InpEnableGoverning = false;
In the first test, the governance layer was disabled, meaning the strategy traded without any drawdown supervision, cooldown logic, restriction enforcement, or trade authorization control. The test engine was therefore free to continue opening positions regardless of equity pressure.

The results show that the strategy generated a total net loss of -1,282.12 with a profit factor of 0.80 and an equity drawdown of approximately 19.20%. A total of 321 trades were executed, indicating aggressive market participation throughout the testing period.

Several important observations can be made from this run:
- The system continued trading during unfavorable market conditions without reducing exposure.
- Consecutive losing trades accumulated rapidly because no restriction state was activated.
- The absence of cooldown logic allowed trading activity to continue during periods of equity instability.
- Drawdown escalated to nearly one-fifth of the account balance before any natural recovery occurred.
This behavior is exactly what the governance framework is designed to address. Without supervision, the strategy reacts only to market signals and has no awareness of account health or equity stress.
With Governance
input bool InpEnableGoverning = true;
The second test enabled the full governance layer, including drawdown pressure analysis, state transitions, cooldown timers, restriction enforcement, and trade authorization checks.

The difference in behavior becomes immediately visible in the results. Total net loss was reduced dramatically to -36.74, while equity drawdown dropped from 19.20% to approximately 4.09%. The number of executed trades also decreased significantly from 321 trades to only 47 trades.

These improvements occurred because the governor actively intervened during unstable periods:
- CAUTION mode reduced trading frequency and exposure.
- RESTRICTED and LOCKDOWN states blocked new entries during deeper equity pressure.
- Cooldown timers prevented immediate re-entry after losses.
- Hard drawdown limits stopped the system from compounding losses uncontrollably.
Although profitability was still slightly negative, the objective of the governance framework was successfully demonstrated: preserving account stability during adverse conditions rather than maximizing raw trade frequency.
An important observation is that the average loss per trade was also reduced, and the system avoided the large equity swings seen in the unrestricted test. The governor effectively transformed an unstable high-exposure system into a controlled and significantly safer execution environment.
Comparison
The contrast between the two runs clearly demonstrates the practical value of governance-based execution control.
| Metric | Without Governance | With Governance |
|---|---|---|
| Total Net Profit | -1,282.12 | -36.74 |
| Equity Drawdown | 19.20% | 4.09% |
| Total Trades | 321 | 47 |
| Profit Factor | 0.80 | 0.94 |
| Largest Loss Trade | -45.17 | -43.45 |
| Consecutive Losses | 8 | 5 |
| Margin Level | 2897.59% | 4529.38% |
The most important improvement was not profitability, but survivability. The governance layer significantly reduced exposure escalation, limited trading during unstable conditions, and prevented prolonged drawdown expansion. In practical algorithmic trading environments, this type of protection is often more important than increasing trade frequency or chasing short-term returns.
The tests also confirm that the governor operates independently of the trading strategy itself. The underlying SMA crossover logic remained unchanged in both runs; only the governance layer was enabled or disabled. This separation validates the modular design discussed earlier in the article – the governor acts as an external supervisory framework rather than becoming part of the signal-generation logic itself.
Conclusion
In this article, we present an Equity Governance Framework that elevates risk management from a passive monitoring function to an active supervisory layer within an automated trading system. Rather than relying solely on strategy logic to control exposure, the framework continuously evaluates account health, classifies drawdown pressure, and applies graduated execution controls through a structured state-driven architecture.
The implementation translates this governance model into a modular MQL5 framework. Specialized components handle equity monitoring, drawdown analysis, recovery management, state transitions, restriction enforcement, trade authorization, visualization, and event logging. By separating governance responsibilities from signal generation, the framework remains strategy-independent, allowing the same control layer to be integrated with a wide range of trading systems without modifying their underlying entry and exit logic.
The practical outcome is a reusable account-level supervision mechanism. It detects adverse equity conditions, restricts trading activity during periods of elevated risk, and enforces recovery procedures before normal operation resumes. Its behavior can be observed and verified through governance state transitions, authorization decisions, dashboard metrics, event logs, and backtesting results. The comparative tests demonstrated that the framework successfully reduced drawdown expansion, limited excessive trading activity during unstable periods, and improved overall execution stability while leaving the trading strategy itself unchanged.
More importantly, this work illustrates a broader engineering principle in algorithmic trading: profitability alone is not sufficient for long-term system robustness. Sustainable automated systems require governance mechanisms. They must supervise behavior under stress, enforce discipline, and protect capital in unfavorable markets. By embedding these controls directly into software, trading discipline becomes a measurable and repeatable process rather than a discretionary decision.
The framework presented here serves as a foundation for more advanced governance research and development. Future extensions may incorporate portfolio-level supervision, adaptive risk allocation, cross-strategy coordination, and intelligent recovery mechanisms, further strengthening the resilience and operational safety of automated trading systems.
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Feature Engineering for ML (Part 5): Microstructural Features in Python
Neural Networks in Trading: Hierarchical Skill Discovery for Adaptive Agent Behavior (HiSSD)
MQL5 Trading Tools (Part 35): Adding Channel, Pitchfork, Gann, and Fibonacci Tools to the Canvas Drawing Layer
Beyond GARCH (Part V): Fitting the Multifractal Spectrum in MQL5
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use