Warrior_EA/Tests/convert_sample_data.py

91 lines
4.8 KiB
Python
Raw Permalink Normal View History

test(unit): add MQL5 unit-test EAs for the now-decoupled arithmetic modules CFirstPassageLadder (RungFor/StoreBar/FirstTouch/OutcomeR/WinShare), CTripleBarrier+CLabelOverlap (ApplyMinStopWidening/ComputeLevels/SnapToLadder, the label-overlap effective-sample-size correction), CMetaFamilies (the classic-pattern taxonomy + table-naming rule), SGeometryScan::Reset() (guards against 7452bd1's partial-reset shape recurring) and System/BinomialStats.mqh (every deploy-gate/edge-floor formula this codebase shares). Each is a small .mq5 Expert Advisor under Tests\ printing PASS/FAIL per assertion via Print(), sharing Tests\TestHarness.mqh. All 5 self-compile-verified 0 errors/0 warnings. FirstPassageLadder.mqh/TripleBarrier.mqh expect BARRIER_LADDER_COUNT/BARRIER_LADDER/BARRIER_HORIZON_LADDER_COUNT predefined by their includer (normally ExpertSignalAIBase.mqh); the test EAs define copies matching production values rather than including the whole AIBase chain. SGeometryScan is reproduced verbatim from ExpertSignalAIBase.mqh for the same reason, flagged in-file as needing to stay byte-identical. Also adds Tests\convert_sample_data.py, which runs research/sqxbars.py's decoder against a COPY of SP500_the5ers_H1.dat (never the SQX install itself) so the operator has real sample data to point a manual tester run at. Decodes structurally (52,542 H1 bars, monotonic, 0 high<low violations) without calibrating a price scale - sqxbars.load()/sqx.calibrate_decimals() both require a validated reference series to do that safely, which this self-contained script does not have. Tests\sample_data\ (the raw copy + decoded .npz) is gitignored, same policy as the existing Market Data/ rule. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 04:28:49 -04:00
r"""Runs the SQX bar decoder (research/sqxbars.py, which wraps research/sqx.py) on a COPY
of one SQX history file, so the operator has real decoded sample data to point a manual
backtest/tester run at without touching the SQX install itself.
WHY A COPY, AND WHY THIS SCRIPT DOESN'T EDIT sqxbars.py:
Tests\sample_data\SQX_History\SP500_the5ers\SP500_the5ers_H1.dat is a copy of
SQX_144_2953_win_20260601\user\data\History\SP500_the5ers\SP500_the5ers_H1.dat - the
original was never opened for writing, only read by `cp`. sqxbars.py hardcodes its own
HIST/CACHE module constants (both point at locations OUTSIDE this repo, one of them inside
the live SQX install), so rather than edit those constants in place or call sqxbars.load()
(which reads through HIST/CACHE), this script calls sqxbars.decode() directly with an
explicit path built from the COPY - bypassing HIST/CACHE entirely, so nothing in sqxbars.py
needs to change and the production research scripts (sqx_audit.py, sqx_portfolio.py,
breadth_seasonal.py, ...) are untouched and keep reading the real install.
WHY THIS DOES NOT PRODUCE SCALED PRICES:
sqxbars.load() requires either an explicit `decimals=` or a `ref=(times, closes)` validated
reference series to CALIBRATE the decimal scale - see sqxbars.calibrate()'s own docstring
and sqx.py's calibrate_decimals(): the scale is not recoverable from the file alone, and
guessing it is exactly the "silent wrong scale" trap this project's own notes warn about
twice already (project_sqx_tick_format.md, project_sqx_bar_decoder.md). This script has no
validated reference series available (fills.Book() pulls from this machine's own live
fill-engine data, out of scope for a self-contained sample), so it calls sqxbars.decode()
directly - the raw, un-scaled int64 decode - which proves the parser reads the copied file
correctly (record count, monotonic time, structural sanity) without fabricating a price
scale. The saved .npz keeps the raw integer columns; see the printed note for how the
operator can calibrate them for real use.
Usage: .venv\Scripts\python.exe Tests\convert_sample_data.py
"""
import os
import sys
import datetime as dt
import numpy as np
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SAMPLE_HIST = os.path.join(REPO_ROOT, "Tests", "sample_data", "SQX_History") + os.sep
SAMPLE_OUT = os.path.join(REPO_ROOT, "Tests", "sample_data", "converted") + os.sep
sys.path.insert(0, os.path.join(REPO_ROOT, "research"))
import sqxbars # noqa: E402 (path must be extended first)
SYMBOL = "SP500_the5ers"
TIMEFRAME = "H1"
def main():
os.makedirs(SAMPLE_OUT, exist_ok=True)
src = os.path.join(SAMPLE_HIST, SYMBOL, f"{SYMBOL}_{TIMEFRAME}.dat")
if not os.path.exists(src):
raise FileNotFoundError(
f"{src} - expected the copied sample .dat here; see this file's docstring.")
print(f"=== SQX SAMPLE CONVERSION: {SYMBOL} {TIMEFRAME} ===")
print(f"source (a COPY, not the SQX install): {src}")
rows, nf = sqxbars.decode(src)
print(f"decoded {len(rows):,} bar records, {nf} fields per record")
if len(rows) == 0:
raise ValueError("decoder produced zero records - the copy may be truncated or corrupt")
t_ms = rows[:, 0]
monotonic = bool((np.diff(t_ms) >= 0).all())
t0 = dt.datetime.fromtimestamp(t_ms[0] / 1000, dt.UTC)
t1 = dt.datetime.fromtimestamp(t_ms[-1] / 1000, dt.UTC)
print(f"time span: {t0:%Y-%m-%d %H:%M} .. {t1:%Y-%m-%d %H:%M} UTC")
print(f"time monotonic: {monotonic}")
#--- Structural sanity independent of any price scale, per sqxbars.load()'s own check:
#--- a high below its low means the FIELD ORDER is wrong, which no rescaling could reveal.
raw_open, raw_high, raw_low, raw_close = rows[:, 1], rows[:, 2], rows[:, 3], rows[:, 4]
bad_hilo = int((raw_high < raw_low).sum())
print(f"raw high<low violations: {bad_hilo} of {len(rows):,} "
f"({'OK' if bad_hilo == 0 else 'SUSPECT FIELD ORDER'})")
print(f"raw close range: [{raw_close.min():,} .. {raw_close.max():,}] "
f"(UNSCALED integer units - see this file's docstring for why)")
out_path = os.path.join(SAMPLE_OUT, f"{SYMBOL}_{TIMEFRAME}_raw.npz")
np.savez_compressed(out_path, rows=rows,
columns=np.array(sqxbars.COLS))
print(f"\nsaved raw decoded sample -> {out_path}")
print("NOTE: prices in this file are RAW UNSCALED INTEGERS, not calibrated to a decimal")
print("point. To get real prices, calibrate against a trusted reference series the way")
print("sqxbars.calibrate()/load() do - e.g. sqxbars.load(sym, tf, ref=(times_ms, closes))")
print("with a validated MT5 rate export for the same instrument/timeframe.")
print(f"\n{SYMBOL} {TIMEFRAME}: {len(rows):,} bars decoded, monotonic={monotonic}, "
f"high<low violations={bad_hilo} -> " +
("PASS" if monotonic and bad_hilo == 0 else "FAIL"))
if __name__ == "__main__":
main()