forked from mnbvc188199/Warrior_EA
Hypotheses were registered in FX_PLAN.md before each round. Trend, breakout, cross reversion, hour seasonality, month-end USD, carry-cross dip-buy, metals dip-buy and flight-to-safety all fail the bar. The weekend-gap fade looked like the best result of the project on bar data (OOS t 20, 28/28 pairs) and loses on real ticks (EURCHF PF 0.52, AUDNZD PF 0.53): the Sunday-open spread is as wide as the gap. WarriorGapFade is kept as the research artifact that proved it and is flagged DO NOT TRADE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
312 lines
13 KiB
MQL5
312 lines
13 KiB
MQL5
//+------------------------------------------------------------------+
| |
//| 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 <Trade\Trade.mqh>
| |
| |
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();
| |
}
|