//+------------------------------------------------------------------+ //| CImbalanceBars.mqh | //| Tick direction classifier and imbalance bar constructor. | //| Updated from the previous article: CTickRule now exposes | //| SetPrevPrice() and SetPrevSign() so that CRunBar::LoadState() | //| can restore tick-rule state after an EA restart. | //+------------------------------------------------------------------+ #include //+------------------------------------------------------------------+ //| Metric selector — shared by CImbalanceBar and CRunBar | //+------------------------------------------------------------------+ enum ENUM_IMBALANCE_METRIC { IMBALANCE_TICK = 0, IMBALANCE_VOLUME = 1, IMBALANCE_DOLLAR = 2, }; //+------------------------------------------------------------------+ //| CTickRule — tick-direction classifier | //| | //| Rule: +1 if price rose, -1 if price fell, carry forward if flat. | //| Default state is +1 (matches the Python kernel's b[0] = 1.0). | //+------------------------------------------------------------------+ class CTickRule { private: double m_prev_price; double m_prev_sign; public: CTickRule(void) : m_prev_price(0.0), m_prev_sign(1.0) {} double Classify(const double price) { if(m_prev_price == 0.0) // first tick seen { m_prev_price = price; return m_prev_sign; } if(price > m_prev_price) m_prev_sign = 1.0; else if(price < m_prev_price) m_prev_sign = -1.0; // else: unchanged price → carry forward m_prev_sign m_prev_price = price; return m_prev_sign; } // ── Read-only accessors ───────────────────────────────────────── double PrevPrice(void) const { return m_prev_price; } double PrevSign(void) const { return m_prev_sign; } // ── Write accessors (new) — required by CRunBar::LoadState() ─── void SetPrevPrice(const double p) { m_prev_price = p; } void SetPrevSign(const double s) { m_prev_sign = s; } }; //+------------------------------------------------------------------+ //| CImbalanceBar | //| | //| Closes when |theta_T| >= E0[T] * E0[|imbalance per tick|]. | //| Both expectations are updated with O(1) EWM after each close. | //+------------------------------------------------------------------+ class CImbalanceBar : public CBarConstructor { private: CTickRule m_tick_rule; ENUM_IMBALANCE_METRIC m_metric_type; double m_theta; double m_ewm_T; double m_ewm_abs_theta; double m_exp_T; double m_exp_abs_imb; double m_ewm_alpha; long m_bar_tick_count; double TickMetric(const MqlTick &tick, const double sign) const; public: CImbalanceBar(ENUM_IMBALANCE_METRIC metric_type, double exp_ticks_init, double exp_imbalance_init, int ewm_span = 20); virtual bool ProcessTick(const MqlTick &tick, const long tick_num, SBar &out_bar) override; virtual bool SaveState(const int file_handle) override; virtual bool LoadState(const int file_handle) override; // ── Diagnostic accessors ───────────────────────────────────────── double CurrentTheta(void) const { return m_theta; } double CurrentThreshold(void) const { return m_exp_T * m_exp_abs_imb; } double ExpT(void) const { return m_exp_T; } double ExpAbsImb(void) const { return m_exp_abs_imb; } }; //+------------------------------------------------------------------+ CImbalanceBar::CImbalanceBar(ENUM_IMBALANCE_METRIC metric_type, double exp_ticks_init, double exp_imbalance_init, int ewm_span) : CBarConstructor(EnumToString(metric_type) == "IMBALANCE_TICK" ? "tick_imbalance" : EnumToString(metric_type) == "IMBALANCE_VOLUME" ? "volume_imbalance" : "dollar_imbalance"), m_metric_type(metric_type), m_theta(0.0), m_bar_tick_count(0) { m_ewm_alpha = 2.0 / (ewm_span + 1); m_ewm_T = MathMax(exp_ticks_init, 1.0); m_ewm_abs_theta = MathMax(MathAbs(exp_imbalance_init), 1e-10) * m_ewm_T; m_exp_T = m_ewm_T; m_exp_abs_imb = MathMax(MathAbs(exp_imbalance_init), 1e-10); } //+------------------------------------------------------------------+ double CImbalanceBar::TickMetric(const MqlTick &tick, const double sign) const { switch(m_metric_type) { case IMBALANCE_TICK: return sign; case IMBALANCE_VOLUME: return sign * tick.volume; case IMBALANCE_DOLLAR: return sign * tick.bid * tick.volume; default: return sign; } } //+------------------------------------------------------------------+ bool CImbalanceBar::ProcessTick(const MqlTick &tick, const long tick_num, SBar &out_bar) { if(!m_initialized) { SeedBar(tick, tick_num); return false; } UpdateAccumulator(tick, tick_num); m_bar_tick_count++; double sign = m_tick_rule.Classify(tick.bid); m_theta += TickMetric(tick, sign); if(MathAbs(m_theta) >= m_exp_T * m_exp_abs_imb) { FillBar(out_bar); double bar_len = (double)m_bar_tick_count; double one_minus_a = 1.0 - m_ewm_alpha; m_ewm_T = m_ewm_alpha * bar_len + one_minus_a * m_ewm_T; m_ewm_abs_theta = m_ewm_alpha * MathAbs(m_theta) + one_minus_a * m_ewm_abs_theta; m_exp_T = m_ewm_T; m_exp_abs_imb = m_ewm_abs_theta / MathMax(m_exp_T, 1.0); m_theta = 0.0; m_bar_tick_count = 0; SeedBar(tick, tick_num); return true; } return false; } //+------------------------------------------------------------------+ bool CImbalanceBar::SaveState(const int fh) { if(!CBarConstructor::SaveState(fh)) return false; FileWriteDouble(fh, m_theta); FileWriteDouble(fh, m_ewm_T); FileWriteDouble(fh, m_ewm_abs_theta); FileWriteDouble(fh, m_exp_T); FileWriteDouble(fh, m_exp_abs_imb); FileWriteLong(fh, m_bar_tick_count); FileWriteDouble(fh, m_tick_rule.PrevPrice()); FileWriteDouble(fh, m_tick_rule.PrevSign()); return true; } //+------------------------------------------------------------------+ bool CImbalanceBar::LoadState(const int fh) { if(!CBarConstructor::LoadState(fh)) return false; m_theta = FileReadDouble(fh); m_ewm_T = FileReadDouble(fh); m_ewm_abs_theta = FileReadDouble(fh); m_exp_T = FileReadDouble(fh); m_exp_abs_imb = FileReadDouble(fh); m_bar_tick_count = FileReadLong(fh); m_tick_rule.SetPrevPrice(FileReadDouble(fh)); m_tick_rule.SetPrevSign(FileReadDouble(fh)); return true; } //+------------------------------------------------------------------+