forked from animatedread/Warrior_EA
Standalone EA for the vol-gated dip-buy. Each chart trades its own symbol; instances sharing InpMagic share one open-risk cap (serialised by a terminal-global lock) and one account-equity kill switch persisted in terminal globals. Friday flat = minutes before the symbol's own Friday session close (the original CH_MARKET_CLOSE design), default 170. Tester, 4 charts 2022-2026: +14.6%, equity DD 2.88%, ret/DD 5.07, every year positive - unchanged on real ticks. run_tests.ps1 compiles, runs, verifies the launch and keeps each journal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
727 lines
28 KiB
MQL5
727 lines
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;
|
|
}
|