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