#!/usr/bin/env python3 # ----------------------------------------------------------------------------- # validate_catch22.py MMQ - Muhammad Minhas Qamar # # Reference cross-check for the MQL5 CCatch22 engine. # # The MQL5 validation script (Catch22Validate.mq5) writes the *exact* window # it evaluated to MQL5\Files\Catch22\input_.csv (one value per line). # This tool loads that identical vector, runs the canonical pycatch22 # reference implementation on it, and prints the 22 features in the same # canonical order the MQL5 engine uses. Paste the two side by side (or use # --mql5 to auto-diff) to confirm the port. # # Usage: # python validate_catch22.py # uses input_fixture.csv # python validate_catch22.py --tag returns # python validate_catch22.py --csv path\to\vector.csv # python validate_catch22.py --mql5 mql5_output.txt # diff against MQL5 log # # Requires: pip install pycatch22 numpy # ----------------------------------------------------------------------------- import argparse import os import re import sys import numpy as np import pycatch22 # Canonical order used by ENUM_CATCH22 in Catch22.mqh. pycatch22 returns its # own order via catch22_all(); we remap into this list so column i here equals # column i in the MQL5 output. CANONICAL = [ "DN_HistogramMode_5", "DN_HistogramMode_10", "CO_f1ecac", "CO_FirstMin_ac", "CO_HistogramAMI_even_2_5", "CO_trev_1_num", "MD_hrv_classic_pnn40", "SB_BinaryStats_mean_longstretch1", "SB_TransitionMatrix_3ac_sumdiagcov", "PD_PeriodicityWang_th0_01", "CO_Embed2_Dist_tau_d_expfit_meandiff", "IN_AutoMutualInfoStats_40_gaussian_fmmi", "FC_LocalSimple_mean1_tauresrat", "DN_OutlierInclude_p_001_mdrmd", "DN_OutlierInclude_n_001_mdrmd", "SP_Summaries_welch_rect_area_5_1", "SB_BinaryStats_diff_longstretch0", "SB_MotifThree_quantile_hh", "SC_FluctAnal_2_rsrangefit_50_1_logi_prop_r1", "SC_FluctAnal_2_dfa_50_1_2_logi_prop_r1", "SP_Summaries_welch_rect_centroid", "FC_LocalSimple_mean3_stderr", ] # Default location of the MQL5 sandbox Files directory on this machine. FILES_DIR = os.path.join( os.environ.get("APPDATA", ""), r"MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Files\Catch22", ) def load_vector(path): """Load a one-value-per-line CSV written by the MQL5 script.""" vals = [] with open(path, "r") as fh: for line in fh: line = line.strip().rstrip(",") if line: vals.append(float(line)) return np.asarray(vals, dtype=float) def reference_features(x): """Run pycatch22 and return an ordered dict in CANONICAL order.""" res = pycatch22.catch22_all(x, catch24=False) by_name = dict(zip(res["names"], res["values"])) out = [] for name in CANONICAL: out.append(by_name.get(name, float("nan"))) return out def parse_mql5_log(path): """Extract 'NN feature_name = value' lines from a pasted MQL5 log.""" got = {} pat = re.compile(r"\d+\s+([A-Za-z0-9_]+)\s*=\s*(-?\d+\.?\d*(?:[eE][-+]?\d+)?)") with open(path, "r") as fh: for line in fh: m = pat.search(line) if m: got[m.group(1)] = float(m.group(2)) return got def main(): ap = argparse.ArgumentParser(description="pycatch22 reference cross-check") ap.add_argument("--tag", default="fixture", help="input_.csv in Files\\Catch22") ap.add_argument("--csv", default=None, help="explicit path to the input vector") ap.add_argument("--mql5", default=None, help="MQL5 log file to auto-diff against") args = ap.parse_args() csv_path = args.csv or os.path.join(FILES_DIR, f"input_{args.tag}.csv") if not os.path.isfile(csv_path): sys.exit(f"input vector not found: {csv_path}") x = load_vector(csv_path) print(f"loaded {len(x)} values from {csv_path}\n") ref = reference_features(x) mql5 = parse_mql5_log(args.mql5) if args.mql5 else None if mql5: hdr = f"{'#':>2} {'feature':<46} {'pycatch22':>14} {'mql5':>14} {'abs.diff':>12}" else: hdr = f"{'#':>2} {'feature':<46} {'pycatch22':>14}" print(hdr) print("-" * len(hdr)) max_rel = 0.0 for i, name in enumerate(CANONICAL): rv = ref[i] if mql5 is not None: mv = mql5.get(name, float("nan")) diff = abs(rv - mv) denom = max(1e-9, abs(rv)) max_rel = max(max_rel, diff / denom) flag = "" if diff / denom < 0.02 else " <-- CHECK" print(f"{i:>2} {name:<46} {rv:>14.8f} {mv:>14.8f} {diff:>12.6f}{flag}") else: print(f"{i:>2} {name:<46} {rv:>14.8f}") if mql5 is not None: print(f"\nmax relative deviation: {max_rel:.4%}") if __name__ == "__main__": main()