//+------------------------------------------------------------------+ //| FFDEngine.mqh — Fixed-Width Fractional Differencing Engine | //| Copyright 2025, Patrick M. Njoroge | //| | //| Implements the FFD method from: | //| López de Prado, M. (2018). Advances in Financial Machine | //| Learning, Chapter 5, Section 5.4.2, p. 83. | //| | //| This file is a numerical translation of the Python functions | //| get_weights_ffd() and frac_diff_ffd() | //| in the afml library. It is designed to produce output that | //| cross-validates against those functions within 1e-12 on the | //| same price series (see FFDValidation.mq5). | //| | //| Weight recurrence (AFML eq. 5.4): | //| w[0] = 1 | //| w[k] = -w[k-1] * (d - k + 1) / k, k = 1, 2, ... | //| Iteration stops when |w[k]| < threshold. | //| The vector is then reversed so the oldest price receives the | //| smallest-magnitude weight and the newest receives 1.0. | //| | //| Numerical contract: | //| - Log transform: p -> ln(max(p, 1e-8)) when use_log = true. | //| The 1e-8 floor matches Python's clip(lower=1e-8) exactly. | //| - Output bars [0, width-1] are set to EMPTY_VALUE. | //| - Output bars [width, N-1] carry FFD values. | //| - Compute() and ComputeBuffer() must agree to within 1e-12. | //| | //| File placement: | //| FFDEngine.mqh -> MQL5\Include\ | //| FFD.mq5 -> MQL5\Indicators\ | //| FFDValidation.mq5 -> MQL5\Scripts\ | //+------------------------------------------------------------------+ #property copyright "Patrick M. Njoroge" #property link "https://www.mql5.com/en/users/patricknjoroge743" #ifndef FFDENGINE_MQH #define FFDENGINE_MQH //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CFFDEngine { private: double m_weights[]; // Reversed weight vector, length = m_width + 1. // m_weights[0] = smallest |w|, applied to oldest price. // m_weights[m_width] = 1.0, applied to newest price. int m_width; // Number of lags = len(weights_before_reversal) - 1. double m_d; // Differentiation order. double m_threshold; // Weight cutoff: iteration stops when |w_k| < threshold. bool m_use_log; // Apply ln(p) transform before differencing. bool m_initialized; // Set to true after a successful Init() call. //--- Build m_weights using the FFD recurrence. No fixed cap. void BuildWeights(); //--- Log with 1e-8 floor, matching Python's clip(lower=1e-8). double SafeLog(double price) const { if(price < 1e-8) price = 1e-8; return MathLog(price); } public: CFFDEngine() : m_d(0.0), m_threshold(1e-5), m_use_log(true), m_width(0), m_initialized(false) {} //--- Initialize the engine and precompute the weight vector. // Call once before Compute() or ComputeBuffer(). // // @param d Fractional-differencing order. Typical range: (0, 1). // @param threshold Weight cutoff tau. Smaller values widen the window // and preserve more memory. Typical: 1e-4 to 1e-5. // @param use_log If true, apply ln(p) before differencing. Recommended // for raw price series; set false for already-stationary // inputs (e.g., log-returns). // @return true on success, false if the weight vector is empty. bool Init(double d, double threshold = 1e-5, bool use_log = true); //--- Accessors int GetWidth() const { return m_width; } int GetMinBars() const { return m_width + 1; } double GetD() const { return m_d; } bool IsReady() const { return m_initialized; } //--- Compute a single FFD value for the most recent bar in prices[]. // // prices[] must be chronological: prices[0] = oldest, prices[count-1] = newest. // count must be >= GetMinBars(). The function uses the final (m_width + 1) // elements regardless of how many extra elements prices[] contains. // // @return FFD value, or EMPTY_VALUE on error. double Compute(const double &prices[], int count); //--- Fill an indicator buffer with FFD values. // prices[] and buffer[] must be chronological (AsSeries = false). // buffer[0..m_width-1] = EMPTY_VALUE (lookback not satisfied). // buffer[m_width..total-1] = FFD values. // // Uses prev_calculated to skip already-computed bars. // Pass prev_calculated = 0 to compute the full buffer from scratch. // // @return total (mirrors the OnCalculate return convention). int ComputeBuffer(const double &prices[], double &buffer[], int total, int prev_calculated); }; //+------------------------------------------------------------------+ //| BuildWeights | //| | //| Translates Python's get_weights_ffd(d, thres, lim) with no cap. | //| | //| The original code had max_weights = 1000. For d = 0.4, thres = | //| 1e-5, the correct window width is 1457, so a cap of 1000 | //| silently truncates the weight vector and corrupts all FFD values.| //| This version grows the buffer dynamically — no fixed cap. | //+------------------------------------------------------------------+ void CFFDEngine::BuildWeights() { //--- Start with capacity 512; double when full. int capacity = 512; double temp[]; ArrayResize(temp, capacity); temp[0] = 1.0; int n = 1; for(int k = 1; ; k++) { double w_next = -temp[n - 1] * (m_d - (double)k + 1.0) / (double)k; if(MathAbs(w_next) < m_threshold) break; // mirrors: "if abs(weights_) < thres: break" if(n >= capacity) { capacity *= 2; ArrayResize(temp, capacity); } temp[n] = w_next; n++; } ArrayResize(temp, n); //--- Reverse into m_weights. // m_weights[0] = temp[n-1] → smallest |w|, multiplies oldest price. // m_weights[n-1] = temp[0] = 1.0, multiplies newest price. m_width = n - 1; ArrayResize(m_weights, n); for(int i = 0; i < n; i++) m_weights[i] = temp[n - 1 - i]; } //+------------------------------------------------------------------+ //| Init | //+------------------------------------------------------------------+ bool CFFDEngine::Init(double d, double threshold = 1e-5, bool use_log = true) { if(d < 0.0 || d > 2.0) { PrintFormat("CFFDEngine::Init — d must be in [0, 2], got %.4f", d); return false; } if(threshold <= 0.0) { Print("CFFDEngine::Init — threshold must be positive"); return false; } m_d = d; m_threshold = threshold; m_use_log = use_log; BuildWeights(); m_initialized = (m_width > 0); if(m_initialized) PrintFormat("CFFDEngine: d=%.4f threshold=%.2e width=%d min_bars=%d use_log=%s", m_d, m_threshold, m_width, GetMinBars(), m_use_log ? "true" : "false"); else Print("CFFDEngine::Init — no weights generated (d or threshold may be extreme)"); return m_initialized; } //+------------------------------------------------------------------+ //| Compute — single FFD value for the last bar in prices[] | //+------------------------------------------------------------------+ double CFFDEngine::Compute(const double &prices[], int count) { if(!m_initialized || count < m_width + 1) return EMPTY_VALUE; //--- Window anchored at the newest element (prices[count-1]). int window_start = count - m_width - 1; double result = 0.0; for(int i = 0; i <= m_width; i++) { double val = prices[window_start + i]; if(m_use_log) val = SafeLog(val); result += m_weights[i] * val; } return result; } //+------------------------------------------------------------------+ //| ComputeBuffer — fill an entire chronological indicator buffer | //+------------------------------------------------------------------+ int CFFDEngine::ComputeBuffer(const double &prices[], double &buffer[], int total, int prev_calculated) { if(!m_initialized) return 0; int start; if(prev_calculated > m_width) start = prev_calculated - 1; // incremental: only recompute current bar else { // Full computation: mark the lookback bars as empty. for(int i = 0; i < m_width && i < total; i++) buffer[i] = EMPTY_VALUE; start = m_width; } for(int i = start; i < total; i++) { double result = 0.0; for(int k = 0; k <= m_width; k++) { //--- Index breakdown for bar i: // k = 0 → prices[i - m_width] = oldest in window // k = m_width → prices[i] = newest (current bar) double val = prices[i - m_width + k]; if(m_use_log) val = SafeLog(val); result += m_weights[k] * val; } buffer[i] = result; } return total; } //+------------------------------------------------------------------+ #endif // FFDENGINE_MQH //+------------------------------------------------------------------+