ответвлён от animatedread/Warrior_EA
137 строки
6,9 КиБ
Python
137 строки
6,9 КиБ
Python
"""
|
|
Swing-structure pattern mining (2026-10-03) - the chart as a trader reads it, not as fixed windows.
|
|
|
|
PRE-REGISTERED (before any result):
|
|
Event = a CONFIRMED swing pivot (zigzag reversal of k x ATR100; k in {2, 4}; k is the scale). At the
|
|
confirmation bar the pattern is the last 6 legs: signed size (ATR), duration (log bars), volume of the
|
|
leg (mean tickvol / mean100), overshoot of the leg's extreme past the previous same-side extreme
|
|
(a sweep/break, ATR, signed), + hour/dow of the pivot. No fixed look-back; invariant to time-stretch.
|
|
Clusters (K=60 per scale) fit on A <= 2015 without labels; selected on B 2016-18; tested once on C 2019-24.
|
|
Labels: realised net R of a long AND a short entered at the open after confirmation (3 variants as in
|
|
pattern_mining2). Control = vol-matched random entry (deciles of recent range). 2025+ SEALED.
|
|
Selection rule and verdict identical to pattern_mining2.
|
|
"""
|
|
from __future__ import annotations
|
|
import os, sys, time
|
|
import numpy as np, pandas as pd
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
import discover as D
|
|
import pattern_mining as PM
|
|
import pattern_mining2 as M2
|
|
|
|
SCALES = (2.0, 4.0)
|
|
NL = 6
|
|
K = int(os.environ.get('KCL', 60))
|
|
|
|
|
|
def zz_confirm(h, l, atr, k):
|
|
"""confirmed legs -> list of (pivot_idx, ext_idx, dir, confirm_idx)."""
|
|
n = len(h)
|
|
i0 = next((i for i in range(n) if np.isfinite(atr[i])), None)
|
|
out = []
|
|
if i0 is None: return out
|
|
hi_i = lo_i = i0; d = 0; piv_i = None; ext_i = i0
|
|
for i in range(i0 + 1, n):
|
|
thr = k * atr[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: piv_i, d, ext_i = lo_i, 1, hi_i
|
|
else: piv_i, d, ext_i = hi_i, -1, lo_i
|
|
elif d == 1:
|
|
if h[i] >= h[ext_i]: ext_i = i
|
|
elif h[ext_i] - l[i] >= thr:
|
|
out.append((piv_i, ext_i, 1, i)); piv_i, d, ext_i = ext_i, -1, i
|
|
else:
|
|
if l[i] <= l[ext_i]: ext_i = i
|
|
elif h[i] - l[ext_i] >= thr:
|
|
out.append((piv_i, ext_i, -1, i)); piv_i, d, ext_i = ext_i, 1, i
|
|
return out
|
|
|
|
|
|
def swing_events(b, k):
|
|
h, l, c = b.h.to_numpy(), b.l.to_numpy(), b.c.to_numpy()
|
|
tv = b.tv.astype(float).to_numpy()
|
|
pc = np.r_[c[0], c[:-1]]
|
|
tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
|
|
atr = pd.Series(tr).rolling(100, min_periods=50).mean().to_numpy()
|
|
vm = pd.Series(tv).rolling(100, min_periods=50).mean().to_numpy()
|
|
legs = zz_confirm(h, l, atr, k)
|
|
idx, feats = [], []
|
|
for j in range(NL + 2, len(legs)):
|
|
piv, ext, d, conf = legs[j]
|
|
s = atr[conf]
|
|
row = []
|
|
for q in range(NL): # q=0 is the leg just confirmed
|
|
pi, ei, di, _ = legs[j - q]
|
|
p0 = l[pi] if di == 1 else h[pi]; p1 = h[ei] if di == 1 else l[ei]
|
|
size = di * (p1 - p0) / s
|
|
dur = np.log1p(ei - pi)
|
|
vol = np.nanmean(tv[pi:ei + 1]) / vm[conf] if vm[conf] > 0 else np.nan
|
|
# overshoot of this leg's extreme past the previous same-direction extreme (legs alternate: two back)
|
|
pk = legs[j - q - 2]
|
|
prev_ext = h[pk[1]] if di == 1 else l[pk[1]]
|
|
over = di * (p1 - prev_ext) / s
|
|
row += [size, dur, vol, over]
|
|
idx.append(conf); feats.append(row)
|
|
idx = np.array(idx, int); F = np.array(feats, np.float32)
|
|
return idx, F, atr
|
|
|
|
|
|
def main():
|
|
from sklearn.cluster import MiniBatchKMeans
|
|
t0 = time.time()
|
|
syms = os.environ.get('SYMS', ','.join(PM.SYMS)).split(',')
|
|
ya, yb = (int(x) for x in os.environ.get('SPLIT', '2015,2018').split(','))
|
|
print('classes:', syms, 'A<=', ya, 'B<=', yb)
|
|
for k in SCALES:
|
|
S, Y, TS, T, F, RR = [], [], [], [], [], []
|
|
OUT = {v: {kk: [] for kk in ("RL", "RS", "XL", "XS")} for v in M2.VARS}
|
|
for sym in syms:
|
|
b = D.load_h1(sym); b = b[b.index < D.SEAL]
|
|
idx, X, atr = swing_events(b, k)
|
|
ok = (idx + 97 < len(b)) & np.isfinite(X).all(1) & np.isfinite(atr[idx])
|
|
idx, X = idx[ok], X[ok]
|
|
ts = b.index[idx]
|
|
rng = (b.h - b.l).to_numpy()
|
|
rr = pd.Series(rng).rolling(12).mean().to_numpy()[idx] / atr[idx]
|
|
for v, (tp, sl, T_) in M2.VARS.items():
|
|
res = M2.realised(b, idx, atr, tp, sl, T_, D.COST_BP[sym])
|
|
for kk, a in zip(("RL", "RS", "XL", "XS"), res): OUT[v][kk].append(a)
|
|
F.append(np.c_[X, np.sin(2 * np.pi * ts.hour / 24), np.cos(2 * np.pi * ts.hour / 24),
|
|
np.sin(2 * np.pi * ts.dayofweek / 5), np.cos(2 * np.pi * ts.dayofweek / 5)])
|
|
S += [sym] * len(idx); Y.append(ts.year.to_numpy()); RR.append(rr)
|
|
TS.append(((ts - pd.Timestamp("1970-01-01")) // pd.Timedelta("1h")).to_numpy())
|
|
cat = np.concatenate
|
|
F = cat(F); Y = cat(Y); S = np.array(S); TS = cat(TS); RR = cat(RR)
|
|
OUT = {v: {kk: cat(a) for kk, a in d.items()} for v, d in OUT.items()}
|
|
A = Y <= ya; B = (Y > ya) & (Y <= yb); C = (Y > yb) & (Y <= 2024)
|
|
print(f"\n##### scale k={k}: {len(Y):,} pivot events (A {A.sum():,} B {B.sum():,} C {C.sum():,}) {time.time() - t0:.0f}s", flush=True)
|
|
Fz = (F - F[A].mean(0)) / (F[A].std(0) + 1e-6)
|
|
Fz = np.clip(Fz, -5, 5)
|
|
km = MiniBatchKMeans(K, random_state=0, n_init=5, batch_size=4096).fit(Fz[A])
|
|
lab = km.predict(Fz)
|
|
edges = np.quantile(RR[A], np.linspace(0, 1, 11)[1:-1]); vb = np.searchsorted(edges, RR)
|
|
for v in M2.VARS:
|
|
for side, rk, xk in (("long", "RL", "XL"), ("short", "RS", "XS")):
|
|
r = OUT[v][rk]; x = OUT[v][xk]; fin = np.isfinite(r)
|
|
sel = [c for c in range(K) if all(((per & (lab == c) & fin).sum() >= 100 and r[per & (lab == c) & fin].mean() > 0) for per in (A, B))]
|
|
ctrl_b = np.array([r[C & fin & (vb == q)].mean() for q in range(10)])
|
|
rows = [(c, (C & fin & (lab == c)).sum(), r[C & fin & (lab == c)].mean()) for c in sel if (C & fin & (lab == c)).sum() >= 30]
|
|
msel = C & fin & np.isin(lab, sel)
|
|
if not msel.any():
|
|
print(f"k={k} {v} {side}: none selected"); continue
|
|
kept = M2.thin_idx(TS, x, msel, S)
|
|
rk_ = r[kept]; ctrl = ctrl_b[vb[msel]].mean()
|
|
t = rk_.mean() / (rk_.std() / np.sqrt(len(rk_))) if len(rk_) > 1 else np.nan
|
|
npos = sum(1 for _, _, m in rows if m > 0)
|
|
print(f"k={k} {v} {side}: sel {len(sel)} cl; C still>0 {npos}/{len(rows)} | thinned n={len(kept)} meanR {rk_.mean():+.3f} "
|
|
f"vs vol-matched {ctrl:+.3f} lift {rk_.mean() - ctrl:+.3f} t(vs0) {t:.2f}", flush=True)
|
|
np.savez_compressed(os.path.join(D.CACHE, f"swings_k{int(k)}.npz"), F=F, lab=lab, Y=Y, S=S, vb=vb,
|
|
**{f"{v}_{kk}": OUT[v][kk] for v in M2.VARS for kk in ("RL", "RS", "XL", "XS")})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|