605faf5310
Add cluster audit pipeline, united EA updates, brochure generators, and publication hygiene (gitignore, MT5 path desensitization, pre-upload scan). Remove tracked reports, models, and binary artifacts from the repo. Co-authored-by: Cursor <cursoragent@cursor.com>
353 lines
9.7 KiB
Plaintext
353 lines
9.7 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| GapGuard.mqh |
|
|
//| Gap-loss prevention: flat before session/weekend, gap-through-SL |
|
|
//+------------------------------------------------------------------+
|
|
#ifndef GAP_GUARD_MQH
|
|
#define GAP_GUARD_MQH
|
|
|
|
#include <Trade\Trade.mqh>
|
|
|
|
struct GapGuardConfig
|
|
{
|
|
bool enable;
|
|
bool closeBeforeSessionEnd;
|
|
int minutesBeforeClose;
|
|
bool closeBeforeWeekend;
|
|
int fridayCloseHour;
|
|
bool closeOnBarGapThroughSL;
|
|
double minGapPoints;
|
|
int equityDailyFlatHour;
|
|
};
|
|
|
|
GapGuardConfig g_gapCfg;
|
|
ulong g_gapMagics[];
|
|
string g_gapSymbols[];
|
|
datetime g_gapLastBarTime[];
|
|
|
|
//+------------------------------------------------------------------+
|
|
void GapGuard_Reset()
|
|
{
|
|
ArrayResize(g_gapMagics, 0);
|
|
ArrayResize(g_gapSymbols, 0);
|
|
ArrayResize(g_gapLastBarTime, 0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
void GapGuard_Init(const GapGuardConfig &cfg)
|
|
{
|
|
g_gapCfg = cfg;
|
|
GapGuard_Reset();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
void GapGuard_RegisterMagic(const ulong magic)
|
|
{
|
|
const int n = ArraySize(g_gapMagics);
|
|
for(int i = 0; i < n; i++)
|
|
{
|
|
if(g_gapMagics[i] == magic)
|
|
return;
|
|
}
|
|
ArrayResize(g_gapMagics, n + 1);
|
|
g_gapMagics[n] = magic;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
void GapGuard_RegisterSymbol(const string symbol)
|
|
{
|
|
const int n = ArraySize(g_gapSymbols);
|
|
for(int i = 0; i < n; i++)
|
|
{
|
|
if(g_gapSymbols[i] == symbol)
|
|
return;
|
|
}
|
|
ArrayResize(g_gapSymbols, n + 1);
|
|
g_gapSymbols[n] = symbol;
|
|
ArrayResize(g_gapLastBarTime, n + 1);
|
|
g_gapLastBarTime[n] = 0;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
int GapGuard_SymbolIndex(const string symbol)
|
|
{
|
|
for(int i = 0; i < ArraySize(g_gapSymbols); i++)
|
|
{
|
|
if(g_gapSymbols[i] == symbol)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
bool GapGuard_IsEAMagic(const ulong magic)
|
|
{
|
|
for(int i = 0; i < ArraySize(g_gapMagics); i++)
|
|
{
|
|
if(g_gapMagics[i] == magic)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
bool GapGuard_IsKnownEquityOrIndex(const string symbol)
|
|
{
|
|
if(symbol == "AAPL" || symbol == "APPL" || symbol == "NVDA" || symbol == "TSLA"
|
|
|| symbol == "ADBE" || symbol == "MU" || symbol == "GER40" || symbol == "US500"
|
|
|| symbol == "NAS100" || symbol == "SPX500" || symbol == "UK100")
|
|
return true;
|
|
|
|
if(StringFind(symbol, "AAPL") == 0 || StringFind(symbol, "NVDA") == 0
|
|
|| StringFind(symbol, "TSLA") == 0 || StringFind(symbol, "ADBE") == 0
|
|
|| StringFind(symbol, ".NAS") > 0 || StringFind(symbol, ".NYS") > 0)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
bool GapGuard_SymbolHasSessionGaps(const string symbol)
|
|
{
|
|
MqlDateTime now;
|
|
TimeToStruct(TimeCurrent(), now);
|
|
|
|
datetime from = 0, to = 0;
|
|
uint idx = 0;
|
|
int totalSec = 0;
|
|
|
|
while(SymbolInfoSessionTrade(symbol, (ENUM_DAY_OF_WEEK)now.day_of_week, idx, from, to))
|
|
{
|
|
MqlDateTime f, t;
|
|
TimeToStruct(from, f);
|
|
TimeToStruct(to, t);
|
|
const int fs = f.hour * 3600 + f.min * 60 + f.sec;
|
|
const int ts = t.hour * 3600 + t.min * 60 + t.sec;
|
|
if(ts > fs)
|
|
totalSec += (ts - fs);
|
|
idx++;
|
|
}
|
|
|
|
if(idx == 0)
|
|
return GapGuard_IsKnownEquityOrIndex(symbol);
|
|
|
|
return totalSec < 23 * 3600;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
bool GapGuard_IsNearSessionClose(const string symbol)
|
|
{
|
|
if(!g_gapCfg.closeBeforeSessionEnd || g_gapCfg.minutesBeforeClose <= 0)
|
|
return false;
|
|
if(!GapGuard_SymbolHasSessionGaps(symbol))
|
|
return false;
|
|
|
|
MqlDateTime now;
|
|
TimeToStruct(TimeCurrent(), now);
|
|
const int nowSec = now.hour * 3600 + now.min * 60 + now.sec;
|
|
|
|
datetime from = 0, to = 0;
|
|
uint idx = 0;
|
|
int lastEndSec = -1;
|
|
|
|
while(SymbolInfoSessionTrade(symbol, (ENUM_DAY_OF_WEEK)now.day_of_week, idx, from, to))
|
|
{
|
|
MqlDateTime tEnd;
|
|
TimeToStruct(to, tEnd);
|
|
const int endSec = tEnd.hour * 3600 + tEnd.min * 60 + tEnd.sec;
|
|
if(endSec > lastEndSec)
|
|
lastEndSec = endSec;
|
|
idx++;
|
|
}
|
|
|
|
if(lastEndSec < 0)
|
|
{
|
|
if(GapGuard_IsKnownEquityOrIndex(symbol) && g_gapCfg.equityDailyFlatHour >= 0)
|
|
{
|
|
const int flatSec = g_gapCfg.equityDailyFlatHour * 3600;
|
|
const int threshold = flatSec - g_gapCfg.minutesBeforeClose * 60;
|
|
return (nowSec >= threshold && nowSec < flatSec);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const int threshold = lastEndSec - g_gapCfg.minutesBeforeClose * 60;
|
|
return (nowSec >= threshold && nowSec < lastEndSec);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
bool GapGuard_IsWeekendRiskWindow()
|
|
{
|
|
if(!g_gapCfg.closeBeforeWeekend)
|
|
return false;
|
|
|
|
MqlDateTime now;
|
|
TimeToStruct(TimeCurrent(), now);
|
|
|
|
if(now.day_of_week == 5 && now.hour >= g_gapCfg.fridayCloseHour)
|
|
return true;
|
|
if(now.day_of_week == 6 || now.day_of_week == 0)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
bool United_IsGapRiskWindow(const string symbol)
|
|
{
|
|
if(!g_gapCfg.enable)
|
|
return false;
|
|
|
|
if(GapGuard_IsWeekendRiskWindow() && GapGuard_SymbolHasSessionGaps(symbol))
|
|
return true;
|
|
|
|
if(GapGuard_IsNearSessionClose(symbol))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
bool GapGuard_CloseEAPosition(CTrade &trade, const ulong ticket, const string reason)
|
|
{
|
|
if(ticket == 0 || !PositionSelectByTicket(ticket))
|
|
return false;
|
|
|
|
const string sym = PositionGetString(POSITION_SYMBOL);
|
|
const ulong magic = (ulong)PositionGetInteger(POSITION_MAGIC);
|
|
|
|
if(!GapGuard_IsEAMagic(magic))
|
|
return false;
|
|
|
|
if(trade.PositionClose(ticket))
|
|
{
|
|
Print("GapGuard: closed ", sym, " ticket=", ticket, " reason=", reason);
|
|
return true;
|
|
}
|
|
|
|
Print("GapGuard: failed to close ", sym, " ticket=", ticket,
|
|
" retcode=", trade.ResultRetcode(), " reason=", reason);
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
void GapGuard_TrySessionFlat(CTrade &trade, const string symbol, const int symIdx)
|
|
{
|
|
if(symIdx < 0)
|
|
return;
|
|
|
|
if(!United_IsGapRiskWindow(symbol))
|
|
return;
|
|
|
|
const string reason = GapGuard_IsWeekendRiskWindow() ? "weekend_flat" : "session_end_flat";
|
|
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
const ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0 || !PositionSelectByTicket(ticket))
|
|
continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != symbol)
|
|
continue;
|
|
if(!GapGuard_IsEAMagic((ulong)PositionGetInteger(POSITION_MAGIC)))
|
|
continue;
|
|
|
|
GapGuard_CloseEAPosition(trade, ticket, reason);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
bool GapGuard_PositionGappedThroughSL(const string symbol, const ulong ticket)
|
|
{
|
|
if(!g_gapCfg.closeOnBarGapThroughSL)
|
|
return false;
|
|
if(ticket == 0 || !PositionSelectByTicket(ticket))
|
|
return false;
|
|
if(PositionGetString(POSITION_SYMBOL) != symbol)
|
|
return false;
|
|
|
|
const double sl = PositionGetDouble(POSITION_SL);
|
|
if(sl <= 0.0)
|
|
return false;
|
|
|
|
const double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
if(point <= 0.0)
|
|
return false;
|
|
|
|
const double barOpen = iOpen(symbol, PERIOD_CURRENT, 0);
|
|
const double prevClose = iClose(symbol, PERIOD_CURRENT, 1);
|
|
if(barOpen <= 0.0 || prevClose <= 0.0)
|
|
return false;
|
|
|
|
const ENUM_POSITION_TYPE ptype = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
|
|
const double minGap = g_gapCfg.minGapPoints * point;
|
|
|
|
if(ptype == POSITION_TYPE_BUY)
|
|
{
|
|
if(barOpen >= sl)
|
|
return false;
|
|
const double gap = prevClose - barOpen;
|
|
if(gap < minGap)
|
|
return false;
|
|
return (prevClose > sl || barOpen < sl);
|
|
}
|
|
|
|
if(ptype == POSITION_TYPE_SELL)
|
|
{
|
|
if(barOpen <= sl)
|
|
return false;
|
|
const double gap = barOpen - prevClose;
|
|
if(gap < minGap)
|
|
return false;
|
|
return (prevClose < sl || barOpen > sl);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
void GapGuard_TryGapThroughSL(CTrade &trade, const string symbol, const int symIdx)
|
|
{
|
|
if(!g_gapCfg.closeOnBarGapThroughSL || symIdx < 0)
|
|
return;
|
|
|
|
const datetime barTime = iTime(symbol, PERIOD_CURRENT, 0);
|
|
if(barTime == 0)
|
|
return;
|
|
if(g_gapLastBarTime[symIdx] == barTime)
|
|
return;
|
|
|
|
g_gapLastBarTime[symIdx] = barTime;
|
|
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
const ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0 || !PositionSelectByTicket(ticket))
|
|
continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != symbol)
|
|
continue;
|
|
if(!GapGuard_IsEAMagic((ulong)PositionGetInteger(POSITION_MAGIC)))
|
|
continue;
|
|
|
|
if(GapGuard_PositionGappedThroughSL(symbol, ticket))
|
|
GapGuard_CloseEAPosition(trade, ticket, "gap_through_sl");
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
void United_ProcessGapGuard(CTrade &trade)
|
|
{
|
|
if(!g_gapCfg.enable)
|
|
return;
|
|
|
|
for(int s = 0; s < ArraySize(g_gapSymbols); s++)
|
|
{
|
|
const string symbol = g_gapSymbols[s];
|
|
if(!SymbolSelect(symbol, true))
|
|
continue;
|
|
|
|
GapGuard_TryGapThroughSL(trade, symbol, s);
|
|
GapGuard_TrySessionFlat(trade, symbol, s);
|
|
}
|
|
}
|
|
|
|
#endif // GAP_GUARD_MQH
|