catch22/Catch22/validate_catch22.py

140 lines
4.8 KiB
Python
Raw Permalink Normal View History

2026-07-12 10:17:20 +00:00
#!/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_<tag>.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_<tag>.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()