727 行
28 KiB
MQL5
727 行
28 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| WarriorDipZ.mq5 |
| |||
//| Volatility-gated dip-buy on equity indices. ONE CHART = ONE |
| |||
//| SYMBOL: attach an instance to each chart you want traded |
| |||
//| (SP500, NAS100, US30, DAX40 are the validated set). |
| |||
//| |
| |||
//| Instances COORDINATE through the account, not through a basket: |
| |||
//| - same InpMagic on every chart, so the open-risk cap sums the |
| |||
//| positions of ALL instances and a check-and-open is serialised|
| |||
//| by a terminal-global lock; |
| |||
//| - the kill switch watches ACCOUNT equity and its peak/halt |
| |||
//| state lives in terminal globals, so one trip halts every |
| |||
//| instance, and survives restarts; |
| |||
//| - each instance only ever opens or closes ITS OWN symbol. |
| |||
//| Do not attach two instances to the same symbol. |
| |||
//| |
| |||
//| THE RULE (research/STRATEGY.md, measured 2021-26 H4 and 2008-26 |
| |||
//| D1): |
| |||
//| entry z = (close - SMA20) / stdev20 <= -1.5 on a closed bar, |
| |||
//| filled at the next bar's open, LONG ONLY |
| |||
//| gate Garman-Klass sigma(30) of the signal bar, as a causal |
| |||
//| expanding-window percentile, >= 0.50 |
| |||
//| exit first closed bar with close >= SMA20, else after 10 |
| |||
//| bars |
| |||
//| stop entry - 3 x Wilder ATR(14) of the signal bar |
| |||
//| size InpRiskPct of equity at the stop distance |
| |||
//| |
| |||
//| This file mirrors research/backtest.py rule-for-rule so the two |
| |||
//| can be reconciled trade by trade. The places that are easy to |
| |||
//| get subtly wrong are commented where they happen: |
| |||
//| - ATR is WILDER, computed here. MT5's iATR is a plain SMA of |
| |||
//| the true range and would give a different stop on every |
| |||
//| trade. |
| |||
//| - All reads go through CopyRates into our own cache, never the|
| |||
//| stdlib series wrappers (which return 0.0 past shift 1023). |
| |||
//| - No re-entry on a bar during which a position was held - |
| |||
//| the backtest resumes scanning at exit_bar + 1. |
| |||
//+------------------------------------------------------------------+
| |||
#property copyright "Warrior"
| |||
#property version "2.10"
| |||
#property strict
| |||
| |||
#include <Trade\Trade.mqh>
| |||
| |||
input ENUM_TIMEFRAMES InpTF = PERIOD_H4; // Signal timeframe
| |||
input int InpZPeriod = 20; // z-score / exit MA period
| |||
input double InpZEntry = -1.5; // Entry z-score (<=)
| |||
input int InpMaxBars = 10; // Time exit (bars held)
| |||
input double InpStopATR = 3.0; // Stop, x ATR
| |||
input int InpATRPeriod = 14; // Wilder ATR period
| |||
input double InpRiskPct = 0.25; // Risk per trade, % of equity
| |||
input bool InpVolGate = true; // Volatility-regime gate (H4: on, D1: OFF)
| |||
input double InpVolPct = 0.50; // Gate: min vol percentile
| |||
input int InpVolWindow = 30; // Gate: GK sigma window
| |||
input int InpVolWarm = 250; // Gate: bars before the gate can pass
| |||
input double InpMaxOpenRiskPct = 0.75; // Portfolio cap: total open risk to stops, % (0=off)
| |||
input double InpMaxDDPct = 4.5; // Kill switch: equity DD from peak, % (0=off)
| |||
input int InpFridayFlatMin = 170; // Friday flat: minutes BEFORE this symbol's Friday session close (-1=off)
| |||
input long InpMagic = 20260922; // Magic number
| |||
input bool InpJournal = true; // Write trade journal on deinit
| |||
input bool InpEquityLog = false; // Tester: per-bar P&L log for portfolio DD
| |||
| |||
#define NA -1.0 // sentinel for "not yet computable" (sigma, ATR and pct are >= 0)
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Per-symbol state and bar cache (chronological: index 0 = oldest). |
| |||
//+------------------------------------------------------------------+
| |||
class CSym
| |||
{
| |||
public:
| |||
string name;
| |||
datetime bar0; // forming bar whose open has been fully handled
| |||
datetime heldBar; // newest bar during which a position was held/closed
| |||
int n;
| |||
datetime t[];
| |||
double o[], h[], l[], c[], tr[], atr[], gk[], sig[];
| |||
| |||
CSym(void) : bar0(0), heldBar(0), n(0) { }
| |||
| |||
void Grow(const int k)
| |||
{
| |||
if(ArraySize(t) > k)
| |||
return;
| |||
int cap = MathMax(1024, k * 2);
| |||
ArrayResize(t, cap);
| |||
ArrayResize(o, cap);
| |||
ArrayResize(h, cap);
| |||
ArrayResize(l, cap);
| |||
ArrayResize(c, cap);
| |||
ArrayResize(tr, cap);
| |||
ArrayResize(atr, cap);
| |||
ArrayResize(gk, cap);
| |||
ArrayResize(sig, cap);
| |||
}
| |||
};
| |||
| |||
CSym *g_s = NULL; // this chart's symbol
| |||
double g_barMinFloat = 0.0; // worst floating P&L seen inside the forming bar
| |||
datetime g_logBar = 0;
| |||
int g_eqLog = INVALID_HANDLE;
| |||
CTrade g_trade;
| |||
double g_peakEquity = 0.0;
| |||
bool g_halted = false;
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Append one CLOSED bar and update every derived series. |
| |||
//+------------------------------------------------------------------+
| |||
void Append(CSym *s, const MqlRates &b)
| |||
{
| |||
int k = s.n;
| |||
s.Grow(k + 1);
| |||
s.t[k] = b.time;
| |||
s.o[k] = b.open;
| |||
s.h[k] = b.high;
| |||
s.l[k] = b.low;
| |||
s.c[k] = b.close;
| |||
| |||
//--- Garman-Klass variance. Non-negative for any valid bar; a bad bar is
| |||
//--- stored as 0 so it cannot dominate the rolling mean.
| |||
double g = 0.0;
| |||
if(b.open > 0 && b.high > 0 && b.low > 0 && b.close > 0 && b.high >= b.low)
| |||
{
| |||
double hl = MathLog(b.high / b.low);
| |||
double co = MathLog(b.close / b.open);
| |||
g = 0.5 * hl * hl - (2.0 * MathLog(2.0) - 1.0) * co * co;
| |||
}
| |||
s.gk[k] = g;
| |||
| |||
//--- Wilder ATR, seeded with the mean of the first P true ranges - exactly
| |||
//--- as backtest.atr() does. NOT iATR (an SMA of TR).
| |||
if(k == 0)
| |||
s.tr[k] = b.high - b.low;
| |||
else
| |||
s.tr[k] = MathMax(b.high - b.low,
| |||
MathMax(MathAbs(b.high - s.c[k - 1]), MathAbs(b.low - s.c[k - 1])));
| |||
int P = InpATRPeriod;
| |||
if(k < P - 1)
| |||
s.atr[k] = NA;
| |||
else
| |||
if(k == P - 1)
| |||
{
| |||
double sum = 0.0;
| |||
for(int j = 0; j < P; j++)
| |||
sum += s.tr[j];
| |||
s.atr[k] = sum / P;
| |||
}
| |||
else
| |||
s.atr[k] = (s.atr[k - 1] * (P - 1) + s.tr[k]) / P;
| |||
| |||
//--- rolling GK sigma
| |||
int W = InpVolWindow;
| |||
if(k < W - 1)
| |||
s.sig[k] = NA;
| |||
else
| |||
{
| |||
double sum = 0.0;
| |||
for(int j = k - W + 1; j <= k; j++)
| |||
sum += s.gk[j];
| |||
s.sig[k] = MathSqrt(MathMax(sum / W, 0.0));
| |||
}
| |||
s.n++;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Bring the cache up to the newest CLOSED bar (shift 1). |
| |||
//+------------------------------------------------------------------+
| |||
bool Sync(CSym *s)
| |||
{
| |||
datetime newestClosed = iTime(s.name, InpTF, 1);
| |||
if(newestClosed == 0)
| |||
return false;
| |||
if(s.n > 0 && s.t[s.n - 1] >= newestClosed)
| |||
return true;
| |||
| |||
MqlRates r[];
| |||
int got;
| |||
if(s.n == 0)
| |||
{
| |||
int total = Bars(s.name, InpTF);
| |||
if(total < InpZPeriod + 2)
| |||
return false;
| |||
got = CopyRates(s.name, InpTF, 1, total - 1, r);
| |||
}
| |||
else
| |||
{
| |||
int shift = iBarShift(s.name, InpTF, s.t[s.n - 1], true);
| |||
if(shift < 0)
| |||
return false; // our last bar vanished from history - wait
| |||
if(shift <= 1)
| |||
return true;
| |||
got = CopyRates(s.name, InpTF, 1, shift - 1, r);
| |||
}
| |||
if(got <= 0)
| |||
return false;
| |||
//--- CopyRates into a non-series array is oldest-first, the cache's order
| |||
for(int i = 0; i < got; i++)
| |||
if(s.n == 0 || r[i].time > s.t[s.n - 1])
| |||
Append(s, r[i]);
| |||
return (s.n > 0 && s.t[s.n - 1] == newestClosed);
| |||
}
| |||
| |||
//--- mean and POPULATION stdev of the last InpZPeriod closes ending at i
| |||
bool MeanStd(CSym *s, const int i, double &m, double &sd)
| |||
{
| |||
int N = InpZPeriod;
| |||
if(i < N - 1)
| |||
return false;
| |||
double sum = 0.0, sum2 = 0.0;
| |||
for(int j = i - N + 1; j <= i; j++)
| |||
{
| |||
sum += s.c[j];
| |||
sum2 += s.c[j] * s.c[j];
| |||
}
| |||
m = sum / N;
| |||
sd = MathSqrt(MathMax(sum2 / N - m * m, 0.0));
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Causal expanding-window percentile of sigma[i] against every |
| |||
//| EARLIER valid sigma. NA until InpVolWarm bars and > 50 samples. |
| |||
//+------------------------------------------------------------------+
| |||
double VolPct(CSym *s, const int i)
| |||
{
| |||
if(i < InpVolWarm || s.sig[i] < 0.0)
| |||
return NA;
| |||
int cnt = 0, less = 0;
| |||
for(int k = InpVolWindow - 1; k < i; k++)
| |||
{
| |||
if(s.sig[k] < 0.0)
| |||
continue;
| |||
cnt++;
| |||
if(s.sig[k] < s.sig[i])
| |||
less++;
| |||
}
| |||
if(cnt <= 50)
| |||
return NA;
| |||
return (double)less / cnt;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Our open position on this symbol, if any. |
| |||
//+------------------------------------------------------------------+
| |||
bool FindPos(const string sym, ulong &ticket)
| |||
{
| |||
for(int i = PositionsTotal() - 1; i >= 0; i--)
| |||
{
| |||
ulong tk = PositionGetTicket(i);
| |||
if(tk == 0)
| |||
continue;
| |||
if(PositionGetString(POSITION_SYMBOL) == sym &&
| |||
PositionGetInteger(POSITION_MAGIC) == InpMagic)
| |||
{
| |||
ticket = tk;
| |||
return true;
| |||
}
| |||
}
| |||
return false;
| |||
}
| |||
| |||
//--- a failure worth retrying later in the same bar (session not open yet,
| |||
//--- requote, busy server) as opposed to one that will never succeed
| |||
bool Retryable(const uint rc)
| |||
{
| |||
return (rc == TRADE_RETCODE_MARKET_CLOSED || rc == TRADE_RETCODE_REQUOTE ||
| |||
rc == TRADE_RETCODE_PRICE_OFF || rc == TRADE_RETCODE_PRICE_CHANGED ||
| |||
rc == TRADE_RETCODE_TIMEOUT || rc == TRADE_RETCODE_CONNECTION ||
| |||
rc == TRADE_RETCODE_TOO_MANY_REQUESTS || rc == TRADE_RETCODE_TRADE_DISABLED);
| |||
}
| |||
| |||
bool ClosePos(const ulong ticket, const string why)
| |||
{
| |||
if(g_trade.PositionClose(ticket))
| |||
{
| |||
uint rc = g_trade.ResultRetcode();
| |||
if(rc == TRADE_RETCODE_DONE || rc == TRADE_RETCODE_PLACED)
| |||
return true;
| |||
}
| |||
PrintFormat("DipZ: close %I64u (%s) failed rc=%u %s", ticket, why,
| |||
g_trade.ResultRetcode(), g_trade.ResultRetcodeDescription());
| |||
return false;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Lots for InpRiskPct of equity across `dist` price units. |
| |||
//| 0 when even the minimum lot would exceed the risk budget: taking |
| |||
//| the trade anyway would silently break the sizing the drawdown |
| |||
//| figures depend on. |
| |||
//+------------------------------------------------------------------+
| |||
double LotsFor(const string sym, const double dist)
| |||
{
| |||
double tv = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE);
| |||
double ts = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE);
| |||
double step = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
| |||
double vmin = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
| |||
double vmax = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
| |||
if(tv <= 0 || ts <= 0 || step <= 0 || dist <= 0)
| |||
return 0.0;
| |||
double money = AccountInfoDouble(ACCOUNT_EQUITY) * InpRiskPct / 100.0;
| |||
double lots = money / (dist * tv / ts);
| |||
lots = MathFloor(lots / step) * step;
| |||
if(lots < vmin)
| |||
return 0.0;
| |||
return MathMin(lots, vmax);
| |||
}
| |||
| |||
//--- 1 = opened, 0 = skipped for good, -1 = retry later this bar
| |||
int OpenLong(CSym *s, const int i)
| |||
{
| |||
MqlTick tk;
| |||
if(!SymbolInfoTick(s.name, tk) || tk.ask <= 0 || tk.bid <= 0)
| |||
return -1;
| |||
//--- stop anchored to the BID at the fill (the backtest's o[fill] is a bid
| |||
//--- open) minus 3 ATR of the SIGNAL bar; a long's SL triggers on the bid.
| |||
double stop = tk.bid - InpStopATR * s.atr[i];
| |||
double tsz = SymbolInfoDouble(s.name, SYMBOL_TRADE_TICK_SIZE);
| |||
if(tsz > 0)
| |||
stop = MathFloor(stop / tsz) * tsz;
| |||
int digits = (int)SymbolInfoInteger(s.name, SYMBOL_DIGITS);
| |||
stop = NormalizeDouble(stop, digits);
| |||
double lots = LotsFor(s.name, tk.ask - stop);
| |||
if(lots <= 0.0)
| |||
{
| |||
PrintFormat("DipZ: %s signal skipped - min lot exceeds %.2f%% risk", s.name, InpRiskPct);
| |||
return 0;
| |||
}
| |||
g_trade.SetTypeFillingBySymbol(s.name);
| |||
if(g_trade.Buy(lots, s.name, 0.0, stop, 0.0, "dipz"))
| |||
{
| |||
uint rc = g_trade.ResultRetcode();
| |||
if(rc == TRADE_RETCODE_DONE || rc == TRADE_RETCODE_PLACED)
| |||
return 1;
| |||
}
| |||
uint rc = g_trade.ResultRetcode();
| |||
//--- MARKET_CLOSED is the normal case at a CFD bar open that precedes the
| |||
//--- session (index H4 bars open at 00:00, trading resumes 01:05): it is
| |||
//--- retried every timer minute, so logging it would print thousands of
| |||
//--- lines of expected behaviour and bury the failures that matter.
| |||
if(rc != TRADE_RETCODE_MARKET_CLOSED)
| |||
PrintFormat("DipZ: %s buy %.2f failed rc=%u %s", s.name, lots, rc,
| |||
g_trade.ResultRetcodeDescription());
| |||
return Retryable(rc) ? -1 : 0;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Total money at risk to the stops across ALL our positions, as % |
| |||
//| of equity. The basket is equity indices: they fall together, so |
| |||
//| per-trade risk says nothing about the drawdown of a selloff that |
| |||
//| stops every position at once. A 7-index basket at 0.25% each |
| |||
//| crossed 4.5% equity DD within five months of 2022 without this. |
| |||
//+------------------------------------------------------------------+
| |||
double OpenRiskPct(void)
| |||
{
| |||
double eq = AccountInfoDouble(ACCOUNT_EQUITY);
| |||
if(eq <= 0.0)
| |||
return 100.0;
| |||
double money = 0.0;
| |||
for(int i = PositionsTotal() - 1; i >= 0; i--)
| |||
{
| |||
ulong tk = PositionGetTicket(i);
| |||
if(tk == 0 || PositionGetInteger(POSITION_MAGIC) != InpMagic)
| |||
continue;
| |||
string sym = PositionGetString(POSITION_SYMBOL);
| |||
double op = PositionGetDouble(POSITION_PRICE_OPEN);
| |||
double sl = PositionGetDouble(POSITION_SL);
| |||
double vol = PositionGetDouble(POSITION_VOLUME);
| |||
double tv = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE);
| |||
double ts = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE);
| |||
//--- a position with no stop has unbounded risk: count it as the whole
| |||
//--- cap so nothing else is added on top of it
| |||
if(sl <= 0.0 || ts <= 0.0)
| |||
return 100.0;
| |||
if(op > sl)
| |||
money += (op - sl) * vol * tv / ts;
| |||
}
| |||
return money / eq * 100.0;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| The LAST trade-session close of a weekday, in seconds from that |
| |||
//| day's midnight (86400 = trades to midnight), -1 = no session. |
| |||
//| Same helper as the original Warrior_EA (WarriorMarketCloseSeconds |
| |||
//| in ExpertSignalCustom.mqh): the broker's own session table, so it |
| |||
//| is right on every symbol, every broker and both sides of DST with |
| |||
//| no hour to maintain - which a fixed server hour cannot be once |
| |||
//| each chart is a different symbol (DAX40 and SP500 close apart). |
| |||
//+------------------------------------------------------------------+
| |||
int 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;
| |||
}
| |||
| |||
//--- Friday flat: from (Friday close - InpFridayFlatMin) until the week reopens
| |||
bool FridayBlock(void)
| |||
{
| |||
if(InpFridayFlatMin < 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;
| |||
int close = MarketCloseSeconds(_Symbol, 5);
| |||
if(close <= 0)
| |||
return true; // no Friday session: nothing may be held into it
| |||
int now = d.hour * 3600 + d.min * 60 + d.sec;
| |||
return now >= close - InpFridayFlatMin * 60;
| |||
}
| |||
| |||
//--- flatten THIS instance's symbol only; every other chart flattens its own
| |||
void CloseMine(const string why)
| |||
{
| |||
ulong tk;
| |||
if(FindPos(_Symbol, tk))
| |||
ClosePos(tk, why);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Terminal-global lock around "check the open-risk cap, then open". |
| |||
//| Without it, several charts rolling to the same H4 bar can each |
| |||
//| read the cap before any of them has opened, and all open. Stale |
| |||
//| locks (a crashed holder) expire after 30 s. |
| |||
//+------------------------------------------------------------------+
| |||
string GvLock(void) { return "DipZ_lock_" + IntegerToString(InpMagic); }
| |||
| |||
bool LockTake(void)
| |||
{
| |||
if(MQLInfoInteger(MQL_TESTER))
| |||
return true; // one instance per tester run
| |||
double now = (double)TimeCurrent();
| |||
if(!GlobalVariableCheck(GvLock()))
| |||
GlobalVariableSet(GvLock(), 0.0);
| |||
double v = GlobalVariableGet(GvLock());
| |||
if(v != 0.0 && now - v < 30.0)
| |||
return false;
| |||
return GlobalVariableSetOnCondition(GvLock(), now, v);
| |||
}
| |||
| |||
void LockDrop(void)
| |||
{
| |||
if(!MQLInfoInteger(MQL_TESTER))
| |||
GlobalVariableSet(GvLock(), 0.0);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| One symbol, one check. Cheap unless its bar has just rolled. |
| |||
//+------------------------------------------------------------------+
| |||
void Process(CSym *s)
| |||
{
| |||
datetime t0 = iTime(s.name, InpTF, 0);
| |||
if(t0 == 0 || t0 == s.bar0)
| |||
return; // nothing new on THIS symbol's clock
| |||
if(!Sync(s))
| |||
return;
| |||
int i = s.n - 1; // the bar that just closed
| |||
bool done = true;
| |||
| |||
//--- EXITS first: a close decided on bar i happens before any entry on it
| |||
ulong ticket;
| |||
if(FindPos(s.name, ticket))
| |||
{
| |||
double m, sd;
| |||
bool target = MeanStd(s, i, m, sd) && s.c[i] >= m;
| |||
//--- bars held = shift of the bar containing the fill; the fill bar
| |||
//--- counts, so the time exit lands on the close of bar fill+9
| |||
datetime opened = (datetime)PositionGetInteger(POSITION_TIME);
| |||
int held = iBarShift(s.name, InpTF, opened, false);
| |||
bool timeout = (held >= InpMaxBars);
| |||
if(target || timeout)
| |||
{
| |||
if(ClosePos(ticket, target ? "target" : "time"))
| |||
s.heldBar = s.t[i]; // exit is "at the close of bar i"
| |||
else
| |||
done = false;
| |||
}
| |||
}
| |||
| |||
//--- ENTRIES
| |||
if(done && !g_halted && !FridayBlock() && !FindPos(s.name, ticket) &&
| |||
s.t[i] > s.heldBar && s.atr[i] > 0.0)
| |||
{
| |||
double m, sd;
| |||
if(MeanStd(s, i, m, sd) && sd > 0.0 && (s.c[i] - m) / sd <= InpZEntry)
| |||
{
| |||
bool gateOk = true;
| |||
if(InpVolGate)
| |||
gateOk = (VolPct(s, i) >= InpVolPct);
| |||
//--- account-wide cap: a signal that would take total open risk (all
| |||
//--- instances) past the cap is DROPPED, not queued - by the time room
| |||
//--- frees up, the dip it measured is gone. When several charts roll on
| |||
//--- the same clock, whichever takes the lock first wins.
| |||
if(gateOk)
| |||
{
| |||
if(!LockTake())
| |||
done = false; // another chart is mid-open: retry next tick
| |||
else
| |||
{
| |||
if(InpMaxOpenRiskPct <= 0.0 ||
| |||
OpenRiskPct() + InpRiskPct <= InpMaxOpenRiskPct + 1e-9)
| |||
{
| |||
int rc = OpenLong(s, i);
| |||
if(rc < 0)
| |||
done = false; // session not open yet - retry this bar
| |||
}
| |||
LockDrop();
| |||
}
| |||
}
| |||
}
| |||
}
| |||
if(done)
| |||
s.bar0 = t0;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| The kill switch's memory lives in terminal global variables, so a |
| |||
//| restart (crash, VPS reboot, re-attach) cannot reset the drawdown |
| |||
//| clock to "no drawdown" - the failure that would let an account |
| |||
//| that is already 4% down open fresh risk as if it were at its peak.|
| |||
//| Delete DipZ_peak_<magic> / DipZ_halt_<magic> by hand to re-arm. |
| |||
//+------------------------------------------------------------------+
| |||
string GvPeak(void) { return "DipZ_peak_" + IntegerToString(InpMagic); }
| |||
string GvHalt(void) { return "DipZ_halt_" + IntegerToString(InpMagic); }
| |||
| |||
void Guards(void)
| |||
{
| |||
double eq = AccountInfoDouble(ACCOUNT_EQUITY);
| |||
if(eq > g_peakEquity)
| |||
{
| |||
g_peakEquity = eq;
| |||
GlobalVariableSet(GvPeak(), g_peakEquity);
| |||
}
| |||
if(!g_halted && InpMaxDDPct > 0.0 && g_peakEquity > 0.0 &&
| |||
(g_peakEquity - eq) / g_peakEquity * 100.0 >= InpMaxDDPct)
| |||
{
| |||
g_halted = true;
| |||
GlobalVariableSet(GvHalt(), 1.0);
| |||
PrintFormat("DipZ: KILL SWITCH - equity %.2f is %.2f%% below peak %.2f; flattening and halting",
| |||
eq, (g_peakEquity - eq) / g_peakEquity * 100.0, g_peakEquity);
| |||
}
| |||
//--- keep trying until actually flat: a missed tick must delay the close,
| |||
//--- never cancel it
| |||
//--- another instance may have tripped the switch: honour it here too
| |||
if(!g_halted && GlobalVariableCheck(GvHalt()) && GlobalVariableGet(GvHalt()) > 0.0)
| |||
g_halted = true;
| |||
if(g_halted || FridayBlock())
| |||
CloseMine(g_halted ? "kill switch" : "friday flat");
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Tester-only per-bar log: this symbol's realised P&L so far, its |
| |||
//| floating P&L at the bar close and the WORST floating P&L inside |
| |||
//| the bar. Summing these across per-symbol runs rebuilds a |
| |||
//| portfolio equity curve that single-symbol tester runs cannot |
| |||
//| report; the worst-in-bar column bounds it from the pessimistic |
| |||
//| side (worsts need not coincide across symbols). |
| |||
//+------------------------------------------------------------------+
| |||
double MyFloating(void)
| |||
{
| |||
ulong tk;
| |||
if(!FindPos(_Symbol, tk))
| |||
return 0.0;
| |||
return PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
| |||
}
| |||
| |||
void EquityLog(void)
| |||
{
| |||
if(g_eqLog == INVALID_HANDLE)
| |||
return;
| |||
double fl = MyFloating();
| |||
datetime b = iTime(_Symbol, InpTF, 0);
| |||
if(g_logBar != 0 && b != g_logBar)
| |||
{
| |||
double realised = AccountInfoDouble(ACCOUNT_BALANCE) - TesterStatistics(STAT_INITIAL_DEPOSIT);
| |||
FileWrite(g_eqLog, TimeToString(g_logBar, TIME_DATE | TIME_MINUTES),
| |||
DoubleToString(realised, 2), DoubleToString(fl, 2),
| |||
DoubleToString(MathMin(g_barMinFloat, fl), 2));
| |||
g_barMinFloat = fl;
| |||
}
| |||
if(b != g_logBar)
| |||
g_logBar = b;
| |||
if(fl < g_barMinFloat)
| |||
g_barMinFloat = fl;
| |||
}
| |||
| |||
void RunAll(void)
| |||
{
| |||
Guards();
| |||
Process(g_s);
| |||
EquityLog();
| |||
}
| |||
| |||
int OnInit(void)
| |||
{
| |||
g_s = new CSym();
| |||
g_s.name = _Symbol;
| |||
if(InpEquityLog && MQLInfoInteger(MQL_TESTER))
| |||
{
| |||
g_eqLog = FileOpen("dipz_eq_" + _Symbol + ".csv",
| |||
FILE_WRITE | FILE_CSV | FILE_ANSI | FILE_COMMON, ',');
| |||
if(g_eqLog != INVALID_HANDLE)
| |||
FileWrite(g_eqLog, "bar", "realised", "float_close", "float_min");
| |||
}
| |||
g_trade.SetExpertMagicNumber(InpMagic);
| |||
g_trade.SetDeviationInPoints(50);
| |||
g_peakEquity = AccountInfoDouble(ACCOUNT_EQUITY);
| |||
if(GlobalVariableCheck(GvPeak()))
| |||
g_peakEquity = MathMax(g_peakEquity, GlobalVariableGet(GvPeak()));
| |||
GlobalVariableSet(GvPeak(), g_peakEquity);
| |||
g_halted = GlobalVariableCheck(GvHalt()) && GlobalVariableGet(GvHalt()) > 0.0;
| |||
if(g_halted)
| |||
Print("DipZ: HALTED by an earlier kill-switch trip - delete global variable ",
| |||
GvHalt(), " to re-arm");
| |||
EventSetTimer(60);
| |||
int fc = MarketCloseSeconds(_Symbol, 5);
| |||
if(InpFridayFlatMin >= 0 && fc > 0)
| |||
{
| |||
int ff = fc - InpFridayFlatMin * 60;
| |||
PrintFormat("DipZ: %s Friday session closes %02d:%02d server; flat from %02d:%02d (%d min before)",
| |||
_Symbol, fc / 3600, (fc % 3600) / 60, ff / 3600, (ff % 3600) / 60, InpFridayFlatMin);
| |||
}
| |||
PrintFormat("DipZ: %s on %s, risk %.2f%%, account open-risk cap %.2f%%, gate %s, kill %.1f%%",
| |||
_Symbol, EnumToString(InpTF), InpRiskPct, InpMaxOpenRiskPct,
| |||
(InpVolGate ? "ON" : "off"), InpMaxDDPct);
| |||
return INIT_SUCCEEDED;
| |||
}
| |||
| |||
void OnTick(void) { RunAll(); }
| |||
void OnTimer(void) { RunAll(); }
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| A stop-out marks its bar as held, so no entry is taken on the |
| |||
//| bar the stop fired in - the backtest resumes at exit_bar + 1. |
| |||
//+------------------------------------------------------------------+
| |||
void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &req,
| |||
const MqlTradeResult &res)
| |||
{
| |||
if(trans.type != TRADE_TRANSACTION_DEAL_ADD || trans.deal == 0)
| |||
return;
| |||
if(!HistoryDealSelect(trans.deal))
| |||
return;
| |||
if(HistoryDealGetInteger(trans.deal, DEAL_MAGIC) != InpMagic)
| |||
return;
| |||
if(HistoryDealGetInteger(trans.deal, DEAL_ENTRY) != DEAL_ENTRY_OUT)
| |||
return;
| |||
long reason = HistoryDealGetInteger(trans.deal, DEAL_REASON);
| |||
if(reason != DEAL_REASON_SL && reason != DEAL_REASON_SO)
| |||
return;
| |||
string sym = HistoryDealGetString(trans.deal, DEAL_SYMBOL);
| |||
datetime dt = (datetime)HistoryDealGetInteger(trans.deal, DEAL_TIME);
| |||
if(g_s != NULL && sym == g_s.name)
| |||
{
| |||
datetime bt = iTime(sym, InpTF, iBarShift(sym, InpTF, dt, false));
| |||
if(bt > g_s.heldBar)
| |||
g_s.heldBar = bt;
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Journal: one row per closed position, for the reconciliation. |
| |||
//+------------------------------------------------------------------+
| |||
void WriteJournal(void)
| |||
{
| |||
if(!HistorySelect(0, TimeCurrent() + 86400))
| |||
return;
| |||
int h = FileOpen("dipz_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");
| |||
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);
| |||
//--- find the matching entry deal
| |||
datetime et = 0;
| |||
double ep = 0, vol = 0, net = 0, gross = 0, swp = 0, com = 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);
| |||
net = gross + swp + com;
| |||
if(HistoryDealGetInteger(e, DEAL_ENTRY) == DEAL_ENTRY_IN)
| |||
{
| |||
et = (datetime)HistoryDealGetInteger(e, DEAL_TIME);
| |||
ep = HistoryDealGetDouble(e, DEAL_PRICE);
| |||
vol = HistoryDealGetDouble(e, DEAL_VOLUME);
| |||
}
| |||
}
| |||
long rsn = HistoryDealGetInteger(d, DEAL_REASON);
| |||
string why = (rsn == DEAL_REASON_SL ? "stop" : (rsn == DEAL_REASON_SO ? "stopout" : "expert"));
| |||
FileWrite(h, (string)pos, HistoryDealGetString(d, DEAL_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(net, 2), why, DoubleToString(gross, 2),
| |||
DoubleToString(swp, 2), DoubleToString(com, 2));
| |||
}
| |||
FileClose(h);
| |||
}
| |||
| |||
void OnDeinit(const int reason)
| |||
{
| |||
EventKillTimer();
| |||
if(InpJournal)
| |||
WriteJournal();
| |||
if(g_eqLog != INVALID_HANDLE)
| |||
FileClose(g_eqLog);
| |||
if(g_s != NULL)
| |||
delete g_s;
| |||
g_s = NULL;
| |||
}
|