Warrior_EA/Expert/WarriorExpert.mqh

236 lines
11 KiB
MQL5

//+------------------------------------------------------------------+
//| 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 <Expert\Expert.mqh>
#include "..\System\TradeChecks.mqh"
#include "..\System\AccountGuard.mqh"
#include "..\Mind\WarriorMind.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
CWarriorMind *m_mind; // not owned; NULL = no trader behind the signal
int m_declined; // entries the Mind stood aside from
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_mind(NULL), m_declined(0), m_retry(false), m_retries(0),
m_flattened(0) {}
~CWarriorExpert(void) {}
void Guard(CAccountGuard *guard) { m_guard = guard; }
void Mind(CWarriorMind *mind) { m_mind = mind; }
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_mind != NULL)
m_mind.OnTick(); // files a position that has just closed
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;
//--- THE MIND FIRST: it reads the signal bar and sets this entry's risk scale. A veto is a
//--- decision, not a transient failure, so it is not retried.
if(m_mind != NULL)
{
string vetoWhy = "";
if(!m_mind.Assess(isLong, price, sl, vetoWhy))
{
m_declined++;
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();
if(ok && m_mind != NULL)
m_mind.Opened(isLong, sl);
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_mind != NULL)
m_mind.Report();
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