"""Tick Forge audit runner and report (free companion script, MIT). The Tick Forge window builds an audit set of custom symbols and writes a manifest (MQL5\\Files\\TickForge\\audit_.json). MQL5 programs cannot start the Strategy Tester, so this script does that part on a machine YOU choose: it runs one Expert over every symbol of the set, collects the tester reports and writes a plain comparison. run tf_audit.py run --manifest M.json --expert Folder\\Expert --terminal T\\terminal64.exe --data [--set my.set] [--period M5] [--model 1] [--deposit 10000] [--leverage 100] [--out audit_out] report tf_audit.py report --manifest M.json --reports DIR (re-read reports already collected) SAFETY * Use a TESTING terminal install, never a terminal that is logged in to a live-money account. The script refuses to start if that terminal is already running (it never kills anything). * --terminal and --data are mandatory: nothing is guessed. * The Expert runs only in the Strategy Tester; no orders reach any account. Models: 0 every tick, 1 1-minute OHLC, 2 open prices only, 4 every tick based on real ticks. Use 4 for symbols generated as 'real ticks', 1 for 'M1 bars only'. """ import argparse, datetime as dt, html, json, os, re, shutil, subprocess, sys, time from pathlib import Path # --------------------------------------------------------------------- helpers def w16(path, text): Path(path).write_bytes(b"\xff\xfe" + text.replace("\n", "\r\n").encode("utf-16-le")) def running(exe): ps = ("Get-Process terminal64 -EA SilentlyContinue | Where-Object {$_.Path -eq '%s'} | " "Select-Object -First 1 -ExpandProperty Id" % exe.replace("'", "''")) out = subprocess.run(["powershell", "-NoProfile", "-Command", ps], capture_output=True, text=True).stdout.strip() return bool(out) def date_add(s, days=0, secs=0): d = dt.datetime.strptime(s, "%Y.%m.%d") + dt.timedelta(days=days, seconds=secs) return d.strftime("%Y.%m.%d") # --------------------------------------------------------------------- report parsing METRICS = (("net", "Total Net Profit:"), ("pf", "Profit Factor:"), ("trades", "Total Trades:"), ("ddrel", "Equity Drawdown Relative:"), ("dd", "Equity Drawdown Maximal:"), ("rf", "Recovery Factor:"), ("win", "Profit Trades (% of total):"), ("avgw", "Average profit trade:"), ("avgl", "Average loss trade:"), ("quality", "History Quality:")) def num(s): m = re.search(r"-?\d[\d\s]*\.?\d*", s.replace("\xa0", " ")) return float(m.group(0).replace(" ", "")) if m else None def parse_report(path): raw = Path(path).read_bytes() txt = raw.decode("utf-16", "ignore") if raw[:2] in (b"\xff\xfe", b"\xfe\xff") else raw.decode("utf-8", "ignore") flat = re.sub(r"<[^>]+>", "|", txt) flat = html.unescape(re.sub(r"[\|\s]*\|[\|\s]*", "|", flat)) out = {} for key, label in METRICS: m = re.search(re.escape(label) + r"\|([^|]*)", flat) out[key] = m.group(1).strip() if m else "" ent = [] for m in re.finditer(r"(\d{4}\.\d\d\.\d\d \d\d:\d\d:\d\d)\|\d+\|[^|]*\|(buy|sell)\|in\|", flat): ent.append((m.group(1), m.group(2))) out["entries"] = ent return out # --------------------------------------------------------------------- comparison def to_dt(s): return dt.datetime.strptime(s, "%Y.%m.%d %H:%M:%S") def compare(manifest, runs, cap=None): """runs: {symbol: parsed report}. Returns rows + flags.""" syms = manifest["symbols"] ctrl = next((s for s in syms if s["offset_sec"] == 0 and s["expr"].replace(" ", "") == "p"), None) rows, flags = [], [] c = runs.get(ctrl["name"]) if ctrl else None cset = {to_dt(t) for t, _ in c["entries"]} if c else set() cap = cap or dt.datetime.now() end0 = dt.datetime.strptime(manifest["to"], "%Y.%m.%d") + dt.timedelta(days=1) for s in syms: r = runs.get(s["name"]) row = {"name": s["name"], "label": s["label"], "offset": s["offset_sec"], "expr": s["expr"], "ok": r is not None} if r: row.update({k: r[k] for k, _ in METRICS}) row["entries"] = len(r["entries"]) off = dt.timedelta(seconds=s["offset_sec"]) # the tester cannot use data dated after today: compare only the part of the control that fits row["truncated"] = (end0 + off) > cap sub = {t for t in cset if t + off <= cap} if row["truncated"] else cset row["ctrl_n"] = len(sub) back = {to_dt(t) - off for t, _ in r["entries"]} row["match"] = (len(back & sub) / len(sub)) if sub else None row["missing"] = sorted(sub - back) rows.append(row) if not c: flags.append("No control run found: compare against the control first, nothing below is reliable without it.") return rows, flags ctr = num(c["trades"]) or 0 cnet = num(c["net"]) or 0.0 if ctr < 20: flags.append("The control run has only %d trades. A few trades cannot separate logic from luck: use a longer range." % ctr) for row in rows: if not row["ok"] or row["name"] == ctrl["name"]: continue tr = num(row["trades"]) or 0 net = num(row["net"]) or 0.0 if row["offset"] and row["offset"] % (7 * 86400) == 0 and row["expr"].replace(" ", "") == "p": cn = row["ctrl_n"] or 0 drop = (cn - tr) / cn * 100 if cn else 0 if row.get("truncated"): flags.append("%s: the shifted data runs past today, so only the first part could be tested; compared on that overlap only (%d of %d control entries)." % (row["name"], cn, ctr)) if abs(drop) >= 25: flags.append("%s: trade count moved %+.0f%% under a pure date shift of whole weeks (weekday and time of day unchanged)." % (row["name"], -drop)) if row["match"] is not None and row["match"] < 0.7: flags.append("%s: only %.0f%% of the control entries reappear at the shifted time. Look at the missing entries below: do they sit on particular dates?" % (row["name"], row["match"] * 100)) if cnet > 0 and net <= 0: flags.append("%s: the control made money, this shifted run did not (net %.2f vs %.2f)." % (row["name"], net, cnet)) elif row["expr"].replace(" ", "") in ("p*0.5", "p*2"): if cnet > 0 and net <= 0: flags.append("%s: profitable on the control, not after rescaling the prices. Check for hard-coded price levels or distances." % row["name"]) if not flags: flags.append("No rule-of-thumb flag fired. That means the Expert behaved similarly on these runs. It is not a guarantee about anything else.") return rows, flags CSS = ("body{font:15px/1.55 Arial,Helvetica,sans-serif;max-width:900px;margin:0 auto;padding:16px;color:#222}" "table{border-collapse:collapse;width:100%;margin:8px 0 16px}td,th{border:1px solid #bbb;padding:5px 8px;text-align:right}" "th{background:#f1f1f1}td:first-child,th:first-child{text-align:left}.col{max-width:680px}" ".flag{border-left:4px solid #b36b00;background:#fff8ec;padding:6px 12px;margin:6px 0}") def write_html(path, manifest, rows, flags, expert): def f(v): return html.escape(str(v)) if v not in (None, "") else "-" h = ["Tick Forge audit report" % CSS, "

