forked from animatedread/Warrior_EA
136 lines
6 KiB
Python
136 lines
6 KiB
Python
"""
| |||
Trade-by-trade reconciliation: WarriorDipZ.ex5 (MT5 tester) vs research/backtest.py.
| |||
| |||
A research result is a claim about the research code until the EA that will
| |||
actually trade reproduces it. This matches each EA trade to the backtest trade
| |||
on the same symbol whose fill bar contains the EA's entry time, then reports
| |||
what agrees, what does not, and why.
| |||
| |||
It also reads the tester report's EQUITY drawdown -- marked to market, with
| |||
concurrent positions counted together -- which the backtest's exit-based
| |||
curve can only bound from below.
| |||
"""
| |||
| |||
from __future__ import annotations
| |||
| |||
import re
| |||
import sys
| |||
import numpy as np
| |||
| |||
sys.path.insert(0, __file__.rsplit("\\", 1)[0] if "\\" in __file__ else ".")
| |||
| |||
from vol_filter_test import collect_filtered # noqa: E402
| |||
| |||
COMMON = r"C:\Users\admin\AppData\Roaming\MetaQuotes\Terminal\Common\Files"
| |||
TERM = r"C:\Users\admin\AppData\Roaming\MetaQuotes\Terminal\10CE948A1DFC9A8C27E56E827008EBD4"
| |||
BAR = np.timedelta64(4 * 3600, "s")
| |||
| |||
| |||
def load_ea(path=rf"{COMMON}\dipz_trades.csv"):
| |||
raw = np.genfromtxt(path, delimiter=",", skip_header=1, dtype=str, encoding="ansi")
| |||
if raw.ndim == 1:
| |||
raw = raw[None, :]
| |||
to_ts = lambda s: np.datetime64(s[:10].replace(".", "-") + "T" + s[11:] + ":00") # noqa: E731
| |||
return [dict(pos=r[0], symbol=r[1], t=to_ts(r[2]), entry=float(r[3]), vol=float(r[4]),
| |||
exit_t=to_ts(r[5]), exit=float(r[6]), net=float(r[7]), reason=r[8])
| |||
for r in raw]
| |||
| |||
| |||
def report_stats(name):
| |||
"""Pull headline numbers out of the tester's HTML report (UTF-16)."""
| |||
path = rf"{TERM}\{name}.htm"
| |||
try:
| |||
txt = open(path, encoding="utf-16").read()
| |||
except (OSError, UnicodeError):
| |||
try:
| |||
txt = open(path, encoding="utf-8", errors="ignore").read()
| |||
except OSError:
| |||
return {}
| |||
txt = re.sub(r"<[^>]+>", "|", txt)
| |||
txt = re.sub(r"\|+", "|", txt)
| |||
out = {}
| |||
for key in ("Total Net Profit", "Profit Factor", "Equity Drawdown Maximal",
| |||
"Balance Drawdown Maximal", "Total Trades", "Expected Payoff",
| |||
"Equity Drawdown Relative"):
| |||
m = re.search(re.escape(key) + r":\|([^|]+)\|", txt)
| |||
if m:
| |||
out[key] = m.group(1).strip()
| |||
return out
| |||
| |||
| |||
def main(symbols, report, lo="2022-01-01", deposit=100000.0, risk=0.0025, gated=True):
| |||
ea = load_ea()
| |||
gate = (lambda p: np.nan_to_num(p, nan=-1) >= 0.50) if gated else (lambda p: np.ones(len(p), bool))
| |||
py = [t for t in collect_filtered(symbols, "PERIOD_H4", gate, lo, None)]
| |||
| |||
# match: same symbol, EA entry inside the python fill bar
| |||
used = set()
| |||
pairs, ea_only = [], []
| |||
for e in ea:
| |||
hit = None
| |||
for k, p in enumerate(py):
| |||
if k in used or p["symbol"] != e["symbol"]:
| |||
continue
| |||
if p["t"] <= e["t"] < p["t"] + BAR:
| |||
hit = k
| |||
break
| |||
if hit is None:
| |||
ea_only.append(e)
| |||
else:
| |||
used.add(hit)
| |||
pairs.append((e, py[hit]))
| |||
py_only = [p for k, p in enumerate(py) if k not in used]
| |||
| |||
print(f"EA trades {len(ea)} backtest trades {len(py)} matched {len(pairs)} "
| |||
f"({len(pairs) / max(len(py), 1):.0%} of backtest) EA-only {len(ea_only)} "
| |||
f"backtest-only {len(py_only)}\n")
| |||
| |||
if pairs:
| |||
er = np.array([(e["exit"] - e["entry"]) / e["entry"] for e, _ in pairs])
| |||
pr = np.array([p["gross"] for _, p in pairs])
| |||
same_exit_bar = np.mean([abs(e["exit_t"] - p["exit_t"]) <= BAR for e, p in pairs])
| |||
rmap = {"stop": "stop", "expert": None}
| |||
agree = np.mean([(e["reason"] == "stop") == (p["reason"] == "stop") for e, p in pairs])
| |||
print(f"matched trades: gross return corr {np.corrcoef(er, pr)[0, 1]:.3f} "
| |||
f"mean |diff| {np.mean(np.abs(er - pr)) * 1e4:.1f} bp "
| |||
f"exit within one bar {same_exit_bar:.0%} stop/non-stop agreement {agree:.0%}")
| |||
print(f" mean gross: EA {er.mean() * 1e4:+.1f} bp backtest {pr.mean() * 1e4:+.1f} bp\n")
| |||
| |||
for tag, rows in (("EA-only", ea_only[:8]), ("backtest-only", py_only[:8])):
| |||
if rows:
| |||
print(f"first {tag}:")
| |||
for r in rows:
| |||
print(f" {r['symbol']:<7} {str(r['t'])[:16]} -> {str(r['exit_t'])[:16]}")
| |||
print()
| |||
| |||
# EA portfolio stats from its own P&L (exit-based, comparable to backtest)
| |||
ea_sorted = sorted(ea, key=lambda e: e["exit_t"])
| |||
eq = deposit + np.cumsum([e["net"] for e in ea_sorted])
| |||
eq = np.concatenate([[deposit], eq])
| |||
dd = ((np.maximum.accumulate(eq) - eq) / np.maximum.accumulate(eq)).max()
| |||
years = (ea_sorted[-1]["exit_t"] - ea_sorted[0]["t"]) / np.timedelta64(365, "D")
| |||
tot = eq[-1] / deposit - 1
| |||
print(f"EA portfolio: {len(ea)} trades, {len(ea) / (years * 12):.1f}/mo, total {tot:+.1%}, "
| |||
f"CAGR {(1 + tot) ** (1 / years) - 1:+.1%}, exit-based maxDD {dd:.1%}, ret/DD {tot / dd:.2f}")
| |||
for split in ("2024-01-01",):
| |||
for nm, sel in (("IS ", [e for e in ea_sorted if e["t"] < np.datetime64(split)]),
| |||
("OOS", [e for e in ea_sorted if e["t"] >= np.datetime64(split)])):
| |||
if not sel:
| |||
continue
| |||
q = deposit + np.concatenate([[0], np.cumsum([e["net"] for e in sel])])
| |||
d_ = ((np.maximum.accumulate(q) - q) / np.maximum.accumulate(q)).max()
| |||
y_ = (sel[-1]["exit_t"] - sel[0]["t"]) / np.timedelta64(365, "D")
| |||
t_ = q[-1] / deposit - 1
| |||
print(f" {nm} {len(sel):>4} trades {len(sel) / (y_ * 12):>5.1f}/mo total {t_:+.1%} "
| |||
f"maxDD {d_:.1%} ret/DD {t_ / d_ if d_ > 0 else float('nan'):.2f}")
| |||
rep = report_stats(report)
| |||
if rep:
| |||
print("\ntester report (EQUITY drawdown is marked to market - the real one):")
| |||
for k, v in rep.items():
| |||
print(f" {k:<28} {v}")
| |||
| |||
| |||
if __name__ == "__main__":
| |||
syms = sys.argv[1].split(",") if len(sys.argv) > 1 else ["SP500", "NAS100", "US30", "DAX40"]
| |||
rep = sys.argv[2] if len(sys.argv) > 2 else "claude_dipz_4"
| |||
main(syms, rep)
|