"""Affina il filtro trend (soglia assoluta ATR) e costruisce il portafoglio combinato. Due risultati: (1) trend filter: salta le fade quando |close-EMA200|/ATR > soglia (non fadare un trend estremo). Soglia ASSOLUTA in multipli di ATR -> stessa regola per tutte le strategie/asset, basso rischio di overfit. Sweep soglie, FULL e OOS. (2) portafoglio: equity curve combinata delle 4 strategie sullo stesso conto (rischio diviso fra N posizioni). Curve poco correlate -> DD aggregato << DD della singola strategia. Confronto singola vs portafoglio, con/senza filtro. """ 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 src.data.downloader import load_data from scripts.analysis.strategy_research import bollinger_fade, atr from scripts.analysis.strategy_research_v2 import donchian_fade, keltner_fade, return_reversal FEE_RT, LEV, INIT, OOS_FRAC = 0.001, 3.0, 1000.0, 0.30 STRATS = { "MR01": (bollinger_fade, dict(n=50, k=2.5, sl_atr=2.0, max_bars=24)), "MR02": (donchian_fade, dict(n=20, sl_atr=2.0, max_bars=24)), "MR03": (keltner_fade, dict(n=30, k=2.0, sl_atr=2.0, max_bars=24)), "MR07": (return_reversal,dict(n=50, k=3.5, tp_atr=2.0, sl_atr=1.5, max_bars=24)), } STRATS_ETH = dict(STRATS); STRATS_ETH["MR03"] = (keltner_fade, dict(n=50, k=2.0, sl_atr=2.0, max_bars=24)) def build_trades(ents, df, lev=LEV, fee_rt=FEE_RT, trend_max=None, ema_long=200): """Ritorna lista trade non-overlap: (entry_idx, exit_idx, ret_netto). Filtro trend opzionale.""" h, l, c = df["high"].values, df["low"].values, df["close"].values n = len(c); a = atr(df, 14) el = pd.Series(c).ewm(span=ema_long, adjust=False).mean().values fee = fee_rt * lev out = []; last = -1 for e in ents: i, d = e["i"], e["d"] if i <= last or i + 1 >= n: continue if trend_max is not None and a[i] and abs(c[i] - el[i]) / a[i] > trend_max: continue entry = c[i]; tp, sl, mb = e["tp"], e["sl"], e["max_bars"] exit_p = c[min(i + mb, n - 1)]; j = min(i + mb, n - 1) for k in range(1, mb + 1): j = i + k if j >= n: exit_p = c[n - 1]; break hs = (d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl) ht = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp) if hs: exit_p = sl; break if ht: exit_p = tp; break if k == mb: exit_p = c[j] ret = (exit_p - entry) / entry * d * lev - fee out.append((i, j, ret)); last = j return out def metrics_single(trades, ts, pos=0.15, split=-1): cap = peak = INIT; dd = 0.0; trd = wins = 0; rets = [] for i, j, ret in trades: if i < split: continue cap = max(cap + cap * pos * ret, 10.0) peak = max(peak, cap); dd = max(dd, (peak - cap) / peak) trd += 1; wins += ret > 0; rets.append(ret * pos) sh = float(np.mean(rets) / np.std(rets) * np.sqrt(len(rets))) if len(rets) > 1 and np.std(rets) > 0 else 0.0 return dict(trades=trd, acc=wins / trd * 100 if trd else 0.0, ret=(cap / INIT - 1) * 100, dd=dd * 100, sharpe=sh) def sleeve_equity(trades, n_bars, pos=0.15, split=-1): """Equity curve di uno sleeve su sotto-conto indipendente (capitale INIT, pos fissa). Ritorna array lungo n_bars (step aggiornato alla chiusura di ogni trade).""" eq = np.full(n_bars, INIT, dtype=float) cap = INIT for i, j, ret in sorted(trades, key=lambda t: t[1]): if i < split: continue cap = max(cap + cap * pos * ret, 10.0) eq[j:] = cap # da j in poi il sotto-conto vale cap return eq def metrics_portfolio(strat_trades, n_bars, ts, pos=0.15, split=-1): """Portafoglio equipesato: capitale diviso in N sotto-conti indipendenti, ciascuno con la sua strategia a `pos` fisso. Equity aggregata = media dei sotto-conti (somma normalizzata a base INIT). DD misurato sull'equity aggregata. Niente leva sovrapposta.""" sleeves = [sleeve_equity(tr, n_bars, pos=pos, split=split) for tr in strat_trades.values()] agg = np.mean(sleeves, axis=0) # media -> base INIT, diversificazione reale # restringi alla finestra effettiva (da split in poi se OOS) lo = max(split, 0) agg = agg[lo:] peak = np.maximum.accumulate(agg) dd = float(np.max((peak - agg) / peak) * 100) trd = sum(1 for tr in strat_trades.values() for i, _, _ in tr if i >= split) wins = sum(1 for tr in strat_trades.values() for i, _, r in tr if i >= split and r > 0) return dict(trades=trd, acc=wins / trd * 100 if trd else 0.0, ret=(agg[-1] / INIT - 1) * 100, dd=dd, sharpe=0.0) def main(): # ---------- (1) sweep soglia trend ---------- print("=" * 104) print(" (1) FILTRO TREND |close-EMA200|/ATR > soglia -> SALTA | NETTO fee 0.10% RT, leva 3x") print("=" * 104) print(f" {'Strat/Asset':<14s}{'soglia':>8s}{'Trd':>6s}{'Acc%':>7s}{'Ret%':>9s}{'DD%':>7s}" f" | {'oAcc':>6s}{'oRet':>9s}{'oDD':>7s}{'oShrp':>7s}") print(" " + "-" * 100) thresholds = [None, 4.0, 3.0, 2.5, 2.0] for asset in ["BTC", "ETH"]: df = load_data(asset, "1h"); ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) split = int(len(df) * (1 - OOS_FRAC)) table = STRATS_ETH if asset == "ETH" else STRATS for nm, (fn, params) in table.items(): ents = fn(df, **params) for thr in thresholds: tr = build_trades(ents, df, trend_max=thr) f = metrics_single(tr, ts); o = metrics_single(tr, ts, split=split) lab = "base" if thr is None else f"{thr}ATR" print(f" {nm+' '+asset:<14s}{lab:>8s}{f['trades']:>6d}{f['acc']:>7.1f}{f['ret']:>+9.0f}{f['dd']:>7.1f}" f" | {o['acc']:>6.1f}{o['ret']:>+9.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f}") print(" " + "-" * 100) # ---------- (2) portafoglio combinato ---------- print("\n" + "=" * 104) print(" (2) PORTAFOGLIO equipesato: capitale diviso in N sotto-conti indipendenti") print(" (pos 0.15 ciascuno, filtro trend 3.0 ATR). DD aggregato vs singola strategia.") print("=" * 104) print(f" {'Universo':<26s}{'Trd':>6s}{'Acc%':>7s}{'Ret%':>10s}{'DD%':>7s}{'':>7s}" f" | {'oAcc':>6s}{'oRet':>9s}{'oDD':>7s}{'':>7s}") print(" " + "-" * 100) all_trades = {} for asset in ["BTC", "ETH"]: df = load_data(asset, "1h"); ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) split = int(len(df) * (1 - OOS_FRAC)); n = len(df) table = STRATS_ETH if asset == "ETH" else STRATS st = {f"{nm}_{asset}": build_trades(fn(df, **p), df, trend_max=3.0) for nm, (fn, p) in table.items()} all_trades.update(st) f = metrics_portfolio(st, n, ts); o = metrics_portfolio(st, n, ts, split=split) print(f" {'Portafoglio '+asset+' (4 strat)':<26s}{f['trades']:>6d}{f['acc']:>7.1f}{f['ret']:>+10.0f}{f['dd']:>7.1f}{f['sharpe']:>7.2f}" f" | {o['acc']:>6.1f}{o['ret']:>+9.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f}") # globale 8 sleeve df0 = load_data("BTC", "1h"); ts0 = pd.to_datetime(df0["timestamp"], unit="ms", utc=True) split0 = int(len(df0) * (1 - OOS_FRAC)) f = metrics_portfolio(all_trades, len(df0), ts0); o = metrics_portfolio(all_trades, len(df0), ts0, split=split0) print(" " + "-" * 100) print(f" {'GLOBALE BTC+ETH (8 sleeve)':<26s}{f['trades']:>6d}{f['acc']:>7.1f}{f['ret']:>+10.0f}{f['dd']:>7.1f}{f['sharpe']:>7.2f}" f" | {o['acc']:>6.1f}{o['ret']:>+9.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f}") print("\n Nota: ogni sleeve gira su un sotto-conto indipendente (pos 0.15); l'equity di") print(" portafoglio e' la media dei sotto-conti. Curve poco correlate => DD aggregato") print(" molto piu' basso del DD del singolo sleeve (la vera leva anti-drawdown).") if __name__ == "__main__": main()