Tick Forge audit report

", "

Expert: %s. Source symbol: %s, %s to %s. Data: %s.

" % ( f(expert), f(manifest.get("source")), f(manifest.get("from")), f(manifest.get("to")), "real ticks" if manifest.get("mode") == 0 else "M1 bars"), "

Every symbol below is the broker's own history with only the date or the price level changed. " "The control is the unchanged history. Flags are rules of thumb, not statistics, and a flag is a question, not a verdict.

", "

Flags

"] h += ["
%s
" % html.escape(x) for x in flags] h.append("

Results

" "") for r in rows: if not r["ok"]: h.append("" % f(r["name"])) continue m = ("%.0f%%" % (r["match"] * 100)) if r.get("match") is not None else "-" h.append("" % ( f(r["name"]), f(r["label"]), f(r["trades"]), f(r["net"]), f(r["pf"]), f(r["ddrel"]), m)) h.append("
SymbolChangeTradesNetPFMax equity DDEntries reproduced
%sno report
%s%s%s%s%s%s%s

Entries reproduced = share of the control run's entries that reappear at the same moment after undoing the time shift. " "It is only meaningful for the calendar-shift rows.

") for r in rows: if r["ok"] and r["offset"] and r.get("missing"): days = sorted({d.strftime("%Y.%m.%d %a") for d in r["missing"]}) h.append("

%s: control entries that did not reappear (%d)

%s

