diff --git a/CLAUDE.md b/CLAUDE.md index 3bf15e0..b7421ae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -183,6 +183,26 @@ equal-weight, leva 2x, cap pairs ~30-35%** (i pairs sono ~57% del rischio; worke 2 gambe implementato, validato e con feed live su tutte e 5 le coppie — resta da verificare liquidità/fill in esecuzione reale). La confluenza multi-TF è stata SCARTATA (overfit). +**Pattern del segnale per FORMA (branch `shape_patterns`, 2026-05-29).** Esplorate 5 famiglie +di *shape forecasting* con agenti paralleli su harness onesto (`scripts/analysis/shape_lab.py`: +analog kNN causale, no-look-ahead verificato). **4/5 sono RUMORE** (riconfermano la dominanza +mean-reversion): analog kNN sulla forma grezza (solo BTC-overfit), encoding candele +UP/DOWN/DOJI+body/shadow (hit-rate ~50%), DTW+template geometrici (DTW *peggiora* l'euclidea; +template overfit), PIP/pivot/zig-zag (0/48 robuste). Vedi `scripts/analysis/shape_*_research.py`. +- **SH01 Shape-ML** (`scripts/strategies/SH01_shape_ml.py`): UNICO edge. Una LogisticRegression + legge 17 feature di forma (body/shadow, rendimenti, pendenza/curvatura, pos max/min, RSI, + estensione) e predice il segno del rendimento a H barre in **walk-forward** (scaler+modello + solo sul passato, no leakage). Config **W24 H12 th0.58**. A differenza dello squeeze + **regge fee 0.20% RT**. Win-rate ~50% → l'edge è nell'**asimmetria**, non nella frequenza. + Validazione (`scripts/analysis/shape_ml_validate.py`): BTC robusto OVUNQUE (expanding +219%/ + OOS +42% Sharpe 2.72 8-9 anni; rolling 2y +166%/+96%; stress 2x+slippage OK), ETH/ADA + robusti solo expanding (secondari), LTC/SOL/XRP scartati. Griglia: **5/27 celle robuste su + cresta stretta W24/H8-12** → overfit moderato, scelta la config conservativa. **Valore vero: + diversificatore** (corr +0.08 col MASTER); aggiungerlo migliora l'OOS del MASTER (Sharpe + 4.33→5.10, DD 4.7%→4.2%). NON motore standalone. **LIVE: serve worker con retraining + periodico** (lo StrategyWorker è a regola fissa) → in `MODULE_MAP` ma non ancora in + `strategies.yml`. Diario: `docs/diary/2026-05-29-shape.md`. + **Metodologia obbligatoria per ogni nuova strategia** (per non ripetere l'errore squeeze): 1. Ingresso eseguibile: direzione e prezzo decisi con dati **fino a `close[i]`**, mai `close[i-1]` con direzione da `i`. 2. Backtest **NETTO** dopo fee realistiche Deribit (**0.10% RT** taker, non 0.20%) + leva. diff --git a/docs/diary/2026-05-29-shape.md b/docs/diary/2026-05-29-shape.md new file mode 100644 index 0000000..280033d --- /dev/null +++ b/docs/diary/2026-05-29-shape.md @@ -0,0 +1,84 @@ +# Diario — 2026-05-29 — Pattern del segnale per FORMA (analog/shape forecasting) + +## Obiettivo + +Verificare se la **forma** del segnale (la morfologia recente del prezzo) permette di +prevedere l'andamento successivo, e ricavarne edge verso il target €1000 → €50/giorno. +Esplorazione onesta (no look-ahead, netto fee, OOS) con **agenti paralleli**, ognuno su +una famiglia di forma indipendente, tutti sullo stesso harness shape (`scripts/analysis/ +shape_lab.py`, che riusa l'engine netto-fee+OOS di `explore_lab.py`). Branch `shape_patterns`. + +## Harness + +`shape_lab.py` — analog forecasting causale: a ogni barra `i` si guarda la forma recente +`W` (closes z-normalizzati fino a `close[i]`), si cercano nel passato le `K` finestre più +simili **il cui esito a `H` barre era già noto prima di `i`** (KDTree ricostruito ogni +`rebuild` barre → niente O(N²)), si prevede la direzione = segno del rendimento medio degli +analoghi. **No-look-ahead verificato** (perturbare il futuro non cambia la forma a `i`, +max diff 0.0). Baseline forma grezza: marginale e **muore sulle fee** (W24H12K50: FULL ++112% / OOS +48% ma a 0.20% RT → −72%; troppi trade, exp 74%, win 49.5%). + +## Famiglie esplorate (5) ed esito onesto + +| Famiglia | Esito | Note | +|---|---|---| +| Analog kNN (forma grezza, selettività) | ❌ RUMORE | Solo BTC-overfit, non robusto ≥2 asset | +| Encoding candele (UP/DOWN/DOJI + body/shadow) | ❌ RUMORE | Hit-rate condizionale ~50%, segno incoerente fra asset | +| DTW + template geometrici (M/W, testa-spalle, V, U) | ❌ RUMORE | DTW *peggiora* l'euclidea; template overfit (FULL ok, OOS crolla) | +| PIP / pivot / zig-zag (geometria svolte) | ❌ RUMORE | 0/48 config robuste; le rotture S/R rientrano (riconferma MR) | +| **Feature-vector + ML walk-forward** | ✅ **EDGE REALE** | LogisticRegression sulla forma, fee-robusto | + +4 famiglie su 5 sono rumore: riconfermano che la forma grezza non contiene edge +direzionale eseguibile e che l'unico edge "classico" resta la mean-reversion (fade/pairs). + +## L'edge: SH01 — Shape-ML + +Una **LogisticRegression** legge 17 feature di forma (body/shadow ratio, rendimenti, +pendenza/curvatura del path, posizione di max/min, RSI, estensione) e predice il segno del +rendimento a `H` barre. **Walk-forward rigoroso**: scaler+modello fittati solo sul passato +con esito noto, poi predicono il blocco corrente; si entra a `close[i]` se la probabilità +≥ soglia. Causalità verificata con check espliciti (feature e predizioni invarianti al +futuro). Il GradientBoosting dà edge equivalente ma è ~60× più lento → si usa il logit. + +A differenza della famiglia squeeze (che moriva anche a fee zero), **questo edge +sopravvive a fee 0.20% RT**. Win-rate ~50% → l'edge è nell'**asimmetria** (quando indovina +la direzione i moti sono più grandi), non nella frequenza. + +### Validazione dura (config W24 H12 th0.58, netto fee, leva 3x, pos 0.15, OOS 30%) + +- **Multi-asset expanding**: robusti **BTC** (FULL +219% / OOS +42% / Sharpe 2.72 / DD 23% + / 8-9 anni+ / accOOS 56%), **ETH** (+80% / +144% / Sharpe 1.21, più volatile), **ADA** + (+707% / +57% / Sharpe 3.22). Scartati LTC/SOL/XRP (perdono netti). +- **Walk-forward rolling (train fisso 2 anni)**: regge **solo BTC** (+166% / +96% / Sharpe + 2.05). L'edge si appoggia in parte alla memoria lunga → BTC è il più solido. +- **Stress leva 2x + slippage doppio (0.20% RT)**: BTC OK (+40% / +17% / Sharpe 1.24), + ETH marginale (+7% / +73% / Sharpe 0.37). +- **Griglia (W,H,thresh) su BTC**: **5/27 celle robuste**, su una **cresta** stretta (W24, + H8-12), non altopiano largo → rischio overfit moderato. Per prudenza si sceglie la config + robusta sul maggior numero di test (W24 H12 th0.58), non il PnL massimo (W24 H8 rende di + più ma accOOS ~49% = più drift che segnale). + +### Il valore vero: diversificatore di portafoglio + +Correlazione 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%** (FULL: Sharpe +4.23 → 4.37, DD 5.2% → 4.3%). Non è un motore standalone (per-asset troppo stretto fuori +da BTC), ma un **free-lunch** da aggiungere al paniere. + +## Artefatti + +- `scripts/analysis/shape_lab.py` — harness analog/forma causale. +- `scripts/analysis/shape_{analog,candle,template,pivot,ml}_research.py` — le 5 ricerche. +- `scripts/analysis/shape_ml_validate.py` — validazione dura del candidato ML. +- `scripts/strategies/SH01_shape_ml.py` — la strategia (Strategy + run() riproducibile). +- Aggiunta a `MODULE_MAP` (caricabile per backtest). + +## Conclusione e prossimi passi + +La forma del segnale **non** predice in modo grezzo (4/5 famiglie rumore), ma un modello +lineare sulle feature di forma in walk-forward onesto **sì**, soprattutto su BTC, e vale +come diversificatore quasi-scorrelato del MASTER. Da fare prima del live: +1. **Worker con retraining periodico** (lo StrategyWorker attuale è a regola fissa; SH01 + riallena il modello → serve un loop tipo legacy signal_engine). +2. Validazione live-path (replay worker == backtest) come fatto per i pairs. +3. Decidere il peso nel MASTER-esteso (cap, leva) col paper trader. diff --git a/scripts/analysis/shape_ml_research.py b/scripts/analysis/shape_ml_research.py index 05fc065..516e637 100644 --- a/scripts/analysis/shape_ml_research.py +++ b/scripts/analysis/shape_ml_research.py @@ -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 diff --git a/scripts/analysis/shape_ml_validate.py b/scripts/analysis/shape_ml_validate.py new file mode 100644 index 0000000..3e56957 --- /dev/null +++ b/scripts/analysis/shape_ml_validate.py @@ -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() diff --git a/scripts/strategies/SH01_shape_ml.py b/scripts/strategies/SH01_shape_ml.py new file mode 100644 index 0000000..3ad38e1 --- /dev/null +++ b/scripts/strategies/SH01_shape_ml.py @@ -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() diff --git a/src/live/strategy_loader.py b/src/live/strategy_loader.py index e6ae01c..dbdb522 100644 --- a/src/live/strategy_loader.py +++ b/src/live/strategy_loader.py @@ -20,6 +20,10 @@ MODULE_MAP = { "MR01_bollinger_fade": ("MR01_bollinger_fade", "BollingerFade"), "MR02_donchian_fade": ("MR02_donchian_fade", "DonchianFade"), "MR07_return_reversal": ("MR07_return_reversal", "ReturnReversal"), + # SH01 Shape-ML: generate_signals fa walk-forward (riallena il modello) -> pesante + # per-tick. Caricabile per backtest; per il live serve un worker con retraining + # periodico (come il legacy signal_engine), NON lo StrategyWorker a regola fissa. + "SH01_shape_ml": ("SH01_shape_ml", "ShapeMLStrategy"), }