forked from animatedread/Warrior_EA
Warrior's defaults are now the validated book: DIP_ZSCORE alone, long only, H4, risk 0.25%, one chart per index with a shared Magic. - System/BarCache.mqh: whole-history closed bars, Wilder ATR, GK sigma and the expanding vol percentile (no 1024-bar stdlib ceiling) - System/AccountGuard.mqh: open-risk cap, kill switch, cross-chart lock and Friday flat, shared through terminal globals by Magic - CWarriorExpert: guard on every tick; a transient open failure retries the bar - CWarriorSignal::SetupStop: the dip owns its 3 x Wilder ATR stop from the bid - SignalDipBuy: no entry vote while holding (a still-dipping time exit never closed, and Processing re-entered on the exit bar); no entry on a stop bar - WarriorMoney sizes on equity; WARRIOR_RISK allows fractional risk - TradeLog + research/compare_ea.py: trade-for-trade check vs WarriorDipZ - SP500/US30/DAX40 identical to the cent, NAS100 96.9% (stale-quote timer fills) - research/nn_cross_index.py: pre-registered cross-index NN meta-label - FAIL (AUC 0.564, CI [0.498, 0.630]); DipMetaCut stays off Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
239 lines
12 KiB
MQL5
239 lines
12 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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 <prefix>_halt_<magic> 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
|