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.
248 lines
9.4 KiB
MQL5
248 lines
9.4 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| BarCache.mqh |
|
|
//| AnimateDread |
|
|
//| |
|
|
//| THE WHOLE CLOSED-BAR HISTORY OF ONE SYMBOL/TIMEFRAME, in our own |
|
|
//| arrays, oldest first - and the three series the vol-gated dip |
|
|
//| rule is built from, computed exactly the way research/backtest.py |
|
|
//| computes them. |
|
|
//| |
|
|
//| WHY NOT THE STDLIB SERIES. CSeries reads past shift 1023 return |
|
|
//| 0.0 in silence, and the volatility gate is an EXPANDING-window |
|
|
//| percentile: it ranks today's sigma against every sigma since the |
|
|
//| first bar. A 1024-bar window would still produce a plausible |
|
|
//| number - just a different rule from the one that was tested. |
|
|
//| DeepenPrices() could grow the buffers, but the gate would then |
|
|
//| re-walk thousands of bars through virtual accessors every bar; |
|
|
//| here each closed bar is appended once and its derived values are |
|
|
//| computed once. |
|
|
//| |
|
|
//| WHY WILDER ATR, NOT iATR. MT5's iATR is a plain SMA of the true |
|
|
//| range. The research priced the stop with Wilder's recursion, |
|
|
//| seeded with the mean of the first P ranges; the two differ on |
|
|
//| every bar, so the stop - and the lot it sizes - would differ on |
|
|
//| every trade. |
|
|
//| |
|
|
//| Ported rule for rule from mql5/WarriorDipZ.mq5 (CSym, Append, |
|
|
//| Sync, MeanStd, VolPct), which reconciled with the Python backtest |
|
|
//| at a per-trade correlation of 0.998. |
|
|
//+------------------------------------------------------------------+
|
|
#ifndef WARRIOR_BARCACHE_MQH
|
|
#define WARRIOR_BARCACHE_MQH
|
|
|
|
#define BARCACHE_NA -1.0 // "not yet computable" - sigma, ATR and percentile are all >= 0
|
|
|
|
class CBarCache
|
|
{
|
|
protected:
|
|
string m_symbol;
|
|
ENUM_TIMEFRAMES m_tf;
|
|
int m_atrPeriod;
|
|
int m_volWindow;
|
|
int m_n;
|
|
datetime m_t[];
|
|
double m_o[], m_h[], m_l[], m_c[], m_tr[], m_atr[], m_gk[], m_sig[], m_v[];
|
|
|
|
void Grow(const int k);
|
|
void Append(const MqlRates &b);
|
|
|
|
public:
|
|
CBarCache(void) : m_symbol(""), m_tf(PERIOD_CURRENT), m_atrPeriod(14),
|
|
m_volWindow(30), m_n(0) {}
|
|
~CBarCache(void) {}
|
|
|
|
void Init(const string symbol, const ENUM_TIMEFRAMES tf,
|
|
const int atrPeriod, const int volWindow)
|
|
{
|
|
m_symbol = symbol; m_tf = tf; m_atrPeriod = atrPeriod; m_volWindow = volWindow; m_n = 0;
|
|
}
|
|
//--- Bring the cache up to the newest CLOSED bar (shift 1). False = not there yet; try again.
|
|
bool Sync(void);
|
|
int Count(void) const { return m_n; }
|
|
int Last(void) const { return m_n - 1; }
|
|
datetime Time(const int i) const { return m_t[i]; }
|
|
double Open(const int i) const { return m_o[i]; }
|
|
double High(const int i) const { return m_h[i]; }
|
|
double Low(const int i) const { return m_l[i]; }
|
|
double Close(const int i) const { return m_c[i]; }
|
|
//--- TICK volume (CFD real volume is 0). Raw counts: the feed's level drifts up to 20x between
|
|
//--- years, so read it through CVolumeProfile, never on its own.
|
|
double Volume(const int i) const { return m_v[i]; }
|
|
double Atr(const int i) const { return m_atr[i]; }
|
|
double Sigma(const int i) const { return m_sig[i]; }
|
|
//--- The `n` closes ending at bar i, NEWEST FIRST (out[0] = Close(i)) - the orientation
|
|
//--- CRegimeMath takes. False when fewer than n bars exist.
|
|
bool ClosesBack(const int i, const int n, double &out[]) const;
|
|
//--- Index of the bar that opened at `t`, or -1. Binary search: the cache is time-ordered.
|
|
int IndexOf(const datetime t) const;
|
|
//--- Mean and POPULATION deviation of the `period` closes ending at i.
|
|
bool MeanStd(const int i, const int period, double &mean, double &sd) const;
|
|
//--- Causal expanding-window percentile of sigma[i] against every EARLIER valid sigma.
|
|
//--- BARCACHE_NA until `warm` bars and more than 50 samples.
|
|
double VolPercentile(const int i, const int warm) const;
|
|
};
|
|
//+------------------------------------------------------------------+
|
|
void CBarCache::Grow(const int k)
|
|
{
|
|
if(ArraySize(m_t) > k)
|
|
return;
|
|
const int cap = MathMax(1024, k * 2);
|
|
ArrayResize(m_t, cap);
|
|
ArrayResize(m_o, cap);
|
|
ArrayResize(m_h, cap);
|
|
ArrayResize(m_l, cap);
|
|
ArrayResize(m_c, cap);
|
|
ArrayResize(m_tr, cap);
|
|
ArrayResize(m_atr, cap);
|
|
ArrayResize(m_gk, cap);
|
|
ArrayResize(m_sig, cap);
|
|
ArrayResize(m_v, cap);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| One CLOSED bar in, every derived series updated. |
|
|
//+------------------------------------------------------------------+
|
|
void CBarCache::Append(const MqlRates &b)
|
|
{
|
|
const int k = m_n;
|
|
Grow(k + 1);
|
|
m_t[k] = b.time;
|
|
m_o[k] = b.open;
|
|
m_h[k] = b.high;
|
|
m_l[k] = b.low;
|
|
m_c[k] = b.close;
|
|
m_v[k] = (double)b.tick_volume;
|
|
//--- Garman-Klass variance. Non-negative for any valid bar; a bad bar is stored as 0 so it
|
|
//--- cannot dominate the rolling mean.
|
|
double g = 0.0;
|
|
if(b.open > 0 && b.high > 0 && b.low > 0 && b.close > 0 && b.high >= b.low)
|
|
{
|
|
const double hl = MathLog(b.high / b.low);
|
|
const double co = MathLog(b.close / b.open);
|
|
g = 0.5 * hl * hl - (2.0 * MathLog(2.0) - 1.0) * co * co;
|
|
}
|
|
m_gk[k] = g;
|
|
//--- Wilder ATR, seeded with the mean of the first P true ranges.
|
|
if(k == 0)
|
|
m_tr[k] = b.high - b.low;
|
|
else
|
|
m_tr[k] = MathMax(b.high - b.low,
|
|
MathMax(MathAbs(b.high - m_c[k - 1]), MathAbs(b.low - m_c[k - 1])));
|
|
const int P = m_atrPeriod;
|
|
if(k < P - 1)
|
|
m_atr[k] = BARCACHE_NA;
|
|
else if(k == P - 1)
|
|
{
|
|
double sum = 0.0;
|
|
for(int j = 0; j < P; j++)
|
|
sum += m_tr[j];
|
|
m_atr[k] = sum / P;
|
|
}
|
|
else
|
|
m_atr[k] = (m_atr[k - 1] * (P - 1) + m_tr[k]) / P;
|
|
//--- Rolling GK sigma.
|
|
const int W = m_volWindow;
|
|
if(k < W - 1)
|
|
m_sig[k] = BARCACHE_NA;
|
|
else
|
|
{
|
|
double sum = 0.0;
|
|
for(int j = k - W + 1; j <= k; j++)
|
|
sum += m_gk[j];
|
|
m_sig[k] = MathSqrt(MathMax(sum / W, 0.0));
|
|
}
|
|
m_n++;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
bool CBarCache::Sync(void)
|
|
{
|
|
const datetime newestClosed = iTime(m_symbol, m_tf, 1);
|
|
if(newestClosed == 0)
|
|
return false;
|
|
if(m_n > 0 && m_t[m_n - 1] >= newestClosed)
|
|
return true;
|
|
MqlRates r[];
|
|
int got;
|
|
if(m_n == 0)
|
|
{
|
|
const int total = Bars(m_symbol, m_tf);
|
|
if(total < 3)
|
|
return false;
|
|
got = CopyRates(m_symbol, m_tf, 1, total - 1, r);
|
|
}
|
|
else
|
|
{
|
|
const int shift = iBarShift(m_symbol, m_tf, m_t[m_n - 1], true);
|
|
if(shift < 0)
|
|
return false; // our last bar vanished from history - wait
|
|
if(shift <= 1)
|
|
return true;
|
|
got = CopyRates(m_symbol, m_tf, 1, shift - 1, r);
|
|
}
|
|
if(got <= 0)
|
|
return false;
|
|
//--- CopyRates into a non-series array is oldest-first, the cache's own order.
|
|
for(int i = 0; i < got; i++)
|
|
if(m_n == 0 || r[i].time > m_t[m_n - 1])
|
|
Append(r[i]);
|
|
return (m_n > 0 && m_t[m_n - 1] == newestClosed);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
bool CBarCache::ClosesBack(const int i, const int n, double &out[]) const
|
|
{
|
|
if(n < 1 || i < n - 1 || i >= m_n)
|
|
return false;
|
|
ArrayResize(out, n);
|
|
for(int k = 0; k < n; k++)
|
|
out[k] = m_c[i - k];
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
int CBarCache::IndexOf(const datetime t) const
|
|
{
|
|
int lo = 0, hi = m_n - 1;
|
|
while(lo <= hi)
|
|
{
|
|
const int mid = (lo + hi) / 2;
|
|
if(m_t[mid] == t)
|
|
return mid;
|
|
if(m_t[mid] < t)
|
|
lo = mid + 1;
|
|
else
|
|
hi = mid - 1;
|
|
}
|
|
return -1;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
bool CBarCache::MeanStd(const int i, const int period, double &mean, double &sd) const
|
|
{
|
|
if(i < period - 1 || i >= m_n)
|
|
return false;
|
|
double sum = 0.0, sum2 = 0.0;
|
|
for(int j = i - period + 1; j <= i; j++)
|
|
{
|
|
sum += m_c[j];
|
|
sum2 += m_c[j] * m_c[j];
|
|
}
|
|
mean = sum / period;
|
|
sd = MathSqrt(MathMax(sum2 / period - mean * mean, 0.0));
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
double CBarCache::VolPercentile(const int i, const int warm) const
|
|
{
|
|
if(i < warm || i >= m_n || m_sig[i] < 0.0)
|
|
return BARCACHE_NA;
|
|
int cnt = 0, less = 0;
|
|
for(int k = m_volWindow - 1; k < i; k++)
|
|
{
|
|
if(m_sig[k] < 0.0)
|
|
continue;
|
|
cnt++;
|
|
if(m_sig[k] < m_sig[i])
|
|
less++;
|
|
}
|
|
if(cnt <= 50)
|
|
return BARCACHE_NA;
|
|
return (double)less / cnt;
|
|
}
|
|
#endif // WARRIOR_BARCACHE_MQH
|