//+------------------------------------------------------------------+ //| 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 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_ / DipZ_halt_ 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; }