//+------------------------------------------------------------------+ //| 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