chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera libreria "validata OOS" era artefatto di feed contaminato (print fantasma del feed Cerbero TESTNET + storico Binance/USDT). - Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE 50-82% barre flat; XRP/BNB non certificabili). - Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST con segnale residuo, da ri-validare in isolamento. - Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio, runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/ portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/ (preservati, non cancellati). Diario consolidato in un unico documento. - Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal + src/backtest/engine + load_data; tool dati certificati (rebuild_history, certify_feed, audit_feed, multi_source_check). - Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
"""Gestione del rischio sulle fade (MR01/MR02/MR03/MR07): alzare Acc, ridurre DD.
|
||||
|
||||
Due analisi, ognuna misurata FULL e OOS (ultimo 30%) per non illudersi:
|
||||
|
||||
(A) SCREENING LEVE — confronta su ogni strategia le leve di rischio:
|
||||
- vol-target sizing (size ~ 1/distanza-SL) -> SCARTATA (peggiora)
|
||||
- skip alta volatilita' (ATR% in coda alta) -> SCARTATA (peggiora)
|
||||
- filtro trend (|close-EMA200|/ATR oltre soglia) -> ADOTTATA (Acc+ DD-)
|
||||
- combinazione di tutte
|
||||
|
||||
(B) FILTRO TREND + PORTAFOGLIO:
|
||||
- sweep della soglia trend (assoluta in ATR, regola unica = no overfit)
|
||||
- portafoglio equipesato su sotto-conti indipendenti: curve poco correlate
|
||||
-> DD aggregato << DD del singolo sleeve (vera leva anti-drawdown)
|
||||
|
||||
Engine fedele: ingresso close[i], exit TP/SL intrabar (high/low) o time-limit,
|
||||
non-overlap, capitale composto. Numeri NETTI fee 0.10% RT, leva 3x.
|
||||
"""
|
||||
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, return_reversal
|
||||
|
||||
FEE_RT, LEV, POS, INIT, OOS_FRAC = 0.001, 3.0, 0.15, 1000.0, 0.30
|
||||
|
||||
# config base di ogni strategia (come strategies.yml).
|
||||
# NB: MR03 keltner_fade spostata in scripts/waste/ (fade piu' debole, ridondante
|
||||
# con MR01); la funzione keltner_fade resta in strategy_research_v2 come record.
|
||||
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)),
|
||||
"MR07": (return_reversal,dict(n=50, k=3.5, tp_atr=2.0, sl_atr=1.5, max_bars=24)),
|
||||
}
|
||||
STRATS_ETH = dict(STRATS)
|
||||
|
||||
|
||||
def strats_for(asset: str) -> dict:
|
||||
return STRATS_ETH if asset == "ETH" else STRATS
|
||||
|
||||
|
||||
# ============================ (A) SCREENING LEVE ============================
|
||||
def add_context(ents, df, ema_long=200):
|
||||
"""Aggiunge a ogni entry: sl_dist, atr_pct, trend_dist (|close-EMA|/ATR)."""
|
||||
c = df["close"].values
|
||||
a = atr(df, 14)
|
||||
el = pd.Series(c).ewm(span=ema_long, adjust=False).mean().values
|
||||
apct = a / c
|
||||
for e in ents:
|
||||
i = e["i"]
|
||||
e["sl_dist"] = abs(c[i] - e["sl"]) / c[i]
|
||||
e["atr_pct"] = apct[i]
|
||||
e["trend_dist"] = abs(c[i] - el[i]) / a[i] if a[i] else 0.0
|
||||
return ents
|
||||
|
||||
|
||||
def simulate(ents, df, fee_rt=FEE_RT, lev=LEV, split=-1,
|
||||
sizer=None, vol_skip=None, trend_skip=None, max_size=0.30):
|
||||
"""sizer: funzione(entry)->frazione capitale; default POS fisso.
|
||||
vol_skip: soglia atr_pct sopra cui salto. trend_skip: soglia trend_dist sopra cui salto."""
|
||||
h, l, c = df["high"].values, df["low"].values, df["close"].values
|
||||
n = len(c)
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
cap = peak = INIT
|
||||
dd = 0.0; last = -1; trd = wins = 0
|
||||
fee = fee_rt * lev
|
||||
yearly = {}; rets = []
|
||||
for e in ents:
|
||||
i, d = e["i"], e["d"]
|
||||
if i <= last or i + 1 >= n or i < split:
|
||||
continue
|
||||
if vol_skip is not None and e["atr_pct"] > vol_skip:
|
||||
continue
|
||||
if trend_skip is not None and e["trend_dist"] > trend_skip:
|
||||
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
|
||||
size = POS if sizer is None else min(sizer(e), max_size)
|
||||
cap = max(cap + cap * size * ret, 10.0)
|
||||
peak = max(peak, cap); dd = max(dd, (peak - cap) / peak)
|
||||
trd += 1; wins += ret > 0; last = j; rets.append(ret * size)
|
||||
y = ts.iloc[i].year; yearly[y] = yearly.get(y, 0.0) + ret * size * INIT
|
||||
sharpe = 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, yearly=yearly, sharpe=sharpe)
|
||||
|
||||
|
||||
def vol_target_sizer(target=0.015):
|
||||
"""size t.c. rischio (size*lev*sl_dist) ~ target; piu' largo lo stop, meno size."""
|
||||
return lambda e: target / (LEV * max(e["sl_dist"], 1e-4))
|
||||
|
||||
|
||||
def _line(label, full, oos):
|
||||
print(f" {label:<28s}{full['trades']:>6d}{full['acc']:>7.1f}{full['ret']:>+10.0f}{full['dd']:>7.1f}{full['sharpe']:>7.2f}"
|
||||
f" | {oos['trades']:>5d}{oos['acc']:>7.1f}{oos['ret']:>+9.0f}{oos['dd']:>7.1f}{oos['sharpe']:>7.2f}")
|
||||
|
||||
|
||||
def screen_levers():
|
||||
print("=" * 110)
|
||||
print(" (A) SCREENING LEVE — vol-target / vol-skip / filtro-trend | NETTO fee 0.10% RT, leva 3x")
|
||||
print("=" * 110)
|
||||
for asset in ["BTC", "ETH"]:
|
||||
df = load_data(asset, "1h")
|
||||
split = int(len(df) * (1 - OOS_FRAC))
|
||||
print(f"\n {asset} 1h")
|
||||
print(f" {'config':<28s}{'Trd':>6s}{'Acc%':>7s}{'Ret%':>10s}{'DD%':>7s}{'Shrp':>7s}"
|
||||
f" | {'oTrd':>5s}{'oAcc':>7s}{'oRet':>9s}{'oDD':>7s}{'oShrp':>7s}")
|
||||
print(" " + "-" * 106)
|
||||
for nm, (fn, params) in strats_for(asset).items():
|
||||
ents = add_context(fn(df, **params), df)
|
||||
p85 = float(np.quantile([e["atr_pct"] for e in ents], 0.85))
|
||||
t90 = float(np.quantile([e["trend_dist"] for e in ents], 0.90))
|
||||
_line(f"{nm} base", simulate(ents, df), simulate(ents, df, split=split))
|
||||
_line(f"{nm} +volTarget", simulate(ents, df, sizer=vol_target_sizer()),
|
||||
simulate(ents, df, split=split, sizer=vol_target_sizer()))
|
||||
_line(f"{nm} +volSkip(p85)", simulate(ents, df, vol_skip=p85),
|
||||
simulate(ents, df, split=split, vol_skip=p85))
|
||||
_line(f"{nm} +trendSkip(p90)", simulate(ents, df, trend_skip=t90),
|
||||
simulate(ents, df, split=split, trend_skip=t90))
|
||||
_line(f"{nm} +ALL", simulate(ents, df, sizer=vol_target_sizer(), vol_skip=p85, trend_skip=t90),
|
||||
simulate(ents, df, split=split, sizer=vol_target_sizer(), vol_skip=p85, trend_skip=t90))
|
||||
print(" " + "-" * 106)
|
||||
print("\n Esito: vol-target e vol-skip PEGGIORANO; il filtro trend e' l'unica leva utile.")
|
||||
|
||||
|
||||
# ===================== (B) FILTRO TREND + PORTAFOGLIO =====================
|
||||
def build_trades(ents, df, lev=LEV, fee_rt=FEE_RT, trend_max=None, ema_long=200):
|
||||
"""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, pos=POS, 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=POS, split=-1):
|
||||
"""Equity di uno sleeve su sotto-conto indipendente (capitale INIT, pos fissa)."""
|
||||
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
|
||||
return eq
|
||||
|
||||
|
||||
def metrics_portfolio(strat_trades, n_bars, pos=POS, split=-1):
|
||||
"""Portafoglio equipesato: media di N sotto-conti indipendenti. DD sull'aggregata."""
|
||||
sleeves = [sleeve_equity(tr, n_bars, pos=pos, split=split) for tr in strat_trades.values()]
|
||||
agg = np.mean(sleeves, axis=0)
|
||||
agg = agg[max(split, 0):]
|
||||
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)
|
||||
|
||||
|
||||
def trend_and_portfolio():
|
||||
# --- sweep soglia trend ---
|
||||
print("\n" + "=" * 104)
|
||||
print(" (B1) 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)
|
||||
for asset in ["BTC", "ETH"]:
|
||||
df = load_data(asset, "1h"); split = int(len(df) * (1 - OOS_FRAC))
|
||||
for nm, (fn, params) in strats_for(asset).items():
|
||||
ents = fn(df, **params)
|
||||
for thr in [None, 4.0, 3.0, 2.5, 2.0]:
|
||||
tr = build_trades(ents, df, trend_max=thr)
|
||||
f = metrics_single(tr); o = metrics_single(tr, 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)
|
||||
|
||||
# --- portafoglio equipesato (filtro trend 3.0 ATR) ---
|
||||
print("\n" + "=" * 104)
|
||||
print(" (B2) PORTAFOGLIO equipesato: N sotto-conti indipendenti (pos 0.15, filtro trend 3.0 ATR)")
|
||||
print("=" * 104)
|
||||
print(f" {'Universo':<26s}{'Trd':>6s}{'Acc%':>7s}{'Ret%':>10s}{'DD%':>7s}"
|
||||
f" | {'oAcc':>6s}{'oRet':>9s}{'oDD':>7s}")
|
||||
print(" " + "-" * 100)
|
||||
all_trades = {}
|
||||
for asset in ["BTC", "ETH"]:
|
||||
df = load_data(asset, "1h"); split = int(len(df) * (1 - OOS_FRAC)); n = len(df)
|
||||
st = {f"{nm}_{asset}": build_trades(fn(df, **p), df, trend_max=3.0) for nm, (fn, p) in strats_for(asset).items()}
|
||||
all_trades.update(st)
|
||||
f = metrics_portfolio(st, n); o = metrics_portfolio(st, n, 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" | {o['acc']:>6.1f}{o['ret']:>+9.0f}{o['dd']:>7.1f}")
|
||||
df0 = load_data("BTC", "1h"); split0 = int(len(df0) * (1 - OOS_FRAC))
|
||||
f = metrics_portfolio(all_trades, len(df0)); o = metrics_portfolio(all_trades, len(df0), 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" | {o['acc']:>6.1f}{o['ret']:>+9.0f}{o['dd']:>7.1f}")
|
||||
print("\n Curve poco correlate => DD aggregato molto piu' basso del singolo sleeve.")
|
||||
|
||||
|
||||
def main():
|
||||
screen_levers()
|
||||
trend_and_portfolio()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user