forked from animatedread/Warrior_EA
133 lines
5.4 KiB
Python
133 lines
5.4 KiB
Python
"""
| |||
Move catalogue (2026-10-02): how many big moves exist, in BOTH directions, at EVERY scale,
| |||
and how much of each is left after the real spread. Hindsight is used ONLY to label legs;
| |||
nothing here is a feature. Reads the fleet terminal's .hcc read-only.
| |||
| |||
Scale ladder: zigzag legs whose reversal is k x (causal local vol) for k in K_LADDER, so a move
| |||
is "big" relative to its own context, at several scales at once (no single threshold).
| |||
Cost: median broker spread (points) x point size, per year.
| |||
| |||
usage: python moves_catalogue.py SP500 XAUUSD ... [--tf 60]
| |||
"""
| |||
from __future__ import annotations
| |||
import os, sys
| |||
import numpy as np
| |||
import pandas as pd
| |||
| |||
sys.path.insert(0, os.path.dirname(__file__))
| |||
import hcc
| |||
| |||
ROOT = (r"C:\Users\admin\AppData\Roaming\MetaQuotes\Terminal\10CE948A1DFC9A8C27E56E827008EBD4"
| |||
r"\bases\FivePercentOnline-Real\history")
| |||
K_LADDER = (2.0, 4.0, 8.0, 16.0)
| |||
VOL_N = 100
| |||
| |||
| |||
def sym_name(d):
| |||
return os.path.basename(d)
| |||
| |||
| |||
def load(symbol: str) -> pd.DataFrame:
| |||
d = os.path.join(ROOT, symbol)
| |||
parts = []
| |||
for f in sorted(os.listdir(d)):
| |||
if f.endswith(".hcc"):
| |||
try:
| |||
a = hcc.read_hcc(os.path.join(d, f))
| |||
except PermissionError: # current-year file is locked by the running terminal
| |||
print(f" (skipped locked {sym_name(d)}/{f})"); continue
| |||
if len(a):
| |||
parts.append(a)
| |||
a = np.concatenate(parts)
| |||
a = a[np.argsort(a["t"], kind="stable")]
| |||
df = pd.DataFrame({"o": a["o"], "h": a["h"], "l": a["l"], "c": a["c"], "tv": a["tv"], "sp": a["sp"]},
| |||
index=pd.to_datetime(a["t"], unit="s"))
| |||
return df[~df.index.duplicated()]
| |||
| |||
| |||
def point_size(c: np.ndarray) -> float:
| |||
d = np.abs(np.diff(c[-200000:]))
| |||
d = d[d > 0]
| |||
return float(np.percentile(d, 1)) if len(d) else 1e-5
| |||
| |||
| |||
def resample(m1: pd.DataFrame, minutes: int) -> pd.DataFrame:
| |||
r = m1.resample(f"{minutes}min")
| |||
out = pd.DataFrame({"o": r.o.first(), "h": r.h.max(), "l": r.l.min(), "c": r.c.last(),
| |||
"tv": r.tv.sum(), "sp": r.sp.median(), "n": r.o.count()})
| |||
return out[out.n >= max(1, minutes // 10)].drop(columns="n")
| |||
| |||
| |||
def local_vol(b: pd.DataFrame) -> np.ndarray:
| |||
pc = b.c.shift(1)
| |||
tr = np.maximum(b.h - b.l, np.maximum((b.h - pc).abs(), (b.l - pc).abs()))
| |||
return tr.rolling(VOL_N, min_periods=VOL_N // 2).mean().shift(1).to_numpy() # causal
| |||
| |||
| |||
def zigzag(h, l, vol, k):
| |||
"""Completed legs (start_idx, end_idx, dir, size) between confirmed pivots; confirmation = reversal of k*vol."""
| |||
n = len(h)
| |||
i0 = next((i for i in range(n) if np.isfinite(vol[i])), None)
| |||
legs = []
| |||
if i0 is None:
| |||
return legs
| |||
hi_i = lo_i = i0
| |||
d = 0
| |||
piv_i, piv_p = None, None # last confirmed pivot
| |||
for i in range(i0 + 1, n):
| |||
thr = k * vol[i]
| |||
if d == 0:
| |||
if h[i] > h[hi_i]: hi_i = i
| |||
if l[i] < l[lo_i]: lo_i = i
| |||
if h[hi_i] - l[lo_i] >= thr:
| |||
if hi_i > lo_i: # rose first: lo is the first pivot, now going up
| |||
piv_i, piv_p, d = lo_i, l[lo_i], 1
| |||
else:
| |||
piv_i, piv_p, d = hi_i, h[hi_i], -1
| |||
ext_i = i
| |||
ext_i = hi_i if d == 1 else lo_i
| |||
elif d == 1:
| |||
if h[i] >= h[ext_i]: ext_i = i
| |||
elif h[ext_i] - l[i] >= thr: # up-leg confirmed
| |||
legs.append((piv_i, ext_i, 1, h[ext_i] - piv_p))
| |||
piv_i, piv_p, d, ext_i = ext_i, h[ext_i], -1, i
| |||
else:
| |||
if l[i] <= l[ext_i]: ext_i = i
| |||
elif h[i] - l[ext_i] >= thr: # down-leg confirmed
| |||
legs.append((piv_i, ext_i, -1, piv_p - l[ext_i]))
| |||
piv_i, piv_p, d, ext_i = ext_i, l[ext_i], 1, i
| |||
return legs
| |||
| |||
| |||
def main(symbols, tf):
| |||
for sym in symbols:
| |||
m1 = load(sym)
| |||
yr = m1.groupby(m1.index.year).size()
| |||
print(f"\n===== {sym} M1 rows/year: " + " ".join(f"{y}:{n // 1000}k" for y, n in yr.items()))
| |||
pt = point_size(m1.c.to_numpy())
| |||
b = resample(m1, tf)
| |||
vol = local_vol(b)
| |||
cost = (b.sp.fillna(b.sp.median()) * pt).to_numpy() # spread in price units, per bar
| |||
h, l, c = b.h.to_numpy(), b.l.to_numpy(), b.c.to_numpy()
| |||
years = b.index.year.to_numpy()
| |||
rows = []
| |||
for k in K_LADDER:
| |||
for (s, e, d, size) in zigzag(h, l, vol, k):
| |||
rows.append((k, years[s], d, size, size / max(cost[s], pt), e - s))
| |||
t = pd.DataFrame(rows, columns=["k", "year", "dir", "size", "x_spread", "bars"])
| |||
if t.empty:
| |||
print("no legs"); continue
| |||
g = t.groupby(["k", "dir"]).agg(n=("size", "size"), med_x_spread=("x_spread", "median"),
| |||
share_ge3=("x_spread", lambda s: (s >= 3).mean()),
| |||
med_bars=("bars", "median"))
| |||
print(f"point~{pt:g} tf={tf}m bars={len(b)}")
| |||
print(g.round(2).to_string())
| |||
pv = t[t.k == 4.0].pivot_table(index="year", columns="dir", values="size", aggfunc="size").fillna(0).astype(int)
| |||
print("legs/year at k=4 (dir -1 / +1):"); print(pv.T.to_string())
| |||
| |||
| |||
if __name__ == "__main__":
| |||
args = [a for a in sys.argv[1:] if not a.startswith("--")]
| |||
tf = int(sys.argv[sys.argv.index("--tf") + 1]) if "--tf" in sys.argv else 60
| |||
args = [a for a in args if not a.isdigit()]
| |||
main(args, tf)
|