//+------------------------------------------------------------------+ //| AccountGuard.mqh | //| AnimateDread | //| | //| THE ACCOUNT, NOT THE CHART. One Warrior instance trades one | //| symbol, but the prop account is judged on ALL of them together - | //| and equity indices fall together. Four charts at 0.25% each are | //| one 1% bet on a selloff. So every instance that shares a Magic | //| number shares three things, through terminal global variables: | //| | //| OPEN-RISK CAP the money at risk to the stops of every position | //| carrying this magic, on any symbol, as % of | //| equity. An entry that would take it past the cap | //| is DROPPED, not queued - by the time room frees | //| up the dip it measured is gone. Measured: a | //| 7-index basket at 0.25% each crossed 4.5% equity | //| DD within five months of 2022 without it. | //| LOCK "check the cap, then open" is serialised, so two | //| charts rolling to the same H4 bar cannot both | //| read the cap before either has opened. | //| KILL SWITCH equity drawdown from its peak. One trip halts | //| and flattens every instance, and the state | //| survives a restart: a VPS reboot must not reset | //| the drawdown clock to "at the peak". | //| Delete _halt_ by hand to re-arm. | //| | //| And one thing per symbol: | //| FRIDAY FLAT from N minutes before THIS symbol's own Friday | //| session close (the broker's session table, so it | //| is right on every symbol and both sides of DST) | //| until the week reopens. Swap was 23% of gross | //| profit without it; with it, ret/DD 2.55 -> 4.11. | //| The original Warrior design (CH_MARKET_CLOSE). | //| | //| Same magic on every chart of one strategy - that IS the grouping. | //| Never reuse it for another EA on the account: its positions would | //| count against the cap and its losses toward the kill switch. | //+------------------------------------------------------------------+ #ifndef WARRIOR_ACCOUNTGUARD_MQH #define WARRIOR_ACCOUNTGUARD_MQH class CAccountGuard { protected: string m_symbol; long m_magic; double m_riskPct; // what one new entry adds to open risk double m_capPct; // 0 = no cap double m_killPct; // 0 = no kill switch int m_fridayMin; // -1 = no Friday flat double m_peak; bool m_halted; int m_capDropped; // entries dropped at the cap, reported at deinit int m_lockBusy; // opens deferred because another chart held the lock string Gv(const string what) const { return "Warrior_" + what + "_" + IntegerToString(m_magic); } public: CAccountGuard(void) : m_symbol(""), m_magic(0), m_riskPct(0.0), m_capPct(0.0), m_killPct(0.0), m_fridayMin(-1), m_peak(0.0), m_halted(false), m_capDropped(0), m_lockBusy(0) {} ~CAccountGuard(void) {} void Init(const string symbol, const long magic, const double riskPct, const double capPct, const double killPct, const int fridayMin); //--- Once per tick: track the peak, trip or honour the kill switch. void Update(void); bool Halted(void) const { return m_halted; } //--- Nothing of this symbol may be held right now (halted, or inside the Friday window). bool MustBeFlat(void) const { return m_halted || FridayBlock(); } //--- Would one more entry of m_riskPct stay inside the cap? bool RoomForOne(void); bool LockTake(void); void LockDrop(void); void NoteLockBusy(void) { m_lockBusy++; } bool FridayBlock(void) const; double OpenRiskPct(void) const; void Report(void) const; static int MarketCloseSeconds(const string symbol, const int dayOfWeek); }; //+------------------------------------------------------------------+ void CAccountGuard::Init(const string symbol, const long magic, const double riskPct, const double capPct, const double killPct, const int fridayMin) { m_symbol = symbol; m_magic = magic; m_riskPct = riskPct; m_capPct = capPct; m_killPct = killPct; m_fridayMin = fridayMin; m_peak = AccountInfoDouble(ACCOUNT_EQUITY); if(GlobalVariableCheck(Gv("peak"))) m_peak = MathMax(m_peak, GlobalVariableGet(Gv("peak"))); GlobalVariableSet(Gv("peak"), m_peak); m_halted = (m_killPct > 0.0 && GlobalVariableCheck(Gv("halt")) && GlobalVariableGet(Gv("halt")) > 0.0); if(m_halted) PrintFormat("CAccountGuard: HALTED by an earlier kill-switch trip - delete global variable %s" " to re-arm.", Gv("halt")); const int fc = MarketCloseSeconds(m_symbol, 5); if(m_fridayMin >= 0 && fc > 0) { const int ff = fc - m_fridayMin * 60; PrintFormat("CAccountGuard: %s Friday session closes %02d:%02d server; flat from %02d:%02d" " (%d min before).", m_symbol, fc / 3600, (fc % 3600) / 60, ff / 3600, (ff % 3600) / 60, m_fridayMin); } PrintFormat("CAccountGuard: magic %I64d - open-risk cap %s across every chart with this magic," " kill switch %s, Friday flat %s.", m_magic, (m_capPct > 0.0 ? DoubleToString(m_capPct, 2) + "%" : "off"), (m_killPct > 0.0 ? DoubleToString(m_killPct, 1) + "% equity DD from peak" : "off"), (m_fridayMin >= 0 ? "on" : "off")); } //+------------------------------------------------------------------+ void CAccountGuard::Update(void) { const double eq = AccountInfoDouble(ACCOUNT_EQUITY); if(eq > m_peak) { m_peak = eq; GlobalVariableSet(Gv("peak"), m_peak); } if(m_killPct <= 0.0) return; if(!m_halted && m_peak > 0.0 && (m_peak - eq) / m_peak * 100.0 >= m_killPct) { m_halted = true; GlobalVariableSet(Gv("halt"), 1.0); PrintFormat("CAccountGuard: KILL SWITCH - equity %.2f is %.2f%% below peak %.2f; flattening" " and halting every chart with magic %I64d.", eq, (m_peak - eq) / m_peak * 100.0, m_peak, m_magic); } //--- Another instance may have tripped it: honour that here too. if(!m_halted && GlobalVariableCheck(Gv("halt")) && GlobalVariableGet(Gv("halt")) > 0.0) { m_halted = true; PrintFormat("CAccountGuard: halted by another chart's kill switch (magic %I64d).", m_magic); } } //+------------------------------------------------------------------+ //| Money at risk to the stops across ALL positions with this magic, | //| any symbol, as % of equity. A position without a stop has | //| unbounded risk and fills the whole cap. | //+------------------------------------------------------------------+ double CAccountGuard::OpenRiskPct(void) const { const double eq = AccountInfoDouble(ACCOUNT_EQUITY); if(eq <= 0.0) return 100.0; double money = 0.0; for(int i = PositionsTotal() - 1; i >= 0; i--) { const ulong tk = PositionGetTicket(i); if(tk == 0 || PositionGetInteger(POSITION_MAGIC) != m_magic) continue; const string sym = PositionGetString(POSITION_SYMBOL); const double op = PositionGetDouble(POSITION_PRICE_OPEN); const double sl = PositionGetDouble(POSITION_SL); const double vol = PositionGetDouble(POSITION_VOLUME); const double tv = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE); const double ts = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE); if(sl <= 0.0 || ts <= 0.0) return 100.0; const bool isLong = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY); const double dist = isLong ? op - sl : sl - op; if(dist > 0.0) money += dist * vol * tv / ts; } return money / eq * 100.0; } //+------------------------------------------------------------------+ bool CAccountGuard::RoomForOne(void) { if(m_capPct <= 0.0) return true; if(OpenRiskPct() + m_riskPct <= m_capPct + 1e-9) return true; m_capDropped++; return false; } //+------------------------------------------------------------------+ //| Terminal-global lock around "check the cap, then open". A stale | //| lock (a crashed holder) expires after 30 s. The tester runs one | //| instance per agent, so there is nothing to serialise there. | //+------------------------------------------------------------------+ bool CAccountGuard::LockTake(void) { if(MQLInfoInteger(MQL_TESTER)) return true; const string gv = Gv("lock"); const double now = (double)TimeCurrent(); if(!GlobalVariableCheck(gv)) GlobalVariableSet(gv, 0.0); const double v = GlobalVariableGet(gv); if(v != 0.0 && now - v < 30.0) return false; return GlobalVariableSetOnCondition(gv, now, v); } //+------------------------------------------------------------------+ void CAccountGuard::LockDrop(void) { if(!MQLInfoInteger(MQL_TESTER)) GlobalVariableSet(Gv("lock"), 0.0); } //+------------------------------------------------------------------+ //| The LAST trade-session close of a weekday, in seconds from that | //| day's midnight (86400 = trades to midnight), -1 = no session. | //+------------------------------------------------------------------+ int CAccountGuard::MarketCloseSeconds(const string symbol, const int dayOfWeek) { datetime from = 0, to = 0; int lastTo = -1; for(uint k = 0; SymbolInfoSessionTrade(symbol, (ENUM_DAY_OF_WEEK)dayOfWeek, k, from, to); k++) lastTo = (int)to; return lastTo; } //+------------------------------------------------------------------+ //| From (Friday close - N minutes) until the week reopens. | //+------------------------------------------------------------------+ bool CAccountGuard::FridayBlock(void) const { if(m_fridayMin < 0) return false; MqlDateTime d; TimeToStruct(TimeCurrent(), d); if(d.day_of_week == 6 || d.day_of_week == 0) return true; if(d.day_of_week != 5) return false; const int close = MarketCloseSeconds(m_symbol, 5); if(close <= 0) return true; // no Friday session: nothing may be held into it const int now = d.hour * 3600 + d.min * 60 + d.sec; return now >= close - m_fridayMin * 60; } //+------------------------------------------------------------------+ void CAccountGuard::Report(void) const { PrintFormat("CAccountGuard: %d entr%s dropped at the open-risk cap, %d open(s) deferred for the" " cross-chart lock; kill switch %s.", m_capDropped, (m_capDropped == 1 ? "y" : "ies"), m_lockBusy, (m_halted ? "TRIPPED" : "not tripped")); } #endif // WARRIOR_ACCOUNTGUARD_MQH