forked from animatedread/Warrior_EA
- Introduced `FeatureScale.mqh` with `FeatSquash` function for stateless feature scaling. - Added `RegimeMath.mqh` class for regime arithmetic, including efficiency and variance calculations. - Documented the Mind trading logic in `MIND.md`, detailing the trading process and modes. - Created `VOLNORM_PLAN.md` and `VOLNORM_RESULTS.md` for tick-volume normalization testing. - Implemented `read_book.py` for analyzing trade book data and correlations. - Developed `volnorm.py` for testing tick-volume normalization with new and old methods.
141 lines
5.8 KiB
MQL5
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
|