//+------------------------------------------------------------------+ //| WarriorGapFade.mq5 | //| *** RESEARCH ARTIFACT - DO NOT TRADE. LOSES ON REAL TICKS. *** | //| EURCHF PF 0.52 / AUDNZD PF 0.53 at the open, ~0.9 one hour later | //| (2016-26, tester Model 4). See research/FX_RESULTS.md. | //| Weekend-gap fade, forex. ONE CHART = ONE SYMBOL. | //| | //| When the first price after the weekend opens more than | //| InpK x ATR(H1) away from Friday's last close, trade back toward | //| Friday's close: | //| target Friday's last close (take-profit order) | //| stop InpStopATR x ATR from entry | //| time flat before the end of the opening day, so no swap | //| One decision per weekend per symbol, trade or no trade. | //| | //| Research: research/fx_round2.py X3 (registered in FX_PLAN.md | //| round 2 before it was run). The whole question is whether the | //| edge survives the REAL spread at the Sunday open, which is why | //| this EA exists: bar data cannot answer it, the tick tester can. | //| | //| Account coordination is the same as WarriorDipZ: the kill | //| switch watches ACCOUNT equity, keyed by InpMagic, persisted in | //| terminal globals. | //+------------------------------------------------------------------+ #property copyright "Warrior" #property version "1.20" #property strict #include input double InpK = 1.0; // Min gap, x ATR(H1) input double InpStopATR = 3.0; // Stop, x ATR(H1) input int InpATRPeriod = 14; // Wilder ATR period (H1) input double InpRiskPct = 0.25; // Risk per trade, % of equity input int InpEntryDelayMin = 0; // Wait after the open before deciding (spread settles) input int InpMinGapHours = 36; // A bar gap this long = a weekend input int InpExitHour = 23; // Time exit, server hour on the opening day input int InpExitMinute = 50; // ... and minute (before the rollover) input double InpMaxDDPct = 4.5; // Kill switch: account equity DD from peak, % input long InpMagic = 20260923; // Magic number input bool InpJournal = true; // Write trade journal on deinit CTrade g_trade; datetime g_doneBar = 0; // H1 open time of the weekend-open bar already decided double g_target = 0.0; // Friday close - managed in-EA, see Decide() double g_stop = 0.0; // intended stop - enforced in-EA until the broker accepts it double g_peak = 0.0; bool g_halted = false; string GvPeak(void) { return "Warrior_peak_" + IntegerToString(InpMagic); } string GvHalt(void) { return "Warrior_halt_" + IntegerToString(InpMagic); } //--- Wilder ATR on H1 over closed bars, seeded far enough back to converge //--- ending at bar `from` - the last bar before the weekend, as the backtest //--- measured it; with an entry delay, bar 1 would be a Monday bar instead double WilderATR(const int from) { int n = InpATRPeriod * 20; MqlRates r[]; if(CopyRates(_Symbol, PERIOD_H1, from, n, r) != n) return 0.0; double atr = 0.0; for(int i = 0; i < n; i++) { double tr = (i == 0) ? r[i].high - r[i].low : MathMax(r[i].high - r[i].low, MathMax(MathAbs(r[i].high - r[i - 1].close), MathAbs(r[i].low - r[i - 1].close))); if(i < InpATRPeriod) { atr += tr / InpATRPeriod; continue; } atr = (atr * (InpATRPeriod - 1) + tr) / InpATRPeriod; } return atr; } bool FindPos(ulong &ticket) { for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong tk = PositionGetTicket(i); if(tk != 0 && PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagic) { ticket = tk; return true; } } return false; } double LotsFor(const double dist) { double tv = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double ts = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); double st = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); double mn = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double mx = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); if(tv <= 0 || ts <= 0 || st <= 0 || dist <= 0) return 0.0; double lots = AccountInfoDouble(ACCOUNT_EQUITY) * InpRiskPct / 100.0 / (dist * tv / ts); lots = MathFloor(lots / st) * st; return (lots < mn) ? 0.0 : MathMin(lots, mx); } void Guards(void) { double eq = AccountInfoDouble(ACCOUNT_EQUITY); if(eq > g_peak) { g_peak = eq; GlobalVariableSet(GvPeak(), g_peak); } if(!g_halted && GlobalVariableCheck(GvHalt()) && GlobalVariableGet(GvHalt()) > 0.0) g_halted = true; if(!g_halted && InpMaxDDPct > 0 && g_peak > 0 && (g_peak - eq) / g_peak * 100.0 >= InpMaxDDPct) { g_halted = true; GlobalVariableSet(GvHalt(), 1.0); PrintFormat("GapFade: KILL SWITCH - equity %.2f is %.2f%% below peak %.2f", eq, (g_peak - eq) / g_peak * 100.0, g_peak); } } void Manage(void) { ulong tk; if(!FindPos(tk)) return; //--- time exit: before the first rollover after entry, so the trade never //--- pays swap - the backtest's "end of the opening day" datetime opened = (datetime)PositionGetInteger(POSITION_TIME); MqlDateTime o, n; TimeToStruct(opened, o); TimeToStruct(TimeCurrent(), n); bool late = (n.day_of_year != o.day_of_year) || (n.hour > InpExitHour) || (n.hour == InpExitHour && n.min >= InpExitMinute); bool isLong = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY); MqlTick tick; bool hit = false, stopped = false; if(SymbolInfoTick(_Symbol, tick) && g_target > 0.0) { //--- target on the price we would actually close at: bid for a long, //--- ask for a short (the backtest used the bid for both - optimistic) hit = isLong ? (tick.bid >= g_target) : (tick.ask <= g_target); stopped = (g_stop > 0.0) && (isLong ? (tick.bid <= g_stop) : (tick.ask >= g_stop)); } //--- attach the broker-side stop as soon as the broker accepts it; until //--- then the in-EA check above is the stop if(PositionGetDouble(POSITION_SL) == 0.0 && g_stop > 0.0) g_trade.PositionModify(tk, g_stop, 0.0); if(late || g_halted || hit || stopped) g_trade.PositionClose(tk); } void Decide(void) { //--- Find the first bar after the weekend among the last few bars. It is //--- NOT necessarily bar 0: with an entry delay the decision happens one or //--- more bars later, and testing only bar 0 against bar 1 would never see //--- the weekend gap again - the delayed variant would silently never trade. datetime t0 = 0; int wk = -1; for(int k = 0; k <= 4; k++) { datetime a = iTime(_Symbol, PERIOD_H1, k); datetime b = iTime(_Symbol, PERIOD_H1, k + 1); if(a == 0 || b == 0) return; if(a - b >= InpMinGapHours * 3600) { t0 = a; wk = k; break; } } if(wk < 0 || t0 == g_doneBar) return; // no weekend open in reach, or already decided if(TimeCurrent() < t0 + InpEntryDelayMin * 60) return; //--- decide within the hour after the delay, never later if(TimeCurrent() >= t0 + (InpEntryDelayMin + 60) * 60) { g_doneBar = t0; return; } ulong tk; if(g_halted || FindPos(tk)) { g_doneBar = t0; return; } double friClose = iClose(_Symbol, PERIOD_H1, wk + 1); // last bar before the weekend double atr = WilderATR(wk + 1); MqlTick tick; if(friClose <= 0 || atr <= 0 || !SymbolInfoTick(_Symbol, tick) || tick.bid <= 0) return; // data not ready - try next tick double gap = tick.bid - friClose; g_doneBar = t0; // one decision per weekend if(MathAbs(gap) < InpK * atr) return; int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); bool longSide = (gap < 0); // gapped down -> buy back up double entry = longSide ? tick.ask : tick.bid; double sl = NormalizeDouble(longSide ? entry - InpStopATR * atr : entry + InpStopATR * atr, digits); double tp = NormalizeDouble(friClose, digits); //--- a gap already closed by the spread leaves nothing to trade if((longSide && tp <= entry) || (!longSide && tp >= entry)) return; double lots = LotsFor(MathAbs(entry - sl)); if(lots <= 0) return; g_trade.SetTypeFillingBySymbol(_Symbol); //--- NO stops on the order itself. At the Sunday open the broker's minimum //--- stop distance is widest, and a take-profit this close to the price //--- was REJECTED (rc 10016 "invalid stops") - which silently dropped real //--- trades from the first real-tick test. The target is managed in-EA and //--- the stop is attached right after the fill (Manage()). g_target = tp; g_stop = sl; bool ok = longSide ? g_trade.Buy(lots, _Symbol, 0, 0, 0, "gapfade") : g_trade.Sell(lots, _Symbol, 0, 0, 0, "gapfade"); if(!ok || (g_trade.ResultRetcode() != TRADE_RETCODE_DONE && g_trade.ResultRetcode() != TRADE_RETCODE_PLACED)) { PrintFormat("GapFade: %s %s failed rc=%u %s", _Symbol, longSide ? "buy" : "sell", g_trade.ResultRetcode(), g_trade.ResultRetcodeDescription()); if(g_trade.ResultRetcode() == TRADE_RETCODE_MARKET_CLOSED) g_doneBar = 0; // session not really open yet - retry } } void OnTick(void) { Guards(); Manage(); Decide(); } void OnTimer(void) { Guards(); Manage(); } int OnInit(void) { g_trade.SetExpertMagicNumber(InpMagic); g_trade.SetDeviationInPoints(100); g_peak = AccountInfoDouble(ACCOUNT_EQUITY); if(GlobalVariableCheck(GvPeak())) g_peak = MathMax(g_peak, GlobalVariableGet(GvPeak())); GlobalVariableSet(GvPeak(), g_peak); g_halted = GlobalVariableCheck(GvHalt()) && GlobalVariableGet(GvHalt()) > 0.0; EventSetTimer(60); PrintFormat("GapFade: %s k=%.2f stop=%.1fxATR risk=%.2f%% delay=%dmin", _Symbol, InpK, InpStopATR, InpRiskPct, InpEntryDelayMin); return INIT_SUCCEEDED; } void WriteJournal(void) { if(!HistorySelect(0, TimeCurrent() + 86400)) return; int h = FileOpen("gapfade_trades_" + _Symbol + ".csv", FILE_WRITE | FILE_CSV | FILE_ANSI | FILE_COMMON, ','); if(h == INVALID_HANDLE) return; FileWrite(h, "position", "symbol", "entry_time", "entry_price", "volume", "exit_time", "exit_price", "net_profit", "exit_reason", "gross_profit", "swap", "commission", "side"); int deals = HistoryDealsTotal(); for(int i = 0; i < deals; i++) { ulong d = HistoryDealGetTicket(i); if(HistoryDealGetInteger(d, DEAL_MAGIC) != InpMagic || HistoryDealGetInteger(d, DEAL_ENTRY) != DEAL_ENTRY_OUT || HistoryDealGetString(d, DEAL_SYMBOL) != _Symbol) continue; long pos = HistoryDealGetInteger(d, DEAL_POSITION_ID); datetime et = 0; double ep = 0, vol = 0, gross = 0, swp = 0, com = 0; long side = 0; for(int j = 0; j < deals; j++) { ulong e = HistoryDealGetTicket(j); if(HistoryDealGetInteger(e, DEAL_POSITION_ID) != pos) continue; gross += HistoryDealGetDouble(e, DEAL_PROFIT); swp += HistoryDealGetDouble(e, DEAL_SWAP); com += HistoryDealGetDouble(e, DEAL_COMMISSION); if(HistoryDealGetInteger(e, DEAL_ENTRY) == DEAL_ENTRY_IN) { et = (datetime)HistoryDealGetInteger(e, DEAL_TIME); ep = HistoryDealGetDouble(e, DEAL_PRICE); vol = HistoryDealGetDouble(e, DEAL_VOLUME); side = (HistoryDealGetInteger(e, DEAL_TYPE) == DEAL_TYPE_BUY) ? 1 : -1; } } long rsn = HistoryDealGetInteger(d, DEAL_REASON); string why = (rsn == DEAL_REASON_SL ? "stop" : (rsn == DEAL_REASON_TP ? "target" : "expert")); FileWrite(h, (string)pos, _Symbol, TimeToString(et, TIME_DATE | TIME_MINUTES), DoubleToString(ep, 5), DoubleToString(vol, 2), TimeToString((datetime)HistoryDealGetInteger(d, DEAL_TIME), TIME_DATE | TIME_MINUTES), DoubleToString(HistoryDealGetDouble(d, DEAL_PRICE), 5), DoubleToString(gross + swp + com, 2), why, DoubleToString(gross, 2), DoubleToString(swp, 2), DoubleToString(com, 2), (string)side); } FileClose(h); } void OnDeinit(const int reason) { EventKillTimer(); if(InpJournal) WriteJournal(); }