feat(shape): SH01 Shape-ML validato come diversificatore + doc
Validazione dura del solo edge sopravvissuto alla ricerca shape (ML walk-forward LogisticRegression sulle feature di forma). SH01 config W24 H12 th0.58: - BTC robusto ovunque (expanding +219%/OOS+42% Sharpe2.72 8-9anni; rolling2y +166%/+96%; stress leva2x+slippage OK), ETH/ADA solo expanding, LTC/SOL/XRP no. - Griglia 5/27 robuste su cresta W24/H8-12 -> overfit moderato, config conservativa. - Free-lunch: corr +0.08 col MASTER, aggiungerlo migliora OOS (Sharpe 4.33->5.10, DD 4.7->4.2%). Diversificatore, non motore standalone. Regge fee 0.20% RT. SH01 come Strategy (in MODULE_MAP) + run() riproducibile. shape_ml_research esteso con walk-forward rolling (train_window). Live richiede worker con retraining. Diario 2026-05-29-shape.md, CLAUDE.md famiglia SHAPE-ML. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -244,10 +244,13 @@ def analog_feat_entries(df, W=24, H=12, K=60, rebuild=300, min_lib=1500,
|
||||
# =============================================================================
|
||||
def ml_wf_entries(df, W=24, H=12, model="gb", thresh=0.58,
|
||||
train_min=4000, retrain=500, n_estimators=80, max_depth=3,
|
||||
tp_atr=None, sl_atr=None, trend_max=None, ema_long=200) -> list[dict]:
|
||||
"""Walk-forward: a blocchi di `retrain` barre, allena su TUTTO il passato il cui esito
|
||||
tp_atr=None, sl_atr=None, trend_max=None, ema_long=200,
|
||||
train_window=None) -> list[dict]:
|
||||
"""Walk-forward: a blocchi di `retrain` barre, allena sul passato il cui esito
|
||||
e' noto, predice il blocco corrente. Scaler+modello fittati solo sul train.
|
||||
Entra a close[i] se proba della classe predetta >= thresh. model in {gb, logit}."""
|
||||
Entra a close[i] se proba della classe predetta >= thresh. model in {gb, logit}.
|
||||
train_window: se None -> expanding (tutto il passato); se int -> ROLLING (solo le
|
||||
ultime train_window barre prima del blocco) -> test di robustezza piu' severo."""
|
||||
c = df["close"].values
|
||||
n = len(c)
|
||||
a = atr(df, 14)
|
||||
@@ -268,7 +271,8 @@ def ml_wf_entries(df, W=24, H=12, model="gb", thresh=0.58,
|
||||
blk_end = min(blk + retrain, n - 1)
|
||||
# TRAIN: finestre la cui forma E il cui esito (e+H) sono < blk
|
||||
# cioe' e <= blk-1-H (esito realizzato prima del primo test del blocco)
|
||||
tr_ends = ends[(ends <= blk - 1 - H) & (ends >= W - 1)]
|
||||
lo_end = (blk - 1 - H - train_window) if train_window is not None else (W - 1)
|
||||
tr_ends = ends[(ends <= blk - 1 - H) & (ends >= max(W - 1, lo_end))]
|
||||
tr_ends = tr_ends[~np.isnan(sgn[tr_ends])]
|
||||
if len(tr_ends) < 800:
|
||||
blk = blk_end
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
"""Validazione DURA del solo edge sopravvissuto alla ricerca shape: ML walk-forward
|
||||
(LogisticRegression) sulle feature di FORMA. Tutto il resto della famiglia shape e' rumore.
|
||||
|
||||
Candidato: BTC logit W24H12 th0.58 (FULL +219% / OOS +42% / Sharpe 2.72 / 8-9 anni+,
|
||||
regge fee 0.20% RT). Prima di promuoverlo a strategia serve (metodologia obbligatoria):
|
||||
1. ROBUSTEZZA MULTI-ASSET: stessa config su BTC/ETH/LTC/SOL/ADA/XRP 1h.
|
||||
2. WALK-FORWARD ROLLING (train fisso 2y) oltre all'expanding -> niente "memoria infinita".
|
||||
3. STRESS leva 2x + slippage doppio (fee 0.20% RT) -> regge in condizioni realistiche?
|
||||
4. ROBUSTEZZA SU GRIGLIA (th, W, H) -> plateau, non picco.
|
||||
5. CORRELAZIONE col MASTER + integrazione -> e' un diversificatore (free-lunch)?
|
||||
|
||||
Tutto netto-fee, OOS = ultimo 30%. Conta il PnL netto, non l'accuracy (lezione squeeze).
|
||||
|
||||
Run: uv run python scripts/analysis/shape_ml_validate.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
from scripts.analysis.explore_lab import get_df, evaluate, robust, FEE_RT, LEV, POS, OOS_FRAC
|
||||
from scripts.analysis.shape_ml_research import ml_wf_entries, acc_oos
|
||||
|
||||
ASSETS = ["BTC", "ETH", "LTC", "SOL", "ADA", "XRP"]
|
||||
TWO_YEARS_1H = 24 * 365 * 2 # ~17520 barre = finestra rolling 2 anni
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
def line(name, ents, df, fees=(0.0, 0.001, 0.002)):
|
||||
"""Riga evaluate + accuracy OOS, ritorna (res, robusto?)."""
|
||||
res = evaluate(name, ents, df)
|
||||
ac = acc_oos(ents, df)
|
||||
rb = (res["full"]["ret"] > 0 and res["oos"]["ret"] > 0
|
||||
and res["sweep"][0.002] > 0 and res["sweep_oos"][0.002] > 0)
|
||||
print(f" ^ accOOS={ac:4.1f}% {'[ROBUST fee0.2%]' if rb else ''}")
|
||||
return res, rb
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
def sec_multi_asset(W=24, H=12, th=0.58):
|
||||
print("\n[1] MULTI-ASSET — logit W%dH%d th%.2f, walk-forward EXPANDING (1h):" % (W, H, th))
|
||||
ok = []
|
||||
dfs = {}
|
||||
for a in ASSETS:
|
||||
df = get_df(a, "1h"); dfs[a] = df
|
||||
ents = ml_wf_entries(df, W=W, H=H, model="logit", thresh=th)
|
||||
_, rb = line(f"{a} exp", ents, df)
|
||||
if rb:
|
||||
ok.append(a)
|
||||
print(f" -> EXPANDING robusti (fee0.2%): {ok if ok else 'NESSUNO'}")
|
||||
return dfs, ok
|
||||
|
||||
|
||||
def sec_rolling(dfs, W=24, H=12, th=0.58, tw=TWO_YEARS_1H):
|
||||
print("\n[2] WALK-FORWARD ROLLING — train fisso ~2 anni (%d barre), stessa config:" % tw)
|
||||
ok = []
|
||||
for a in ASSETS:
|
||||
ents = ml_wf_entries(dfs[a], W=W, H=H, model="logit", thresh=th, train_window=tw)
|
||||
_, rb = line(f"{a} roll2y", ents, dfs[a])
|
||||
if rb:
|
||||
ok.append(a)
|
||||
print(f" -> ROLLING robusti (fee0.2%): {ok if ok else 'NESSUNO'}")
|
||||
return ok
|
||||
|
||||
|
||||
def sec_stress(dfs, W=24, H=12, th=0.58):
|
||||
print("\n[3] STRESS — leva 2x + slippage doppio (fee 0.20% RT) su BTC/ETH:")
|
||||
print(" (la config nominale e' leva 3x fee 0.10%; qui peggioro entrambe)")
|
||||
from scripts.analysis.explore_lab import simulate
|
||||
for a in ["BTC", "ETH"]:
|
||||
ents = ml_wf_entries(dfs[a], W=W, H=H, model="logit", thresh=th)
|
||||
df = dfs[a]
|
||||
split = int(len(df) * (1 - OOS_FRAC))
|
||||
base = simulate(ents, df, fee_rt=0.001, lev=3.0)
|
||||
stress_f = simulate(ents, df, fee_rt=0.002, lev=2.0)
|
||||
stress_o = simulate(ents, df, fee_rt=0.002, lev=2.0, split=split)
|
||||
print(f" {a}: base(3x,0.1%) FULL={base['ret']:+.0f}% Shrp={base['sharpe']:.2f} | "
|
||||
f"STRESS(2x,0.2%) FULL={stress_f['ret']:+.0f}% OOS={stress_o['ret']:+.0f}% "
|
||||
f"DD={stress_f['dd']:.0f}% Shrp={stress_f['sharpe']:.2f} "
|
||||
f"{'OK' if stress_f['ret'] > 0 and stress_o['ret'] > 0 else 'KO'}")
|
||||
|
||||
|
||||
def sec_grid(dfs, asset="BTC"):
|
||||
print(f"\n[4] ROBUSTEZZA GRIGLIA su {asset} (plateau, non picco):")
|
||||
rob = tot = 0
|
||||
for W in (16, 24, 32):
|
||||
for H in (8, 12, 16):
|
||||
for th in (0.56, 0.58, 0.60):
|
||||
ents = ml_wf_entries(dfs[asset], W=W, H=H, model="logit", thresh=th)
|
||||
_, rb = line(f"{asset} W{W}H{H}th{th}", ents, dfs[asset])
|
||||
tot += 1; rob += rb
|
||||
print(f" -> {asset}: {rob}/{tot} celle robuste a fee 0.2% (plateau se alta frazione)")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
def shape_daily_equity(asset, IDX, W=24, H=12, th=0.58):
|
||||
"""Equity giornaliera dello sleeve shape-ML (time-exit a H, non-overlap, pos 0.15,
|
||||
leva 3x, fee 0.10% RT), normalizzata sull'indice comune dei portafogli."""
|
||||
from src.data.downloader import load_data
|
||||
df = get_df(asset, "1h")
|
||||
c = df["close"].values
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
ents = ml_wf_entries(df, W=W, H=H, model="logit", thresh=th)
|
||||
n = len(c); eq = np.full(n, 1000.0); cap = 1000.0; last_exit = -1
|
||||
fee = FEE_RT * LEV
|
||||
for e in sorted(ents, key=lambda x: x["i"]):
|
||||
i, d, mb = e["i"], e["d"], e["max_bars"]
|
||||
if i <= last_exit or i + mb >= n:
|
||||
continue
|
||||
j = i + mb
|
||||
ret = (c[j] - c[i]) / c[i] * d * LEV - fee
|
||||
cap = max(cap + cap * POS * ret, 10.0)
|
||||
eq[j:] = cap; last_exit = j
|
||||
s = pd.Series(eq, index=ts).resample("1D").last().reindex(IDX).ffill().bfill()
|
||||
return s / s.iloc[0]
|
||||
|
||||
|
||||
def sec_master_integration():
|
||||
print("\n[5] CORRELAZIONE + INTEGRAZIONE COL MASTER:")
|
||||
from scripts.analysis.combine_portfolio import (
|
||||
build_all_sleeves, port_returns, metrics, IDX, SPLIT,
|
||||
)
|
||||
sleeves = build_all_sleeves()
|
||||
# sleeve shape: BTC + ETH (i due con piu' storia/edge)
|
||||
shape = {}
|
||||
for a in ("BTC", "ETH"):
|
||||
shape[f"SH_{a}"] = shape_daily_equity(a, IDX)
|
||||
dr_master = port_returns(sleeves) # MASTER equal-weight attuale
|
||||
dr_shape = port_returns(shape)
|
||||
corr = float(dr_master.corr(dr_shape))
|
||||
print(f" correlazione daily MASTER vs sleeve-shape: {corr:+.3f}")
|
||||
# correlazione media shape vs ogni sleeve esistente
|
||||
cs = {k: float(port_returns({k: v}).corr(dr_shape)) for k, v in sleeves.items()}
|
||||
print(" corr shape vs singole sleeve: " + ", ".join(f"{k}={v:+.2f}" for k, v in cs.items()))
|
||||
|
||||
base = {**sleeves}
|
||||
ext = {**sleeves, **shape}
|
||||
fb, ob = metrics(port_returns(base)), metrics(port_returns(base), lo=SPLIT)
|
||||
fe, oe = metrics(port_returns(ext)), metrics(port_returns(ext), lo=SPLIT)
|
||||
print(" %-22s %9s %6s %6s %6s | %9s %6s %6s %6s" %
|
||||
("portafoglio", "FULLret", "CAGR", "DD", "Shrp", "OOSret", "CAGR", "DD", "Shrp"))
|
||||
print(" %-22s %+9.0f %6.0f %6.1f %6.2f | %+9.0f %6.0f %6.1f %6.2f" %
|
||||
("MASTER (9 sleeve)", fb["ret"], fb["cagr"], fb["dd"], fb["sharpe"],
|
||||
ob["ret"], ob["cagr"], ob["dd"], ob["sharpe"]))
|
||||
print(" %-22s %+9.0f %6.0f %6.1f %6.2f | %+9.0f %6.0f %6.1f %6.2f" %
|
||||
("MASTER + shape", fe["ret"], fe["cagr"], fe["dd"], fe["sharpe"],
|
||||
oe["ret"], oe["cagr"], oe["dd"], oe["sharpe"]))
|
||||
better = oe["sharpe"] > ob["sharpe"] and oe["dd"] <= ob["dd"] + 1
|
||||
print(f" -> aggiungere shape MIGLIORA il MASTER OOS (Sharpe up, DD ~stabile)? "
|
||||
f"{'SI' if better else 'NO'}")
|
||||
|
||||
|
||||
def run():
|
||||
t0 = time.time()
|
||||
print("=" * 100)
|
||||
print(" VALIDAZIONE DURA — shape-ML (LogisticRegression walk-forward sulle feature di forma)")
|
||||
print("=" * 100)
|
||||
dfs, _ = sec_multi_asset()
|
||||
sec_rolling(dfs)
|
||||
sec_stress(dfs)
|
||||
sec_grid(dfs, "BTC")
|
||||
sec_master_integration()
|
||||
print(f"\n tempo totale: {time.time() - t0:.0f}s")
|
||||
print("=" * 100)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,102 @@
|
||||
"""SH01 — Shape-ML: direzione predetta dalla FORMA del segnale (ML walk-forward).
|
||||
|
||||
FAMIGLIA NUOVA, distinta da tutto l'esistente. Non e' una regola fissa su bande/canali
|
||||
(fade) ne' momentum/rotazione (honest) ne' spread (pairs): una LogisticRegression legge
|
||||
la MORFOLOGIA della finestra recente (body/shadow delle candele, rendimenti, pendenza,
|
||||
curvatura, posizione di max/min, RSI, estensione) e predice il segno del rendimento a H
|
||||
barre. Entra a close[i] solo se la probabilita' supera una soglia (selettivita').
|
||||
|
||||
E' l'UNICO edge sopravvissuto alla ricerca sui pattern-di-forma (2026-05-29): le altre
|
||||
4 famiglie testate con agenti paralleli su harness onesto sono RUMORE (analog kNN forma
|
||||
grezza, encoding candele UP/DOWN/DOJI, DTW+template geometrici, PIP/pivot) — confermano
|
||||
la dominanza mean-reversion. Vedi scripts/analysis/shape_*_research.py e docs/diary.
|
||||
|
||||
Logica (engine onesto verificato in scripts/analysis/shape_ml_research.py):
|
||||
feature di forma X[i] da o/h/l/c[i-W+1..i] (causali: solo dati fino a close[i])
|
||||
walk-forward a blocchi: scaler+modello fittati SOLO sul passato con esito noto
|
||||
(finestre e con e+H <= inizio_blocco-1), poi predicono il blocco corrente
|
||||
proba(classe) >= thresh -> entra a close[i] nella direzione predetta, exit a H barre
|
||||
fee 0.10% RT (single-leg). NESSUN look-ahead (check espliciti: perturbare il futuro
|
||||
non cambia ne' le feature a i ne' le predizioni fino a i).
|
||||
|
||||
VALIDAZIONE DURA (netto fee, leva 3x, pos 0.15, OOS = ultimo 30%, config W24 H12 th0.58,
|
||||
scripts/analysis/shape_ml_validate.py):
|
||||
- MULTI-ASSET expanding: robusti BTC, ETH, ADA; scartati LTC/SOL/XRP.
|
||||
BTC : FULL +219% / OOS +42% / Sharpe 2.72 / DD 23% / 8-9 anni+ / accOOS 56% (regge fee 0.2%: +60/+26)
|
||||
ETH : FULL +80% / OOS +144% / Sharpe 1.21 / DD 61% / 6/9 anni+ / accOOS 55% (piu' volatile -> secondario)
|
||||
ADA : FULL +707% / OOS +57% / Sharpe 3.22 / DD 39% / 7/8 anni+ (robusto solo expanding)
|
||||
- WALK-FORWARD ROLLING (train fisso 2 anni): regge solo BTC (FULL +166% / OOS +96% / Sharpe 2.05).
|
||||
-> l'edge si appoggia in parte alla memoria lunga: BTC e' il piu' solido.
|
||||
- STRESS leva 2x + slippage doppio (fee 0.20% RT): BTC OK (FULL +40% / OOS +17% / Sharpe 1.24),
|
||||
ETH marginale (FULL +7% / OOS +73% / Sharpe 0.37).
|
||||
- GRIGLIA (W,H,thresh) su BTC: 5/27 celle robuste a fee 0.2%, su una CRESTA stretta
|
||||
(W24, H8-12), non altopiano largo -> rischio overfit moderato. Per prudenza si sceglie
|
||||
la config robusta sul PIU' ALTO numero di test (W24 H12 th0.58), non il PnL massimo
|
||||
(W24 H8 rende di piu' ma accOOS ~49% = piu' drift che segnale).
|
||||
|
||||
USO CONSIGLIATO: NON motore standalone (per-asset e' troppo stretto/fragile fuori da BTC),
|
||||
ma DIVERSIFICATORE di portafoglio. Corr daily col MASTER +0.08 (quasi scorrelato).
|
||||
Aggiungere lo sleeve shape (BTC+ETH) al MASTER migliora l'OOS: Sharpe 4.33->5.10,
|
||||
DD 4.7%->4.2% (scripts/analysis/shape_ml_validate.py sez. 5).
|
||||
|
||||
LIVE: richiede un worker capace di RIALLENARE periodicamente il modello (come il legacy
|
||||
signal_engine ML, non lo StrategyWorker a regola fissa). Da wirare prima del paper live.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from src.strategies.base import Strategy, Signal # noqa: E402
|
||||
from scripts.analysis.explore_lab import get_df, evaluate, robust # noqa: E402
|
||||
from scripts.analysis.shape_ml_research import ml_wf_entries # noqa: E402
|
||||
|
||||
# Config robusta scelta (cresta W24 H8-12; H12 th0.58 = la piu' robusta sui test).
|
||||
CONFIG = dict(W=24, H=12, model="logit", thresh=0.58)
|
||||
|
||||
# Asset con edge robusto. BTC primario (regge ogni stress); ETH secondario (diversificatore
|
||||
# piu' volatile). ADA robusto solo expanding -> tenuto fuori dal set live conservativo.
|
||||
ASSETS = ["BTC", "ETH"]
|
||||
|
||||
|
||||
class ShapeMLStrategy(Strategy):
|
||||
name = "SH01_shape_ml"
|
||||
description = "Direzione predetta dalla forma del segnale (LogisticRegression walk-forward), exit a H barre"
|
||||
default_assets = ASSETS
|
||||
default_timeframes = ["1h"]
|
||||
fee_rt = 0.001
|
||||
leverage = 3.0
|
||||
position_size = 0.15
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex, **params) -> list[Signal]:
|
||||
cfg = {**CONFIG, **{k: params[k] for k in ("W", "H", "model", "thresh") if k in params}}
|
||||
ents = ml_wf_entries(df, W=cfg["W"], H=cfg["H"], model=cfg["model"], thresh=cfg["thresh"])
|
||||
c = df["close"].values
|
||||
out: list[Signal] = []
|
||||
for e in ents:
|
||||
out.append(Signal(idx=e["i"], direction=e["d"], entry_price=float(c[e["i"]]),
|
||||
metadata={"max_bars": e["max_bars"]}))
|
||||
return out
|
||||
|
||||
|
||||
def run():
|
||||
print("=" * 96)
|
||||
print(" SH01 — Shape-ML | direzione dalla FORMA (LogisticRegression walk-forward) | netto fee 0.10% RT, leva 3x")
|
||||
print("=" * 96)
|
||||
print(f" config: {CONFIG} (W=finestra forma, H=orizzonte/exit, thresh=soglia proba)")
|
||||
for a in ASSETS:
|
||||
df = get_df(a, "1h")
|
||||
ents = ml_wf_entries(df, **CONFIG)
|
||||
res = evaluate(f"{a}", ents, df)
|
||||
print(f" ^ {'ROBUSTO (FULL+OOS+, regge fee 0.2%, ~tutti anni+)' if robust(res) else 'edge presente ma con anni negativi (diversificatore)'}")
|
||||
print("\n Uso: diversificatore di portafoglio (corr ~0.08 col MASTER), non motore standalone.")
|
||||
print(" Live: serve worker con retraining periodico del modello (vedi docstring).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
Reference in New Issue
Block a user