//+------------------------------------------------------------------+ //| WarriorExpert.mqh | //| AnimateDread | //| | //| CExpert WITH A SESSION-AWARE BAR CLOCK. One override, Refresh(). | //| | //| WHAT THE STDLIB DOES. With EveryTick = false, CExpert::Refresh() | //| lets Processing() run only on ticks TimeframesFlags() marks as a | //| new bar of the chart period, and for D1 that is the first tick of | //| each calendar day - whatever hour it arrives at. The tick is then | //| CONSUMED: m_last_tick_time moves to it, and every later tick that | //| day compares against it and reads as "same day, no new bar". | //| | //| WHY THAT FAILS ON A CFD. An index CFD quotes a tick at 00:00 (or | //| 01:00 across the DST seam) while its trading session is still | //| shut, so the one evaluation the day gets lands on a tick the | //| broker will not fill. Measured on SP500 D1 2012-2026: 1,647 of | //| 3,887 daily entries declined at the gate - "SP500 has no open | //| trading session at 2012.01.04 00:00" - and every exit attempted at | //| 01:00 failed with [Market closed] and was retried a day later. | //| NAS100 lost 189 the same way. The signal was right; the clock was | //| reading it during the maintenance hour. | //| | //| THE FIX. A new-bar tick that arrives while the session is shut is | //| NOT consumed: m_last_tick_time stays where it was, so the next | //| tick still compares as a new bar, and the first tick INSIDE a | //| session is the one that gets processed. The signals read bar 1 - | //| the closed bar - through StartIndex() either way, so what is | //| evaluated is identical; only the minute it is acted on moves, | //| from the maintenance hour to the open. Every-tick mode is left | //| exactly as the stdlib has it. | //| | //| THE ACCOUNT GUARD (System\AccountGuard.mqh), optional. When one | //| is attached, every tick first updates the kill switch and, while | //| the account or this symbol must be flat (kill switch, Friday | //| window), closes this chart's position and refuses entries. Every | //| open goes through the cross-chart lock and the open-risk cap. | //| | //| A BAR IS NOT CONSUMED BY A FAILED OPEN. With EveryTick = false | //| the stdlib processes a bar once; an entry refused for a TRANSIENT | //| reason (another chart holds the lock, requote, market closed at | //| the server) would be lost for good. Here the bar stays "new" and | //| is processed again on the next tick - the reference EA's retry. | //+------------------------------------------------------------------+ #ifndef WARRIOR_EXPERT_MQH #define WARRIOR_EXPERT_MQH #include #include "..\System\TradeChecks.mqh" #include "..\System\AccountGuard.mqh" class CWarriorExpert : public CExpert { protected: int m_deferred; // new-bar ticks held back because the session was shut int m_deferredBars; // ...and how many distinct bars that was datetime m_deferredBar; // the bar being held back, so it is counted once CAccountGuard *m_guard; // not owned; NULL = no account-level rules MqlDateTime m_prevTick; // m_last_tick_time before this bar was taken, for a retry bool m_retry; // an open failed transiently: process this bar again int m_retries; // how often that happened, reported at deinit int m_flattened; // positions closed by the guard (kill switch / Friday) bool Open(const bool isLong, double price, double sl, double tp); void FlattenMine(void); public: CWarriorExpert(void) : m_deferred(0), m_deferredBars(0), m_deferredBar(0), m_guard(NULL), m_retry(false), m_retries(0), m_flattened(0) {} ~CWarriorExpert(void) {} void Guard(CAccountGuard *guard) { m_guard = guard; } virtual void OnTick(void) override; virtual void Deinit(void) override; protected: virtual bool Refresh(void) override; virtual bool OpenLong(double price, double sl, double tp) override { return Open(true, price, sl, tp); } virtual bool OpenShort(double price, double sl, double tp) override { return Open(false, price, sl, tp); } }; //+------------------------------------------------------------------+ bool CWarriorExpert::Refresh(void) { //--- Every-tick mode: nothing to defer, the stdlib is right as it is. if(m_period_flags == WRONG_VALUE || m_period_flags == 0) return CExpert::Refresh(); if(!m_symbol.RefreshRates()) return false; MqlDateTime time; TimeToStruct(m_symbol.Time(), time); if((m_period_flags & TimeframesFlags(time)) == 0) return false; // same bar as the last one processed //--- A new bar - but is the market open to act on it? If not, leave m_last_tick_time alone so //--- this bar is still "new" on the next tick, and say nothing per tick: the count is reported //--- once at deinit, the way the vote reports its gates. string why = ""; if(!TCMarketIsOpen(m_symbol.Name(), m_symbol.Time(), why)) { m_deferred++; const datetime bar = iTime(m_symbol.Name(), m_period, 0); if(bar != m_deferredBar) { m_deferredBar = bar; m_deferredBars++; } return false; } m_prevTick = m_last_tick_time; m_last_tick_time = time; m_indicators.Refresh(); return true; } //+------------------------------------------------------------------+ //| The guard runs on EVERY tick - a kill switch or a Friday close | //| cannot wait for the next H4 bar - and the stdlib's bar-clocked | //| processing after it. | //+------------------------------------------------------------------+ void CWarriorExpert::OnTick(void) { if(m_guard != NULL) { m_guard.Update(); if(m_guard.MustBeFlat()) FlattenMine(); } if(!m_on_tick_process || !Refresh()) return; m_retry = false; Processing(); //--- Only the bar clock can be rewound; in every-tick mode the next tick re-evaluates anyway. if(m_retry && m_period_flags != WRONG_VALUE && m_period_flags != 0) { m_last_tick_time = m_prevTick; m_retries++; } } //+------------------------------------------------------------------+ //| CExpert::OpenLong/OpenShort, with the guard around the send and a | //| transient failure turned into a retry instead of a lost signal. | //| The body is the stdlib's (lot, LotCheck, send), written out so | //| the retcode read afterwards is this request's, never a stale one. | //+------------------------------------------------------------------+ bool CWarriorExpert::Open(const bool isLong, double price, double sl, double tp) { if(price == EMPTY_VALUE) return false; if(m_guard != NULL) { if(m_guard.MustBeFlat()) return false; if(!m_guard.LockTake()) { m_guard.NoteLockBusy(); m_retry = true; // another chart is mid-open: try again next tick return false; } if(!m_guard.RoomForOne()) { m_guard.LockDrop(); return false; // dropped, not queued - the dip will be gone } } double lot = isLong ? LotOpenLong(price, sl) : LotOpenShort(price, sl); lot = LotCheck(lot, price, (isLong ? ORDER_TYPE_BUY : ORDER_TYPE_SELL)); bool ok = false; if(lot > 0.0) { ok = isLong ? m_trade.Buy(lot, price, sl, tp) : m_trade.Sell(lot, price, sl, tp); const uint rc = m_trade.ResultRetcode(); if(ok && rc != TRADE_RETCODE_DONE && rc != TRADE_RETCODE_PLACED && rc != TRADE_RETCODE_DONE_PARTIAL) ok = false; string why = ""; if(!ok && (rc == TRADE_RETCODE_MARKET_CLOSED || rc == TRADE_RETCODE_TRADE_DISABLED || TCRetcodeAdvice(rc, why) > 0)) m_retry = true; } if(m_guard != NULL) m_guard.LockDrop(); return ok; } //+------------------------------------------------------------------+ //| Close THIS chart's position - every other chart closes its own. | //| Only inside a session: outside one the server refuses, and the | //| next in-session tick will do it. | //+------------------------------------------------------------------+ void CWarriorExpert::FlattenMine(void) { if(!SelectPosition()) return; string why = ""; if(!TCMarketIsOpen(m_symbol.Name(), TimeCurrent(), why)) return; if(m_trade.PositionClose(m_position.Ticket())) m_flattened++; } //+------------------------------------------------------------------+ void CWarriorExpert::Deinit(void) { if(m_deferredBars > 0) PrintFormat("CWarriorExpert: BAR CLOCK - %d bar(s) opened with the session shut and were" " processed at the first tick inside a session instead (%d closed ticks skipped).", m_deferredBars, m_deferred); if(m_retries > 0) PrintFormat("CWarriorExpert: %d bar(s) re-processed after a transient open failure.", m_retries); if(m_guard != NULL) { PrintFormat("CWarriorExpert: %d position(s) closed by the account guard (kill switch / Friday).", m_flattened); m_guard.Report(); } CExpert::Deinit(); } #endif // WARRIOR_EXPERT_MQH