forked from animatedread/Warrior_EA
- 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.
200 lines
11 KiB
MQL5
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));
|
|
}
|
|
//+------------------------------------------------------------------+
|