Warrior_EA/mql5/WarriorDipZ.mq5
AnimateDread faa5ae698f feat(dipz): WarriorDipZ - one chart per symbol, account-level guards
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>
2026-09-23 13:24:57 -04:00

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;
}