//+------------------------------------------------------------------+ //| SignalMarketDepth.mqh | //| AnimateDread | //| https://www.mql5.com | //+------------------------------------------------------------------+ #include "..\Expert\ExpertSignalCustom.mqh" // wizard description start //+------------------------------------------------------------------+ //| Description of the class | //| Title=Signals of order book (Depth of Market) confirmation | //| Type=SignalAdvanced | //| Name=MarketDepth | //| ShortName=DOM | //| Class=CSignalMarketDepth | //| Page=signal_market_depth | //+------------------------------------------------------------------+ // wizard description end //+------------------------------------------------------------------+ //| Class CSignalMarketDepth. | //| Purpose: rule-based (non-ML) confirmation signal built from the | //| live order book (MarketBookGet). NOT a trained input feature - | //| MT5 only exposes the CURRENT order book (no historical Depth of | //| Market), so this can never be replayed through Train()'s bar-by- | //| bar historical loop the way the AI signals' indicator features | //| are. It participates in the SAME weighted Direction() composite | //| CExpertSignalCustom::Direction() already combines PAI/CONV/LSTM/ | //| News/Session/ITF through (see that method) - a nonzero return | //| pushes the composite bullish/bearish, EMPTY_VALUE vetoes the tick | //| entirely (thin/abnormal-spread protection, same role as the News/ | //| Session filters), and 0.0 abstains without influencing anything. | //| Availability is checked once, externally, in Warrior_EA.mq5's | //| OnInit() (CheckMarketDepthAvailability()) BEFORE this signal is | //| even constructed - if the broker/symbol doesn't provide real DOM | //| data, EnableMarketDepth is treated as false for the whole run and | //| this class is never instantiated, so Direction() being called on | //| an unavailable book is not a case this class has to handle. | //+------------------------------------------------------------------+ class CSignalMarketDepth : public CExpertSignalCustom { protected: //--- top-N book levels (per side) summed into the imbalance ratio - deeper levels are noisier/ //--- more easily spoofed, so this deliberately stays shallow (near-touch liquidity) by default int m_depthLevels; //--- scales the raw imbalance ratio (-1..+1, (bidVol-askVol)/(bidVol+askVol)) into the -100..100 //--- range Direction() must return. 1.0 = full range at a fully one-sided book; lower values mute //--- this signal's influence on the composite without disabling it outright. double m_imbalanceScale; //--- hard veto (EMPTY_VALUE, same as the News/Session filters) when the current spread exceeds //--- this multiple of its own recent rolling average - protects against entering on a //--- momentarily blown-out/illiquid book (thin session, news spike, broker feed hiccup) that a //--- one-off imbalance reading from the same broken snapshot can't be trusted to characterize. double m_maxSpreadMultiple; //--- rolling spread history for the average above - in-memory only, live-only (never touched by //--- Train(), never persisted): each new tick's spread is a live, causal reading of "right now", //--- exactly like the order book itself. double m_spreadHistory[]; int m_spreadHistoryNext; int m_spreadHistoryCount; static const int SPREAD_HISTORY_SIZE; void UpdateSpreadHistory(double spread); double AverageSpreadHistory(void) const; //--- Best-effort CSV append of every real (non-abstain) reading - timestamp, symbol, imbalance, //--- spread, bidVol, askVol, whether it was a veto this tick. Not a trained feature (nothing in //--- this codebase reads this file back) - it exists purely so that after enough live/forward //--- running, there's a genuine, EA-collected historical DOM dataset an operator could eventually //--- fold into a real retrain, rather than the impossible-to-obtain broker DOM history a trained //--- NN feature would otherwise need. A write failure here must never affect trading, so every //--- call site treats this as fire-and-forget (return value intentionally ignored). bool m_logSnapshots; void LogSnapshot(double imbalance, double spread, double bidVol, double askVol, bool vetoed) const; public: CSignalMarketDepth(void); ~CSignalMarketDepth(void); void DepthLevels(int value) { m_depthLevels = MathMax(1, value); } void ImbalanceScale(double value) { m_imbalanceScale = value; } void MaxSpreadMultiple(double value) { m_maxSpreadMultiple = value; } void LogSnapshots(bool value) { m_logSnapshots = value; } //--- order-book imbalance vote / thin-book veto - see the class-level comment above virtual double Direction(void) override; }; const int CSignalMarketDepth::SPREAD_HISTORY_SIZE = 50; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CSignalMarketDepth::CSignalMarketDepth(void) : m_depthLevels(5), m_imbalanceScale(1.0), m_maxSpreadMultiple(3.0), m_spreadHistoryNext(0), m_spreadHistoryCount(0), m_logSnapshots(true) { ArrayResize(m_spreadHistory, SPREAD_HISTORY_SIZE); ArrayInitialize(m_spreadHistory, 0.0); } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CSignalMarketDepth::~CSignalMarketDepth(void) { } //+------------------------------------------------------------------+ //| Rolling spread history - simple circular buffer, oldest overwritten| //+------------------------------------------------------------------+ void CSignalMarketDepth::UpdateSpreadHistory(double spread) { m_spreadHistory[m_spreadHistoryNext] = spread; m_spreadHistoryNext = (m_spreadHistoryNext + 1) % SPREAD_HISTORY_SIZE; if(m_spreadHistoryCount < SPREAD_HISTORY_SIZE) m_spreadHistoryCount++; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ double CSignalMarketDepth::AverageSpreadHistory(void) const { if(m_spreadHistoryCount <= 0) return 0.0; double sum = 0.0; for(int i = 0; i < m_spreadHistoryCount; i++) sum += m_spreadHistory[i]; return sum / m_spreadHistoryCount; } //+------------------------------------------------------------------+ //| Best-effort append-only CSV log - see the member declaration | //| comment. One file per symbol+period, FILE_COMMON so it survives | //| Strategy Tester/optimizer agent isolation the same way the | //| production .nnw weight files do (Expert\ExpertSignalAIBase.mqh's | //| SetIdentity()/m_folderPath). | //+------------------------------------------------------------------+ void CSignalMarketDepth::LogSnapshot(double imbalance, double spread, double bidVol, double askVol, bool vetoed) const { if(!m_logSnapshots) return; string folderPath = eaName + "\\MarketDepthLog\\"; if(!FolderCreate(folderPath, FILE_COMMON) && GetLastError() != 5010) // 5010 = already exists return; string fileName = folderPath + m_symbol.Name() + "_" + IntegerToString(m_period) + ".csv"; bool isNewFile = !FileIsExist(fileName, FILE_COMMON); int handle = FileOpen(fileName, FILE_READ | FILE_WRITE | FILE_CSV | FILE_COMMON | FILE_ANSI, ','); if(handle == INVALID_HANDLE) return; // fire-and-forget - never let a log failure affect trading FileSeek(handle, 0, SEEK_END); if(isNewFile) FileWrite(handle, "timestamp", "symbol", "imbalance", "spread", "bidVol", "askVol", "vetoed"); FileWrite(handle, (long)TimeCurrent(), m_symbol.Name(), imbalance, spread, bidVol, askVol, (vetoed ? 1 : 0)); FileClose(handle); } //+------------------------------------------------------------------+ //| Order-book imbalance vote + thin-book veto. | //+------------------------------------------------------------------+ double CSignalMarketDepth::Direction(void) { double spread = (double)(m_symbol.Ask() - m_symbol.Bid()); //--- build the average from history BEFORE this reading is added, so a single already-blown-out //--- spread can't inflate the very average it's about to be compared against double avgSpread = AverageSpreadHistory(); UpdateSpreadHistory(spread); // m_maxSpreadMultiple <= 0 means the spread veto is disabled (DOM_SPREADMULT_OFF) - checked // explicitly rather than left to fall out of the multiplication below, since spread > avgSpread*0 // (i.e. spread > 0) would otherwise be true on almost every tick and veto permanently instead of // never. if(m_maxSpreadMultiple > 0.0 && m_spreadHistoryCount >= 10 && avgSpread > 0.0 && spread > avgSpread * m_maxSpreadMultiple) { LogSnapshot(0.0, spread, 0.0, 0.0, true); return EMPTY_VALUE; // thin/abnormal book - veto this tick, same role as News/Session filters } MqlBookInfo book[]; if(!MarketBookGet(m_symbol.Name(), book) || ArraySize(book) == 0) return 0.0; // momentary empty snapshot - abstain rather than veto on a single missed read double bidVol = 0.0, askVol = 0.0; int bidLevels = 0, askLevels = 0; for(int i = 0; i < ArraySize(book); i++) { // volume_real (double) rather than volume (long, whole-lot only) - correctly supports // brokers/instruments that report fractional book depth (e.g. crypto CFDs), and avoids an // implicit long->double narrowing conversion warning for no benefit. if(book[i].type == BOOK_TYPE_BUY && bidLevels < m_depthLevels) { bidVol += book[i].volume_real; bidLevels++; } else if(book[i].type == BOOK_TYPE_SELL && askLevels < m_depthLevels) { askVol += book[i].volume_real; askLevels++; } } if(bidVol + askVol <= 0.0) return 0.0; //--- (bidVol-askVol)/(bidVol+askVol): +1 = book entirely bid-side (favors Buy), -1 = entirely //--- ask-side (favors Sell). More resting bid volume than ask volume near the touch means more //--- buyers queued than sellers at nearby prices - a real, right-now liquidity read no historical //--- OHLC bar could ever encode, complementary to (not a replacement for) the AI signals. double imbalance = (bidVol - askVol) / (bidVol + askVol); LogSnapshot(imbalance, spread, bidVol, askVol, false); return MathMax(-100.0, MathMin(100.0, imbalance * m_imbalanceScale * 100.0)); } //+------------------------------------------------------------------+