" % ( f(r["name"]), len(r["missing"]), html.escape(", ".join(days[:40]) + (" ..." if len(days) > 40 else "")))) h.append("

What this does not show

A pass means the Expert did not depend on what was changed. It does not prove " "the Expert is safe or profitable. A failure points at something to investigate (dates, weekdays, price levels). " "Backtests on altered data are stress tests, not forecasts or trading results.

") h.append("

Generated by tf_audit.py (Tick Forge, MIT).

") Path(path).write_text("\n".join(h), encoding="utf-8") # --------------------------------------------------------------------- run def run(a): manifest = json.loads(Path(a.manifest).read_text(encoding="utf-8")) exe = a.terminal if running(exe): sys.exit("That terminal is running. Close it first (this script never closes anything for you).") data = Path(a.data) out = Path(a.out) out.mkdir(parents=True, exist_ok=True) if a.set: prof = data / "MQL5" / "Profiles" / "Tester" prof.mkdir(parents=True, exist_ok=True) shutil.copy2(a.set, prof / Path(a.set).name) reports = {} for i, s in enumerate(manifest["symbols"]): tag = "tf_audit_%s" % s["name"] shift = int(s["offset_sec"]) d1 = date_add(manifest["from"], secs=shift) d2 = date_add(manifest["to"], days=1, secs=shift) ini = out / (tag + ".ini") lines = ["[Tester]", "Expert=%s" % a.expert, "Symbol=%s" % s["name"], "Period=%s" % a.period, "Model=%s" % a.model, "FromDate=%s" % d1, "ToDate=%s" % d2, "ForwardMode=0", "Deposit=%s" % a.deposit, "Currency=%s" % a.currency, "Leverage=%s" % a.leverage, "Optimization=0", "Visual=0", "Report=%s" % tag, "ReplaceReport=1", "ShutdownTerminal=1"] if a.set: lines.insert(2, "ExpertParameters=%s" % Path(a.set).name) w16(ini, "\n".join(lines) + "\n") rep = data / (tag + ".htm") if rep.exists(): rep.unlink() print("[%d/%d] %s (%s)" % (i + 1, len(manifest["symbols"]), s["name"], s["label"]), flush=True) p = subprocess.Popen([exe, "/config:%s" % ini]) t0 = time.time() while p.poll() is None and time.time() - t0 < a.timeout: time.sleep(3) if p.poll() is None: print(" timeout after %d s; leaving the terminal for you to inspect" % a.timeout) break time.sleep(1) if rep.exists(): shutil.copy2(rep, out / rep.name) reports[s["name"]] = parse_report(rep) r = reports[s["name"]] print(" trades %s net %s PF %s" % (r["trades"], r["net"], r["pf"])) else: print(" no report written (is the Expert name right, and the symbol present in Market Watch?)") finish(manifest, reports, out, a.expert) def finish(manifest, reports, out, expert): rows, flags = compare(manifest, reports) path = Path(out) / "tick_forge_audit_report.html" write_html(path, manifest, rows, flags, expert) print("\nFlags:") for x in flags: print(" -", x) print("Report:", path) def report(a): manifest = json.loads(Path(a.manifest).read_text(encoding="utf-8")) d = Path(a.reports) runs = {} for s in manifest["symbols"]: f = d / ("tf_audit_%s.htm" % s["name"]) if f.exists(): runs[s["name"]] = parse_report(f) finish(manifest, runs, d, a.expert or "(see reports)") def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) sub = ap.add_subparsers(dest="cmd", required=True) r = sub.add_parser("run") r.add_argument("--manifest", required=True) r.add_argument("--expert", required=True, help="path under MQL5\\Experts, e.g. MyFolder\\MyEA") r.add_argument("--terminal", required=True, help="full path of a TESTING terminal64.exe") r.add_argument("--data", required=True, help="that terminal's data folder") r.add_argument("--set", help="Expert settings file (.set); copied into Profiles\\Tester") r.add_argument("--period", default="M5") r.add_argument("--model", default="1") r.add_argument("--deposit", default="10000") r.add_argument("--currency", default="USD") r.add_argument("--leverage", default="100") r.add_argument("--timeout", type=int, default=3600) r.add_argument("--out", default="audit_out") p = sub.add_parser("report") p.add_argument("--manifest", required=True) p.add_argument("--reports", required=True) p.add_argument("--expert") a = ap.parse_args() run(a) if a.cmd == "run" else report(a) if __name__ == "__main__": main()