| # SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
|
| """Python twin of mql5/Include/EAHealth/HealthCore.mqh: per-magic statistics from a deal history (docs/SPEC.md),
|
| operation for operation, so MT5's output can be compared (tests/check_core.py).
|
|
|
| from reference import compute, synth
|
| deals = synth(seed=1) # random hedging/netting-like history with partial and manual closes
|
| stats = compute(deals, day_start=0)
|
| Deal: dict(time, pos, magic, entry 'in'|'out'|'inout'|'outby', volume, profit, swap, commission, fee)."""
|
| import random
|
|
|
| EPS = 1e-8
|
|
|
|
|
| def compute(deals, day_start=0):
|
| pos = {} # pos id -> [magic, open volume, net so far]
|
| st = {}
|
|
|
| def S(m):
|
| if m not in st:
|
| st[m] = dict(n=0, w=0, l=0, gp=0.0, gl=0.0, net=0.0, curve=0.0, peak=0.0, maxdd=0.0, today=0.0,
|
| last=0, deals=0)
|
| return st[m]
|
|
|
| for d in deals:
|
| pnl = d["profit"] + d["swap"] + d["commission"] + d["fee"]
|
| p = pos.get(d["pos"])
|
| if d["entry"] == "in":
|
| if p is None:
|
| p = pos[d["pos"]] = [d["magic"], 0.0, 0.0]
|
| p[1] += d["volume"]
|
| elif p is None: # opening deal not in the history: use this deal's magic
|
| p = pos[d["pos"]] = [d["magic"], d["volume"], 0.0]
|
| m = p[0]
|
| s = S(m)
|
| p[2] += pnl
|
| s["curve"] += pnl
|
| s["peak"] = max(s["peak"], s["curve"])
|
| s["maxdd"] = max(s["maxdd"], s["peak"] - s["curve"])
|
| s["net"] += pnl
|
| s["deals"] += 1
|
| if d["time"] >= day_start:
|
| s["today"] += pnl
|
| s["last"] = max(s["last"], d["time"])
|
| closed = False
|
| if d["entry"] in ("out", "outby"):
|
| p[1] -= d["volume"]
|
| closed = p[1] <= EPS
|
| elif d["entry"] == "inout":
|
| rest = d["volume"] - p[1] # the part beyond the open volume opens the other way
|
| closed = True
|
| if closed:
|
| s["n"] += 1
|
| if p[2] > 0:
|
| s["w"] += 1
|
| s["gp"] += p[2]
|
| elif p[2] < 0:
|
| s["l"] += 1
|
| s["gl"] += -p[2]
|
| if d["entry"] == "inout":
|
| p[1], p[2] = max(rest, 0.0), 0.0
|
| else:
|
| p[1], p[2] = 0.0, 0.0
|
| if p[1] <= EPS:
|
| del pos[d["pos"]] # fully closed: forgotten (as HealthCore.mqh)
|
| return st
|
|
|
|
|
| def synth(seed=1, n_pos=400, magics=(0, 11, 22, 771500, 123456)):
|
| """Deals in time order: each position opens (with commission), may be closed in 1-3 parts, sometimes by hand
|
| (out deal magic 0), sometimes reversed (inout), some stay open."""
|
| rnd = random.Random(seed)
|
| ev = []
|
| t0 = 1_700_000_000
|
| for i in range(n_pos):
|
| m = rnd.choice(magics)
|
| t = t0 + rnd.randint(0, 3_000_000)
|
| vol = round(rnd.choice([0.01, 0.02, 0.05, 0.1, 0.3, 1.0]), 2)
|
| ev.append(dict(time=t, pos=1000 + i, magic=m, entry="in", volume=vol, profit=0.0, swap=0.0,
|
| commission=-round(vol * 3.5, 2), fee=0.0))
|
| if rnd.random() < 0.1:
|
| continue # still open
|
| parts = rnd.choice([1, 1, 1, 2, 3]) if vol >= 0.03 else 1
|
| left = vol
|
| for k in range(parts):
|
| t += rnd.randint(60, 200_000)
|
| v = left if k == parts - 1 else round(left / 2, 2)
|
| left = round(left - v, 2)
|
| kind = "out"
|
| if k == parts - 1 and rnd.random() < 0.05:
|
| kind = "inout"
|
| v = round(v + rnd.choice([0.01, 0.1]), 2)
|
| elif rnd.random() < 0.03:
|
| kind = "outby"
|
| ev.append(dict(time=t, pos=1000 + i, magic=0 if (m and rnd.random() < 0.15) else m, entry=kind,
|
| volume=v, profit=round(rnd.gauss(0.5, 30) * v * 10, 2),
|
| swap=round(rnd.gauss(0, 1), 2) if rnd.random() < 0.4 else 0.0,
|
| commission=-round(v * 3.5, 2), fee=-0.05 if rnd.random() < 0.05 else 0.0))
|
| ev.sort(key=lambda d: (d["time"], d["pos"]))
|
| return ev
|