| # SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
|
| """EA Health Monitor MT4 against tests/reference.py, from the snapshots of tests/mt4/HealthTrader.mq4.
|
|
|
| python3 tests/mt4/check_mt4.py <folder with the snapshot files>
|
|
|
| For every snapshot: the raw MT4 orders are turned into deals HERE, independently of HealthCore.mqh's EHLoadHistory
|
| (each buy/sell order an opening deal at its open time and lots, a closed one also a closing deal with its profit, swap
|
| and commission; the parts of a partly closed order, linked by "from #<ticket>" comments, are one position; time order,
|
| opening deals first in a second, then reading order), then reference.compute gives the statistics per magic for the
|
| whole history. Compared with _stats.csv, the test EA's own book (HealthLive.mqh EHRefresh): every statistic, open trades,
|
| floating (profit + swap + commission of the open orders) and the row name.
|
| The MT4 tester gives an indicator run through iCustom no chart, so the panel itself cannot be read there; the book is
|
| what the panel shows."""
|
| import csv
|
| import re
|
| import sys
|
| from pathlib import Path
|
|
|
| sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| from reference import compute # noqa: E402
|
|
|
| FL = ("gp", "gl", "net", "curve", "peak", "maxdd", "today")
|
|
|
|
|
| def close(a, b):
|
| return abs(a - b) <= 1e-9 * max(1.0, abs(a), abs(b))
|
|
|
|
|
| def clean(c):
|
| if c.startswith(("from #", "to #", "partial close", "close hedge")):
|
| return ""
|
| return c.split("[")[0].rstrip()
|
|
|
|
|
| def deals_of(orders):
|
| """orders: rows of _orders.csv (file order). Returns deals and the comment of each opening deal (EA orders)."""
|
| od = [o for o in orders if int(o["type"]) <= 1]
|
| # "from #43", also "from #43[sl]" when the rest was closed by the server: the leading digits (as StringToInteger)
|
| par = {int(o["ticket"]): (int(re.match(r"\d*", o["comment"][6:])[0] or 0) if o["comment"].startswith("from #") else 0)
|
| for o in od}
|
| out = []
|
| for o in od:
|
| r, p = int(o["ticket"]), par[int(o["ticket"])]
|
| while p:
|
| r, p = p, par.get(p, 0)
|
| mg, lots = int(o["magic"]), float(o["lots"])
|
| out.append((int(o["open_time"]), 0, len(out), dict(time=int(o["open_time"]), pos=r, magic=mg, entry="in", volume=lots,
|
| profit=0.0, swap=0.0, commission=0.0, fee=0.0), clean(o["comment"]) if mg else ""))
|
| if int(o["close_time"]):
|
| out.append((int(o["close_time"]), 1, len(out), dict(time=int(o["close_time"]), pos=r, magic=mg, entry="out",
|
| volume=lots, profit=float(o["profit"]), swap=float(o["swap"]), commission=float(o["commission"]),
|
| fee=0.0), ""))
|
| out.sort(key=lambda x: (x[0], x[1], x[2]))
|
| return [x[3] for x in out], [x[4] for x in out]
|
|
|
|
|
| def check_snapshot(base: Path, day):
|
| orders = list(csv.DictReader(open(f"{base}_orders.csv")))
|
| deals, names = deals_of(orders)
|
| opened = {}
|
| for o in orders:
|
| if o["close_time"] == "0" and int(o["type"]) <= 1:
|
| x = opened.setdefault(int(o["magic"]), [0, 0.0])
|
| x[0] += 1
|
| x[1] += float(o["profit"]) + float(o["swap"]) + float(o["commission"])
|
| first = {}
|
| for d, nm in zip(deals, names):
|
| if d["entry"] == "in" and d["magic"] and nm and d["magic"] not in first:
|
| first[d["magic"]] = nm
|
| mt = {}
|
| for r in csv.DictReader(open(f"{base}_stats.csv")):
|
| mt.setdefault(r["book"], []).append(r)
|
| probs = []
|
| books = {}
|
| for book in ("all",):
|
| ref = compute(deals, day_start=day)
|
| for m, s in ref.items():
|
| s["open"], s["floating"] = opened.get(m, [0, 0.0])
|
| nm = first.get(m, "")
|
| s["name"] = "Manual" if m == 0 else (f"{nm[:14]} #{m}" if nm else f"#{m}")
|
| books[book] = ref
|
| rows = mt.get(book, [])
|
| if [int(r["magic"]) for r in rows] != list(ref):
|
| probs.append(f"{book}: magics/order {[r['magic'] for r in rows]} vs {list(ref)}")
|
| continue
|
| for r in rows:
|
| s = ref[int(r["magic"])]
|
| for c in ("n", "w", "l", "deals", "last", "open"):
|
| if int(r[c]) != int(s[c]):
|
| probs.append(f"{book} {r['magic']}.{c} {r[c]} vs {s[c]}")
|
| for c in FL + ("floating",):
|
| if not close(float(r[c]), s[c]):
|
| probs.append(f"{book} {r['magic']}.{c} {r[c]} vs {s[c]}")
|
| if r["name"] != s["name"].replace(",", ";"):
|
| probs.append(f"{book} {r['magic']}.name '{r['name']}' vs '{s['name']}'")
|
| ref = books["all"]
|
| trades = sum(s["n"] for s in ref.values())
|
| return probs, len(orders), trades, len(ref)
|
|
|
|
|
| def main(root: Path) -> bool:
|
| allok, snaps, trades_last = True, 0, 0
|
| for sf in sorted(root.glob("*_snaps.csv")):
|
| stem = sf.name[:-len("_snaps.csv")]
|
| bad_here, last = 0, None
|
| for r in csv.DictReader(open(sf)):
|
| probs, no, nt, nm = check_snapshot(root / f"{stem}_{r['k']}", int(r["day"]))
|
| snaps += 1
|
| bad_here += bool(probs)
|
| last = (no, nt, nm)
|
| if probs:
|
| print(f"FAIL {stem} snapshot {r['k']}: " + "; ".join(probs[:5]))
|
| allok &= bad_here == 0
|
| if last:
|
| trades_last += last[1]
|
| print(("PASS " if not bad_here else "FAIL ") + f"{stem:28} snapshots ok {snaps_of(sf) - bad_here}/{snaps_of(sf)} "
|
| f"final: {last[0]} orders, {last[1]} trades, {last[2]} rows")
|
| print(f"\n{'ALL PASS' if allok and snaps else 'FAILURES'} ({snaps} snapshots, "
|
| f"{trades_last} trades in the final snapshots)")
|
| return bool(allok and snaps)
|
|
|
|
|
| def snaps_of(sf):
|
| return sum(1 for _ in csv.DictReader(open(sf)))
|
|
|
|
|
| if __name__ == "__main__":
|
| sys.exit(0 if main(Path(sys.argv[1])) else 1)
|