Warrior_EA/mql5/WarriorGapFade.mq5

312 行
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();
}