Warrior_EA/research/meta_pool.py
AnimateDread 932c94e890 feat(research): offline meta-pool pipeline - loader, EA-mirrored splits, MLP, cov x (p-BE) eval
Loads the EA's MetaExport .f32 datasets (UTF-16 sidecars), applies the EA's
own discipline offline: chronological 55/15/30 split with horizon-length
purges, operating point fitted on the calibration slice only via
coverage x (precision - BE) with the 25% floor, test slice touched once,
deployability at the 2-sigma edge floor. Small leaky-ReLU MLP + Adam in
numpy; `stats` / `eval <tag>` / `pool` commands.

First run on XAUUSD_16388 validated the plumbing and exposed the data:
the 2.5h gold tester run only covered 2004-07..2006-10 (3,214 candidates)
because gold tick volume is huge - and corpus builds do not need ticks at
all (journaling is bar-open-keyed, labels come from bar history later), so
"Open prices only" modeling builds the same corpus ~100x faster.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:09:14 -04:00

242 lines
10 KiB
Python

"""Offline meta-labeling pipeline over the EA's exported candidate datasets.
Input: Common\\Files\\Warrior_EA\\MetaExport\\<SYM>_<PERIOD>.f32 (+ .meta.csv sidecar),
written by CSignalMETA::ExportMetaDataset - one row per resolved, labeled candidate:
[barTime int64][family int32][pattern int32][side int32][won int32][width float32s]
byte-equivalent to what the EA's pass 2 trains on (same window builder, descriptor, labels).
Discipline mirrors the EA exactly:
- chronological split per symbol: oldest 55% train | purge | 15% calibration | purge | 30% test,
purges = horizonBars * period (triple-barrier labels look that far forward -> leakage band);
- operating point fitted on the CALIBRATION slice only (coverage x (p - BE), the EA's
FitDirConfThreshold objective, with the EA's 25%-coverage floor);
- the TEST slice is touched once, by the fitted threshold.
Usage:
py -3 research/meta_pool.py stats # per-file inventory + base rates
py -3 research/meta_pool.py eval XAUUSD_16388 # single-symbol train+eval
py -3 research/meta_pool.py pool # pooled train, per-symbol eval
"""
import csv
import glob
import os
import sys
import numpy as np
EXPORT_DIR = r"C:\Users\admin\AppData\Roaming\MetaQuotes\Terminal\Common\Files\Warrior_EA\MetaExport"
PERIOD_SECONDS = {16385: 3600, 16386: 7200, 16387: 10800, 16388: 14400, 16390: 21600,
16392: 28800, 16396: 43200, 16408: 86400}
MIN_COVERAGE_FRACTION = 0.25 # the EA's MIN_COVERAGE_FRACTION_OF_BASE_RATE against a 100% base
EDGE_MIN_SIGMAS = 2.0 # the EA's deployability floor
SEED = 20260813
def load(tag):
meta_path = os.path.join(EXPORT_DIR, tag + ".meta.csv")
# MQL5's FileOpen(FILE_CSV) without FILE_ANSI writes UTF-16LE
with open(meta_path, encoding="utf-16") as f:
r = list(csv.DictReader(f))[0]
width = int(r["width"])
dt = np.dtype([("t", "<i8"), ("fam", "<i4"), ("pat", "<i4"), ("side", "<i4"),
("won", "<i4"), ("x", "<f4", (width,))])
raw = np.fromfile(os.path.join(EXPORT_DIR, tag + ".f32"), dtype=dt)
order = np.argsort(raw["t"], kind="stable") # oldest first; export walks candidate ids
raw = raw[order]
meta = {k: (float(v) if k not in ("symbol",) else v) for k, v in r.items()}
meta["period_sec"] = PERIOD_SECONDS[int(float(r["period"]))]
return raw, meta
def chrono_split(rows, meta):
"""Return boolean masks (train, calib, test) with horizon purges between slices."""
t = rows["t"]
purge = int(float(meta["horizonBars"])) * meta["period_sec"]
n = len(t)
i_train_end = int(n * 0.55)
i_calib_end = int(n * 0.70)
t_train_hi = t[i_train_end - 1]
t_calib_lo = t_train_hi + purge
t_calib_hi = t[min(i_calib_end, n - 1)]
t_test_lo = t_calib_hi + purge
train = t <= t_train_hi
calib = (t >= t_calib_lo) & (t <= t_calib_hi)
test = t >= t_test_lo
return train, calib, test
class MLP:
"""width -> h1 -> h2 -> 2 softmax, input standardization + Adam + CE. Mirrors the EA's
head in spirit (dense+norm+bounded scale); small enough to train in seconds."""
def __init__(self, width, h1=32, h2=16, seed=SEED):
rng = np.random.default_rng(seed)
s1 = np.sqrt(2.0 / width)
s2 = np.sqrt(2.0 / h1)
s3 = np.sqrt(2.0 / h2)
self.W1 = rng.normal(0, s1, (width, h1)); self.b1 = np.zeros(h1)
self.W2 = rng.normal(0, s2, (h1, h2)); self.b2 = np.zeros(h2)
self.W3 = rng.normal(0, s3, (h2, 2)); self.b3 = np.zeros(2)
self.mu = None
self.sd = None
def _fwd(self, X):
Z1 = X @ self.W1 + self.b1
A1 = np.maximum(Z1, 0.01 * Z1)
Z2 = A1 @ self.W2 + self.b2
A2 = np.maximum(Z2, 0.01 * Z2)
Z3 = A2 @ self.W3 + self.b3
Z3 = Z3 - Z3.max(axis=1, keepdims=True)
P = np.exp(Z3)
P /= P.sum(axis=1, keepdims=True)
return Z1, A1, Z2, A2, P
def fit(self, X, y, epochs=30, batch=256, lr=1e-3, l2=3e-3, verbose=False):
self.mu = X.mean(axis=0)
self.sd = X.std(axis=0) + 1e-8
Xs = (X - self.mu) / self.sd
Y = np.zeros((len(y), 2)); Y[np.arange(len(y)), 1 - y] = 0.0 # placeholder
Y = np.stack([y, 1 - y], axis=1).astype(np.float64) # slot0 = P(win)
rng = np.random.default_rng(SEED)
params = [self.W1, self.b1, self.W2, self.b2, self.W3, self.b3]
m = [np.zeros_like(p) for p in params]
v = [np.zeros_like(p) for p in params]
b1m, b2m, eps, step = 0.9, 0.999, 1e-8, 0
for ep in range(epochs):
idx = rng.permutation(len(Xs))
for lo in range(0, len(idx), batch):
sel = idx[lo:lo + batch]
Xb, Yb = Xs[sel], Y[sel]
Z1, A1, Z2, A2, P = self._fwd(Xb)
dZ3 = (P - Yb) / len(sel)
dW3 = A2.T @ dZ3 + l2 * self.W3; db3 = dZ3.sum(0)
dA2 = dZ3 @ self.W3.T
dZ2 = dA2 * np.where(Z2 > 0, 1.0, 0.01)
dW2 = A1.T @ dZ2 + l2 * self.W2; db2 = dZ2.sum(0)
dA1 = dZ2 @ self.W2.T
dZ1 = dA1 * np.where(Z1 > 0, 1.0, 0.01)
dW1 = Xb.T @ dZ1 + l2 * self.W1; db1 = dZ1.sum(0)
grads = [dW1, db1, dW2, db2, dW3, db3]
step += 1
for p, g, mi, vi in zip(params, grads, m, v):
mi *= b1m; mi += (1 - b1m) * g
vi *= b2m; vi += (1 - b2m) * g * g
mh = mi / (1 - b1m ** step)
vh = vi / (1 - b2m ** step)
p -= lr * mh / (np.sqrt(vh) + eps)
if verbose and ep % 10 == 9:
_, _, _, _, P = self._fwd(Xs)
ce = -np.mean(np.log(np.clip(P[np.arange(len(Y)), (1 - y)], 1e-9, 1)))
print(f" epoch {ep+1}: train CE {ce:.4f}")
return self
def p_win(self, X):
Xs = (X - self.mu) / self.sd
return self._fwd(Xs)[4][:, 0]
def fit_threshold(p, won, be_pct):
"""The EA's operating point: maximize coverage x (precision - BE) over the calib slice,
subject to the 25% coverage floor; ties to the lower threshold."""
be = be_pct / 100.0
best = (-np.inf, 0.0)
for thr in np.linspace(0.0, 0.98, 50):
sel = p >= thr
cov = sel.mean()
if cov < MIN_COVERAGE_FRACTION or sel.sum() == 0:
continue
prec = won[sel].mean()
score = cov * (prec - be)
if score > best[0]:
best = (score, thr)
return best[1]
def evaluate(tag, p_test, rows_test, meta, label=""):
won = rows_test["won"].astype(np.float64)
be = float(meta["breakEvenPct"])
base = 100.0 * won.mean()
out = [f"{tag}{label}: test n={len(won)}, base win {base:.1f}% vs BE {be:.1f}% (gap {base-be:+.1f}pp)"]
for thr_label, thr in [("argmax(0.5)", 0.5), ("fitted", None)]:
t = thr if thr is not None else evaluate.fitted_thr
sel = p_test >= t
if sel.sum() == 0:
out.append(f" {thr_label:<12} thr {t:.2f}: no trades")
continue
cov = 100.0 * sel.mean()
prec = 100.0 * won[sel].mean()
n = int(sel.sum())
se = 100.0 * np.sqrt((base / 100) * (1 - base / 100) / n)
score = cov * (prec - be) / 100.0
verdict = "DEPLOYABLE" if (cov >= 25.0 and prec - base > EDGE_MIN_SIGMAS * se
and prec > be) else ""
out.append(f" {thr_label:<12} thr {t:.2f}: trades {n} ({cov:.1f}%) win {prec:.1f}% | "
f"cov x (p-BE) = {score:+.2f} | skill vs base {prec-base:+.1f}pp "
f"(2 sigma = {EDGE_MIN_SIGMAS*se:.1f}pp) {verdict}")
print("\n".join(out))
def tags_on_disk():
return sorted(os.path.basename(f)[:-4] for f in glob.glob(os.path.join(EXPORT_DIR, "*.f32")))
def cmd_stats():
for tag in tags_on_disk():
rows, meta = load(tag)
t0 = np.datetime64(int(rows["t"].min()), "s")
t1 = np.datetime64(int(rows["t"].max()), "s")
won = rows["won"].astype(float)
long_mask = rows["side"] > 0
print(f"{tag}: {len(rows)} rows {t0}..{t1} | base {100*won.mean():.1f}% "
f"(long {100*won[long_mask].mean():.1f}% short {100*won[~long_mask].mean():.1f}%) "
f"vs BE {meta['breakEvenPct']:.1f}% | width {int(meta['width'])} "
f"geom {meta['slMult']:.2f}/{meta['tpMult']:.2f}")
def run_eval(train_sets, eval_tags, label):
Xs, ys = [], []
for rows, meta, mask in train_sets:
Xs.append(rows["x"][mask].astype(np.float64))
ys.append(rows["won"][mask])
X = np.concatenate(Xs)
y = np.concatenate(ys)
print(f"[{label}] training on {len(X)} rows from {len(train_sets)} file(s)...")
net = MLP(X.shape[1]).fit(X, y, verbose=True)
for tag, rows, meta, calib, test in eval_tags:
p_cal = net.p_win(rows["x"][calib].astype(np.float64))
evaluate.fitted_thr = fit_threshold(p_cal, rows["won"][calib].astype(float), float(meta["breakEvenPct"]))
p_test = net.p_win(rows["x"][test].astype(np.float64))
evaluate(tag, p_test, rows[test], meta, label=f" [{label}]")
def main():
cmd = sys.argv[1] if len(sys.argv) > 1 else "stats"
if cmd == "stats":
cmd_stats()
return
if cmd == "eval":
tag = sys.argv[2]
rows, meta = load(tag)
train, calib, test = chrono_split(rows, meta)
print(f"{tag}: split train {train.sum()} / calib {calib.sum()} / test {test.sum()}")
run_eval([(rows, meta, train)], [(tag, rows, meta, calib, test)], "self")
return
if cmd == "pool":
data = {}
for tag in tags_on_disk():
rows, meta = load(tag)
if int(meta["width"]) != 1103:
print(f"{tag}: width {int(meta['width'])} != 1103 - skipped (pool needs one input shape)")
continue
data[tag] = (rows, meta, *chrono_split(rows, meta))
train_sets = [(r, m, tr) for (r, m, tr, _, _) in data.values()]
eval_tags = [(t, r, m, c, te) for t, (r, m, _, c, te) in data.items()]
run_eval(train_sets, eval_tags, "pooled")
for t, (r, m, tr, c, te) in data.items():
run_eval([(r, m, tr)], [(t, r, m, c, te)], f"self:{t}")
return
print(__doc__)
if __name__ == "__main__":
main()