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,152 @@
|
||||
"""Base condivisa per strategie mean-reversion con exit TP/SL/max_bars.
|
||||
|
||||
Tutte le strategie fade (MR02/MR03/MR07) generano Signal con metadata
|
||||
{tp, sl, max_bars} e usano lo stesso backtest fedele: ingresso a close[i]
|
||||
(eseguibile dal vivo), uscita su take-profit / stop-loss intrabar (high/low)
|
||||
o time-limit, una posizione per volta (non-overlap), capitale composto,
|
||||
fee+leva nette. Identico all'engine di scripts/analysis/strategy_research.py.
|
||||
|
||||
Le sottoclassi implementano solo generate_signals().
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.strategies.base import Strategy, BacktestResult, YearlyStats, TF_MINUTES
|
||||
from src.data.downloader import load_data
|
||||
from src.fractal.indicators import rolling_hurst
|
||||
|
||||
|
||||
def hurst_skip_mask(df: pd.DataFrame, hurst_max: float | None, window: int = 100,
|
||||
step: int = 6) -> np.ndarray:
|
||||
"""Loss-guard Hurst: maschera bool (True = SALTA il segnale) per regime PERSISTENTE/trending,
|
||||
dove la rolling-Hurst >= hurst_max. Le fade concentrano stop-loss e perdite proprio li'
|
||||
(diagnosi: stop-rate 43% per hurst>0.55 vs 21% anti-persistente). Filtrare hurst>=0.55
|
||||
DIMEZZA il DD del PORT06 (FULL 4.10%->2.39%) alzando lo Sharpe (validato 2026-06-02).
|
||||
CAUSALE: rolling_hurst usa solo i rendimenti fino a close[i]. hurst_max=None -> nessuno skip.
|
||||
Calcolata dalle SOLE close -> nessun feed dati esterno necessario (a differenza di DVOL).
|
||||
step=6: calcola l'Hurst ogni 6 barre (ffill) -> ~6x piu' veloce per il worker live su finestre
|
||||
lunghe (440g/10560 barre), e coincide con la cache di validazione (frac_step=6). L'Hurst varia
|
||||
lentamente -> differenza trascurabile vs step=1."""
|
||||
n = len(df)
|
||||
if hurst_max is None:
|
||||
return np.zeros(n, dtype=bool)
|
||||
h = rolling_hurst(df["close"].values.astype(float), window=window, step=step)
|
||||
return h >= hurst_max
|
||||
|
||||
|
||||
def atr(df: pd.DataFrame, n: int = 14) -> np.ndarray:
|
||||
h, l, c = df["high"].values, df["low"].values, df["close"].values
|
||||
pc = np.roll(c, 1); pc[0] = c[0]
|
||||
tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
|
||||
return pd.Series(tr).rolling(n).mean().values
|
||||
|
||||
|
||||
def trend_distance(df: pd.DataFrame, ema_long: int = 200) -> np.ndarray:
|
||||
"""Distanza del close dalla EMA lunga, in multipli di ATR(14).
|
||||
|
||||
Misura quanto il prezzo e' esteso rispetto al trend di fondo. Le fade
|
||||
falliscono quando si oppongono a un trend estremo (crolli/parabolic): il
|
||||
filtro `trend_max` salta i segnali con distanza > soglia. Riduce DD e alza
|
||||
l'accuratezza (validato OOS: scripts/analysis/risk_portfolio.py).
|
||||
"""
|
||||
c = df["close"].values
|
||||
a = atr(df, 14)
|
||||
el = pd.Series(c).ewm(span=ema_long, adjust=False).mean().values
|
||||
with np.errstate(divide="ignore", invalid="ignore"):
|
||||
return np.abs(c - el) / np.where(a == 0, np.nan, a)
|
||||
|
||||
|
||||
class FadeStrategy(Strategy):
|
||||
"""Strategy con backtest intrabar TP/SL/max_bars (exit guidati dai metadata)."""
|
||||
|
||||
fee_rt = 0.001 # Deribit perp realistico (taker 0.05%/lato)
|
||||
leverage = 3.0
|
||||
position_size = 0.15
|
||||
initial_capital = 1000.0
|
||||
|
||||
def backtest(self, asset: str, tf: str = "1h", hold: int = 3,
|
||||
**params) -> BacktestResult | None:
|
||||
df = load_data(asset, tf)
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
signals = self.generate_signals(df, ts, **params)
|
||||
if not signals:
|
||||
return None
|
||||
|
||||
# EXIT-16 close-confirm SL (2026-06-04): se settato, lo SL intrabar e'
|
||||
# DISATTIVATO e lo stop scatta solo se il CLOSE sfonda sl ∓ buf*ATR14
|
||||
# (immune ai wick: l'overshoot che buca lo stop e rientra e' esattamente
|
||||
# il movimento che la fade vuole fadare). TP intrabar e max_bars invariati.
|
||||
# PORT06: FULL Sharpe 6.47->7.84 DD 4.10->2.60, OOS 8.82->10.06 (exit-lab,
|
||||
# 34 agenti). None = comportamento storico. Diario 2026-06-04-exit-lab.md.
|
||||
sl_confirm = params.get("sl_confirm_atr")
|
||||
a14 = atr(df, 14) if sl_confirm else None
|
||||
|
||||
h, l, c = df["high"].values, df["low"].values, df["close"].values
|
||||
n = len(c)
|
||||
fee = self.fee_rt * self.leverage
|
||||
capital = peak = float(self.initial_capital)
|
||||
max_dd = 0.0
|
||||
total_bars = 0
|
||||
last_exit = -1
|
||||
yearly: dict[int, dict] = {}
|
||||
|
||||
for sig in signals:
|
||||
i, d = sig.idx, sig.direction
|
||||
if i <= last_exit or i + 1 >= n:
|
||||
continue
|
||||
entry = c[i]
|
||||
tp, sl, mb = sig.metadata["tp"], sig.metadata["sl"], sig.metadata["max_bars"]
|
||||
exit_p = c[min(i + mb, n - 1)]
|
||||
j = min(i + mb, n - 1)
|
||||
for step in range(1, mb + 1):
|
||||
j = i + step
|
||||
if j >= n:
|
||||
j = n - 1; exit_p = c[j]; break
|
||||
hit_tp = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp)
|
||||
if sl_confirm is None:
|
||||
hit_sl = (d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl)
|
||||
if hit_sl: # conservativo: SL prima del TP nello stesso bar
|
||||
exit_p = sl; break
|
||||
if hit_tp:
|
||||
exit_p = tp; break
|
||||
else:
|
||||
# close-confirm: TP intrabar al livello; SL valutato sul CLOSE
|
||||
if hit_tp:
|
||||
exit_p = tp; break
|
||||
buf = sl_confirm * a14[j]
|
||||
if (d == 1 and c[j] < sl - buf) or (d == -1 and c[j] > sl + buf):
|
||||
exit_p = c[j]; break
|
||||
if step == mb:
|
||||
exit_p = c[j]
|
||||
|
||||
ret = (exit_p - entry) / entry * d * self.leverage - fee
|
||||
capital = max(capital + capital * self.position_size * ret, 10.0)
|
||||
if capital > peak:
|
||||
peak = capital
|
||||
max_dd = max(max_dd, (peak - capital) / peak)
|
||||
total_bars += (j - i)
|
||||
last_exit = j
|
||||
|
||||
year = ts.iloc[i].year
|
||||
yr = yearly.setdefault(year, {"w": 0, "t": 0, "pnl": 0.0})
|
||||
yr["t"] += 1
|
||||
if ret > 0:
|
||||
yr["w"] += 1
|
||||
yr["pnl"] += ret * self.initial_capital
|
||||
|
||||
all_t = sum(v["t"] for v in yearly.values())
|
||||
all_w = sum(v["w"] for v in yearly.values())
|
||||
if all_t == 0:
|
||||
return None
|
||||
|
||||
yearly_stats = [YearlyStats(y, v["t"], v["w"], v["pnl"]) for y, v in sorted(yearly.items())]
|
||||
return BacktestResult(
|
||||
strategy_name=self.name, asset=asset, timeframe=tf, params=params,
|
||||
trades=all_t, wins=all_w, pnl=sum(v["pnl"] for v in yearly.values()),
|
||||
capital=capital, initial_capital=self.initial_capital,
|
||||
max_dd=max_dd * 100, time_in_market_pct=total_bars / n * 100,
|
||||
avg_trade_duration_h=total_bars / all_t * TF_MINUTES.get(tf, 60) / 60,
|
||||
years_active=len(yearly), yearly=yearly_stats,
|
||||
)
|
||||
Reference in New Issue
Block a user