Warrior_EA/Signals/SignalMarketDepth.mqh
AnimateDread e62c710d6f fix: correct array orientation and PReLU gradient backprop in hidden layers
- Ensure `tick_volume` array is set as series in ADShorteningOfThrust.mq5 to prevent future-data leak in volume calculations.
- Ensure `open` array is set as series in ADWyckoffFailedStructure.mq5 to prevent future-data leak in structure detection.
- Add missing PReLU gradient scaling (multiply by 0.01 for negative outputs) in CPU_CalcHiddenGradient and DirectML shader to match expected derivative behavior across all backends.
2026-07-17 23:21:12 -04:00

200 lines
11 KiB
MQL5

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