Files
PythagorasGoal/scripts/analysis/shape_analog_research.py
T
Adriano 4ac87ab385 research(shape): 5 famiglie di pattern-forma su harness onesto
Harness shape_lab (analog kNN causale, no look-ahead verificato) + 5 ricerche
parallele. 4/5 famiglie = RUMORE (confermano dominanza mean-reversion):
- analog kNN forma grezza: solo BTC-overfit, non robusto >=2 asset
- encoding candele UP/DOWN/DOJI + body/shadow: hit-rate ~50%, muore a fee
- DTW + template geometrici: DTW peggiora euclidea; template overfit
- PIP/pivot/zig-zag: 0/48 config robuste
1/5 = EDGE REALE: ML walk-forward (LogisticRegression) sulle feature di forma.
  BTC logit W24H12 th0.58: FULL +219% / OOS +42% / Sharpe 2.72 / 8-9 anni+ /
  regge fee 0.20% RT (+60/+26). Causalita' verificata. Da validare a fondo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 12:09:28 +02:00

178 lines
8.4 KiB
Python

"""Ricerca sistematica edge nella FORMA (analog forecasting / kNN) — netto fee, OOS.
Obiettivo: trovare una config di analog forecasting ROBUSTA, cioe' positiva
FULL+OOS, che regge fee 0.20% RT e ha quasi tutti gli anni positivi, su >=2 asset.
Si combatte la "morte per fee" della baseline (BTC1h W24H12K50 agree0.60:
FULL +112%/OOS +48% Sharpe 1.38 ma a 0.2% RT -> FULL -72 / OOS -18, win 49.5%,
esposizione 73.9%, 4531 trade) con SELETTIVITA':
- agree alto (0.70..0.90) -> entra solo con analoghi molto concordi
- conf_atr > 0 -> richiede |rendimento medio analoghi| >= conf_atr*ATR
- trend_max/ema_long -> salta forme in trend estremo
- tp_atr/sl_atr -> exit intrabar invece che solo a tempo
Tutto causale: la forma usa solo close<=i, la libreria analoghi termina < i-H.
Per performance, il forecast kNN grezzo per barra si calcola UNA volta per
(W,H,K,rebuild) con analog_signals(); i filtri (agree/conf/trend/tp/sl) sono
applicati a valle con entries_from_signals() (cheap, risultato identico ad
analog_entries — verificato). Engine netto-fee + OOS da explore_lab.
Uso:
uv run python scripts/analysis/shape_analog_research.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.shape_lab import ( # noqa: E402
analog_signals, entries_from_signals, check_no_lookahead,
)
from scripts.analysis.explore_lab import get_df, evaluate, robust # noqa: E402
ROBUSTE: list[tuple] = []
MIN_TRADES = 100 # un edge "robusto" su <100 trade e' rumore campionario, non edge
def _hdr(s: str) -> None:
print("\n" + "=" * 100, flush=True)
print(" " + s, flush=True)
print("=" * 100, flush=True)
def _eval(df, sig, asset, tf, tag, **filt):
ents = entries_from_signals(df, sig, **filt)
res = evaluate(f"[{asset} {tf}] {tag}", ents, df)
# robusto E con campione sufficiente (un edge su <100 trade non e' affidabile)
if robust(res) and res["full"]["trades"] >= MIN_TRADES:
print(f" ^^^ ROBUSTA ({asset} {tf}): {tag} filt={filt}", flush=True)
ROBUSTE.append((asset, tf, tag, dict(filt), res))
elif robust(res):
print(f" (robust ma trade={res['full']['trades']}<{MIN_TRADES}: campione "
f"insufficiente, ignorato)", flush=True)
return res
def run():
# --- 0) sanity no-lookahead ---------------------------------------------
_hdr("0) SANITY no-lookahead (forma causale)")
df_btc = get_df("BTC", "1h")
check_no_lookahead(df_btc, W=24, H=12)
# sig base W24H12K50 (riusato per selettivita' agree/conf/tp/sl/trend)
sig0 = analog_signals(df_btc, W=24, H=12, K=50, rebuild=250)
# --- 1) selettivita' via agree ------------------------------------------
_hdr("1) BTC 1h — selettivita' agree (W24 H12 K50, time-exit)")
for ag in (0.60, 0.70, 0.80, 0.90):
_eval(df_btc, sig0, "BTC", "1h", f"agree{ag}", agree=ag)
# --- 2) conf_atr (forza segnale) ----------------------------------------
_hdr("2) BTC 1h — conf_atr (W24 H12 K50 agree0.70)")
for ca in (0.0, 0.25, 0.5, 1.0, 1.5):
_eval(df_btc, sig0, "BTC", "1h", f"ag0.70 conf{ca}", agree=0.70, conf_atr=ca)
# --- 3) tp/sl intrabar ---------------------------------------------------
_hdr("3) BTC 1h — exit intrabar tp/sl (W24 H12 K50 agree0.70 conf0.5)")
for tp, sl in [(1.0, 1.0), (1.5, 1.0), (2.0, 1.5), (1.5, 2.0), (3.0, 2.0)]:
_eval(df_btc, sig0, "BTC", "1h", f"tp{tp}sl{sl}",
agree=0.70, conf_atr=0.5, tp_atr=tp, sl_atr=sl)
# --- 4) filtro trend -----------------------------------------------------
_hdr("4) BTC 1h — filtro trend_max (W24 H12 K50 agree0.70 conf0.5)")
for tm in (None, 2.0, 3.0, 4.0):
_eval(df_btc, sig0, "BTC", "1h", f"trend_max{tm}",
agree=0.70, conf_atr=0.5, trend_max=tm, ema_long=200)
# --- 5) griglia W/H/K (agree0.80, time-exit) plateau ---------------------
# Griglia focalizzata: con agree0.80 e H>=24 i trade -> ~0 (vedi sez.1), e W>=24
# porta OOS negativo; il segnale vive su W piccolo, H breve. Testo il plateau
# attorno a quella regione + una banda di controllo (W24/48) per confermare il bordo.
_hdr("5) BTC 1h — griglia W/H/K (agree0.80, time-exit) — plateau check")
for W in (12, 24, 48):
for H in (6, 12, 24):
for K in (30, 50, 80):
sig = analog_signals(df_btc, W=W, H=H, K=K, rebuild=250)
_eval(df_btc, sig, "BTC", "1h", f"W{W}H{H}K{K}", agree=0.80)
# --- 6) rebuild sensitivity ---------------------------------------------
_hdr("6) BTC 1h — rebuild 250 vs 500 (W24 H12 K80 agree0.80)")
for rb in (250, 500):
sig = analog_signals(df_btc, W=24, H=12, K=80, rebuild=rb)
_eval(df_btc, sig, "BTC", "1h", f"rebuild{rb}", agree=0.80)
# --- 7) cross-asset 1h: candidati selettivi -----------------------------
_hdr("7) cross-asset 1h — candidati selettivi (>=2 robusti richiesto)")
# (build_kw: per analog_signals) (filt: per entries_from_signals)
# Su BTC 1h le uniche regioni con OOS positivo che regge fee0.2% sono W piccolo,
# H breve, K basso (W12H12K30: FULL+88/OOS+36, fee0.2% +69/+32, 243 trade, 8/9 anni;
# W12H6K30: +35/+11, fee0.2% +20/+7). conf0.25 con W24H12 e' il miglior in-sample
# ma OOS@fee~0. Verifico questi candidati cross-asset (>=2 robusti richiesto).
candidates = [
("C1 W12H12K30 ag.80", dict(W=12, H=12, K=30), dict(agree=0.80)),
("C2 W12H6K30 ag.80", dict(W=12, H=6, K=30), dict(agree=0.80)),
("C3 W12H12K30 ag.70", dict(W=12, H=12, K=30), dict(agree=0.70)),
("C4 W24H12K50 ag.70 conf.25", dict(W=24, H=12, K=50), dict(agree=0.70, conf_atr=0.25)),
("C5 W12H12K30 ag.80 trend3", dict(W=12, H=12, K=30), dict(agree=0.80, trend_max=3.0, ema_long=200)),
("C6 W12H6K50 ag.70", dict(W=12, H=6, K=50), dict(agree=0.70)),
]
per_cand: dict[str, int] = {}
for asset in ("BTC", "ETH", "ADA", "LTC", "SOL", "XRP"):
try:
df = get_df(asset, "1h")
except Exception as ex:
print(f" [{asset} 1h] SKIP load: {ex}", flush=True)
continue
# cache analog_signals per ogni build_kw distinto su questo asset
sig_cache: dict[tuple, dict] = {}
for tag, bkw, filt in candidates:
key = tuple(sorted(bkw.items()))
if key not in sig_cache:
sig_cache[key] = analog_signals(df, rebuild=250, **bkw)
res = _eval(df, sig_cache[key], asset, "1h", tag, **filt)
if robust(res):
per_cand[tag] = per_cand.get(tag, 0) + 1
# --- 8) verifica 15m dei candidati robusti su >=2 asset 1h --------------
_hdr("8) verifica 15m dei candidati robusti su >=2 asset 1h")
good = [t for t, c in per_cand.items() if c >= 2]
if not good:
print(" Nessun candidato robusto su >=2 asset 1h -> niente verifica 15m.", flush=True)
else:
for tag in good:
_, bkw, filt = next(c for c in candidates if c[0] == tag)
for asset in ("BTC", "ETH"):
try:
df = get_df(asset, "15m")
except Exception as ex:
print(f" [{asset} 15m] SKIP load: {ex}", flush=True)
continue
sig = analog_signals(df, rebuild=250, **bkw)
_eval(df, sig, asset, "15m", f"{tag} (15m)", **filt)
# --- VERDETTO ------------------------------------------------------------
_hdr("VERDETTO")
if ROBUSTE:
agg: dict[str, list] = {}
for asset, tf, tag, filt, res in ROBUSTE:
agg.setdefault(tag, []).append(f"{asset}/{tf}")
print(f" {len(ROBUSTE)} sleeve robusti (FULL+OOS+ fee0.2% + anniPos):", flush=True)
edge = False
for tag, asl in agg.items():
n_assets = len({a.split('/')[0] for a in asl})
mark = " *** EDGE (>=2 asset)" if n_assets >= 2 else " (1 asset: non sufficiente)"
if n_assets >= 2:
edge = True
print(f" - {tag}: {asl}{mark}", flush=True)
if not edge:
print("\n CONCLUSIONE: nessuna config robusta su >=2 asset -> RUMORE.", flush=True)
else:
print(" NESSUNA config robusta. Famiglia analog/forma = RUMORE sotto fee reali.", flush=True)
return ROBUSTE
if __name__ == "__main__":
run()