140 lines
4.8 KiB
Python
140 lines
4.8 KiB
Python
#!/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()
|