"""Strategie candidate ONESTE + sweep multi-asset/tf con verdetto. Ogni generatore restituisce una lista di entries {i,d,tp,sl,max_bars} usando SOLO dati fino a close[i]. L'engine (honest_lab.simulate) entra a close[i]. Famiglie testate (meccanismi distinti, per diversificazione): MR mean-reversion single-asset (Bollinger fade, RSI revert, Z-score) XS cross-sectional relative-value (fade della divergenza vs paniere) MOM time-series momentum / trend su timeframe alto SES seasonality (ora del giorno UTC) """ from __future__ import annotations import sys 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)) from scripts.analysis.honest_lab import ( # noqa: E402 atr, rsi, ema, get_df, simulate, oos_split, verdict, available_assets, FEE_RT, ) # ============================================================================ # MR — mean reversion single-asset # ============================================================================ def bollinger_fade(df, n=50, k=2.5, sl_atr=2.0, max_bars=24): c = df["close"].values ma = pd.Series(c).rolling(n).mean().values sd = pd.Series(c).rolling(n).std().values a = atr(df, 14) up, lo = ma + k * sd, ma - k * sd ents = [] for i in range(n + 14, len(c)): if np.isnan(up[i]) or np.isnan(a[i]): continue if c[i] < lo[i] and c[i - 1] >= lo[i - 1]: ents.append({"i": i, "d": 1, "tp": ma[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars}) elif c[i] > up[i] and c[i - 1] <= up[i - 1]: ents.append({"i": i, "d": -1, "tp": ma[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars}) return ents def rsi_revert(df, n=14, lo=25, hi=75, sl_atr=2.5, max_bars=24, ma_n=20): c = df["close"].values r = rsi(c, n) ma = pd.Series(c).rolling(ma_n).mean().values a = atr(df, 14) ents = [] for i in range(max(n, ma_n) + 1, len(c)): if np.isnan(r[i]) or np.isnan(ma[i]) or np.isnan(a[i]): continue if r[i - 1] < lo <= r[i]: ents.append({"i": i, "d": 1, "tp": ma[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars}) elif r[i - 1] > hi >= r[i]: ents.append({"i": i, "d": -1, "tp": ma[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars}) return ents def zscore_revert(df, n=50, z_in=2.5, sl_atr=2.5, max_bars=24): """Entra quando close e' a |z|>z_in std dalla media; TP alla media.""" c = df["close"].values ma = pd.Series(c).rolling(n).mean().values sd = pd.Series(c).rolling(n).std().values a = atr(df, 14) z = (c - ma) / sd ents = [] for i in range(n + 14, len(c)): if np.isnan(z[i]) or np.isnan(a[i]) or sd[i] == 0: continue if z[i] <= -z_in and z[i - 1] > -z_in: ents.append({"i": i, "d": 1, "tp": ma[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars}) elif z[i] >= z_in and z[i - 1] < z_in: ents.append({"i": i, "d": -1, "tp": ma[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars}) return ents # ============================================================================ # MOM — time-series momentum / trend (timeframe alto, niente breakout intrabar) # ============================================================================ def ema_trend(df, fast=20, slow=50, sl_atr=3.0, tp_atr=10.0, max_bars=240): """Trend following: cross EMA fast/slow deciso a close[i], TP/SL ad ATR.""" c = df["close"].values ef, es = ema(c, fast), ema(c, slow) a = atr(df, 14) ents = [] for i in range(slow + 14, len(c)): if np.isnan(a[i]): continue cross_up = ef[i] > es[i] and ef[i - 1] <= es[i - 1] cross_dn = ef[i] < es[i] and ef[i - 1] >= es[i - 1] if cross_up: ents.append({"i": i, "d": 1, "tp": c[i] + tp_atr * a[i], "sl": c[i] - sl_atr * a[i], "max_bars": max_bars}) elif cross_dn: ents.append({"i": i, "d": -1, "tp": c[i] - tp_atr * a[i], "sl": c[i] + sl_atr * a[i], "max_bars": max_bars}) return ents # ============================================================================ # SES — seasonality (ora del giorno UTC). Direzione fissa decisa solo dall'ora. # ============================================================================ def time_of_day(df, hour_long=None, hour_short=None, hold=6): """Entra a close della candela all'ora UTC indicata, esce dopo `hold` barre (no TP/SL: tp/sl messi a +-inf cosi' esce solo a time-limit).""" ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) c = df["close"].values hours = ts.dt.hour.values hour_long = set(hour_long or []) hour_short = set(hour_short or []) ents = [] for i in range(1, len(c)): if hours[i] in hour_long: ents.append({"i": i, "d": 1, "tp": np.inf, "sl": -np.inf, "max_bars": hold}) elif hours[i] in hour_short: ents.append({"i": i, "d": -1, "tp": -np.inf, "sl": np.inf, "max_bars": hold}) return ents # ============================================================================ # sweep # ============================================================================ def run_sweep(generators: dict, assets: list[str], tfs: list[str]): print("=" * 130) print(f" HONEST LAB — NETTO fee {FEE_RT*100:.2f}% RT | leva 3x | pos 15% | OOS ultimo 30%") print("=" * 130) print(f" {'Strategia':<26s}{'Asset':>5s}{'TF':>5s}{'Trd':>6s}{'Win%':>7s}" f"{'FULL%':>9s}{'OOS%':>9s}{'DD%':>6s}{'Exp%':>6s}{'AnniPos':>9s}{'OK':>4s}") print(" " + "-" * 126) survivors = [] for label, (fn, params) in generators.items(): for asset in assets: for tf in tfs: try: df = get_df(asset, tf) except Exception: continue ents = fn(df, **params) if len(ents) < 30: continue full = simulate(ents, df) _, oos_e = oos_split(ents, df) oos = simulate(oos_e, df) ok = verdict(full, oos) flag = " OK" if ok else "" print(f" {label:<26s}{asset:>5s}{tf:>5s}{full.trades:>6d}{full.win:>7.1f}" f"{full.ret:>+9.0f}{oos.ret:>+9.0f}{full.dd:>6.0f}{full.exposure:>6.0f}" f"{f'{full.pos_years}/{full.n_years}':>9s}{flag:>4s}") if ok: survivors.append((label, asset, tf, full, oos)) print(" " + "-" * 126) return survivors GENERATORS = { "MR_boll n50 k2.5": (bollinger_fade, dict(n=50, k=2.5, sl_atr=2.0, max_bars=24)), "MR_boll n20 k2.5": (bollinger_fade, dict(n=20, k=2.5, sl_atr=2.0, max_bars=24)), "MR_rsi 25/75": (rsi_revert, dict(n=14, lo=25, hi=75, sl_atr=2.5, max_bars=24)), "MR_zscore z2.5": (zscore_revert, dict(n=50, z_in=2.5, sl_atr=2.5, max_bars=24)), "MR_zscore z3": (zscore_revert, dict(n=50, z_in=3.0, sl_atr=2.5, max_bars=24)), "MOM_ema 20/50": (ema_trend, dict(fast=20, slow=50, sl_atr=3.0, tp_atr=10.0, max_bars=240)), } if __name__ == "__main__": assets = available_assets() print("Asset disponibili:", assets) survivors = run_sweep(GENERATORS, assets, ["1h", "4h"]) print(f"\n SOPRAVVISSUTI (FULL+OOS+anni+DD): {len(survivors)}") for label, a, tf, full, oos in survivors: print(f" {label:<26s} {a} {tf} FULL {full.ret:+.0f}% OOS {oos.ret:+.0f}% DD {full.dd:.0f}%")