Warrior_EA/Mind/VolumeProfile.mqh

141 lines
5.8 KiB
MQL5

//+------------------------------------------------------------------+
//| VolumeProfile.mqh |
//| AnimateDread |
//| |
//| TICK VOLUME WITH THE CLOCK TAKEN OUT. |
//| |
//| THE DEFECT THIS REPLACES. Relative volume was computed as the bar |
//| over the mean of the previous 20 bars in three places. On H1/H4 |
//| those 20 bars span several sessions, so the ratio answered "which |
//| hour is this?": measured 2026-09-30 (research/VOLNORM_RESULTS.md) |
//| 57-77% of its variance was hour-of-day. The cash open read as |
//| "high participation" every single day. |
//| |
//| THE FIX. Each bar is divided by the MEDIAN of the same hour-of-day |
//| slot over its previous SlotWindow occurrences (causal: prior bars |
//| only), in logs. After that R^2 on hour-of-day is 0.1-1.4%. |
//| |
//| THE SECOND DEFECT, NOT YET PROVEN FIXED. The slot median lags a |
//| level shift, and this feed's tick counts shift a lot (SP500 34M |
//| ticks in 2022, 10.5M in 2024; DAX40 258M -> 13M). The plain |
//| version left the yearly mean of the feature drifting more than the |
//| old one. Detrended() subtracts the mean of the previous TrendBars |
//| valid readings, so a step change is absorbed within a few days. |
//| That variant is UNTESTED as a predictor: the trade book records |
//| both, so the first backtest's journal is the test. |
//+------------------------------------------------------------------+
#ifndef WARRIOR_VOLUMEPROFILE_MQH
#define WARRIOR_VOLUMEPROFILE_MQH
#include "..\System\BarCache.mqh"
#define VOLPROF_NA (-999.0)
class CVolumeProfile
{
protected:
CBarCache *m_cache; // not owned
int m_slotWindow; // same-hour readings the median looks back over
int m_minSeen; // readings before a slot can answer
int m_trendBars; // level-detrend look-back, in valid readings
int m_done; // cache bars already folded in
double m_ring[]; // 24 slots x m_slotWindow volumes
int m_seen[24]; // valid readings pushed per slot (saturates at the window)
int m_head[24]; // next write position per slot
double m_raw[]; // per cache bar
double m_det[];
static int HourOf(const datetime t)
{
MqlDateTime s;
TimeToStruct(t, s);
return s.hour;
}
double SlotMedian(const int slot) const;
public:
CVolumeProfile(void) : m_cache(NULL), m_slotWindow(60), m_minSeen(20),
m_trendBars(20), m_done(0) {}
~CVolumeProfile(void) {}
void Init(CBarCache *cache, const int slotWindow = 60, const int trendBars = 20)
{
m_cache = cache;
m_slotWindow = MathMax(slotWindow, 10);
m_minSeen = MathMin(20, m_slotWindow);
m_trendBars = MathMax(trendBars, 5);
m_done = 0;
ArrayResize(m_ring, 24 * m_slotWindow);
ArrayInitialize(m_ring, 0.0);
ArrayInitialize(m_seen, 0);
ArrayInitialize(m_head, 0);
ArrayResize(m_raw, 0);
ArrayResize(m_det, 0);
}
//--- Fold in every cache bar not yet seen. Call after the cache has synced.
void Update(void);
//--- VOLPROF_NA until the slot has m_minSeen readings (raw) / the trend has 10 (detrended).
double Raw(const int i) const { return (i >= 0 && i < m_done) ? m_raw[i] : VOLPROF_NA; }
double Detrended(const int i) const { return (i >= 0 && i < m_done) ? m_det[i] : VOLPROF_NA; }
};
//+------------------------------------------------------------------+
double CVolumeProfile::SlotMedian(const int slot) const
{
const int n = MathMin(m_seen[slot], m_slotWindow);
double tmp[];
ArrayResize(tmp, n);
for(int k = 0; k < n; k++)
tmp[k] = m_ring[slot * m_slotWindow + k];
ArraySort(tmp);
return (n % 2 == 1) ? tmp[n / 2] : 0.5 * (tmp[n / 2 - 1] + tmp[n / 2]);
}
//+------------------------------------------------------------------+
void CVolumeProfile::Update(void)
{
if(m_cache == NULL)
return;
const int n = m_cache.Count();
if(n <= m_done)
return;
ArrayResize(m_raw, n);
ArrayResize(m_det, n);
for(int i = m_done; i < n; i++)
{
const int slot = HourOf(m_cache.Time(i));
const double v = m_cache.Volume(i);
double raw = VOLPROF_NA;
if(v > 0.0 && m_seen[slot] >= m_minSeen)
{
const double med = SlotMedian(slot);
if(med > 0.0)
raw = MathLog(v / med);
}
m_raw[i] = raw;
//--- Level detrend: this reading against the mean of the previous valid ones.
double det = VOLPROF_NA;
if(raw != VOLPROF_NA)
{
double s = 0.0;
int c = 0;
for(int j = i - 1; j >= 0 && c < m_trendBars; j--)
{
if(m_raw[j] == VOLPROF_NA)
continue;
s += m_raw[j];
c++;
}
if(c >= 10)
det = raw - s / c;
}
m_det[i] = det;
//--- Only AFTER the reading is taken does this bar enter its slot's history.
if(v > 0.0)
{
m_ring[slot * m_slotWindow + m_head[slot]] = v;
m_head[slot] = (m_head[slot] + 1) % m_slotWindow;
if(m_seen[slot] < m_slotWindow)
m_seen[slot]++;
}
}
m_done = n;
}
#endif // WARRIOR_VOLUMEPROFILE_MQH