//+------------------------------------------------------------------+ //| FFDValidation.mq5 — Export FFD values for Python cross-check | //| Copyright 2025, Patrick M. Njoroge | //| | //| Run as a script. Exports close prices and FFD values to CSV. | //| Compare the CSV output against Python's frac_diff_ffd() to | //| verify numerical equivalence (see ffd_cross_validate.py). | //+------------------------------------------------------------------+ #property copyright "Patrick M. Njoroge" #property link "https://www.mql5.com/en/users/patricknjoroge743" #property version "1.11" #property description "Exports FFD values for cross-validation with Python" #property script_show_inputs //--- Inputs input double InpD = 0.4; // Differencing order d input double InpThreshold = 1e-5; // Weight cutoff threshold input bool InpUseLog = true; // Log-transform prices input int InpBars = 5000; // Number of bars to export //--- Include the computation engine from MQL5\Include\ #include "FFDEngine.mqh" //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- Initialize engine CFFDEngine engine; if(!engine.Init(InpD, InpThreshold, InpUseLog)) { Print("Validation: engine initialization failed"); return; } PrintFormat("Validation: requesting %d bars, engine width=%d", InpBars, engine.GetWidth()); //--- Copy close prices double close[]; int copied = CopyClose(_Symbol, _Period, 0, InpBars, close); if(copied < engine.GetMinBars()) { PrintFormat("Not enough bars: got %d, need at least %d", copied, engine.GetMinBars()); return; } //--- Ensure chronological order (oldest first). // CopyClose returns chronological by default, but if any other // code in the terminal has set ArraySetAsSeries on this buffer // globally, the order silently reverses and all FFD values are // wrong. This defensive call costs nothing and prevents that. ArraySetAsSeries(close, false); PrintFormat("Copied %d bars", copied); //--- Copy timestamps for reference datetime time[]; CopyTime(_Symbol, _Period, 0, InpBars, time); ArraySetAsSeries(time, false); //--- Compute FFD for all bars double ffd_buffer[]; ArrayResize(ffd_buffer, copied); engine.ComputeBuffer(close, ffd_buffer, copied, 0); //--- Write to CSV string filename = StringFormat("ffd_validation_%s_%s_d%.2f.csv", _Symbol, EnumToString(_Period), InpD); int file = FileOpen(filename, FILE_WRITE | FILE_CSV, ","); if(file == INVALID_HANDLE) { Print("Cannot open file: ", filename); return; } //--- Header FileWrite(file, "bar_index", "datetime", "close", "ffd"); //--- Data rows (skip EMPTY_VALUE bars) int written = 0; for(int i = 0; i < copied; i++) { if(ffd_buffer[i] != EMPTY_VALUE) { FileWrite(file, IntegerToString(i), TimeToString(time[i], TIME_DATE | TIME_SECONDS), DoubleToString(close[i], 8), DoubleToString(ffd_buffer[i], 12)); written++; } } FileClose(file); PrintFormat("Validation file written: %s (%d data rows out of %d bars)", filename, written, copied); PrintFormat("Expected %d empty bars (width) + %d data bars = %d total", engine.GetWidth(), written, engine.GetWidth() + written); //--- Self-check: verify the first data bar via Compute() if(written > 0) { int first_bar = engine.GetWidth(); int window = engine.GetMinBars(); // Extract the first valid price window and compute independently double check_prices[]; ArrayResize(check_prices, window); for(int k = 0; k < window; k++) check_prices[k] = close[k]; double check_val = engine.Compute(check_prices, window); double buffer_val = ffd_buffer[first_bar]; double diff = MathAbs(check_val - buffer_val); PrintFormat("Self-check (bar %d): Compute=%.12f, Buffer=%.12f, diff=%.2e", first_bar, check_val, buffer_val, diff); if(diff > 1e-12) Print("WARNING: Compute() and ComputeBuffer() disagree."); else Print("Self-check PASSED: Compute() and ComputeBuffer() agree."); } } //+------------------------------------------------------------------+