14522262e6
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>
97 lines
3.9 KiB
Python
97 lines
3.9 KiB
Python
"""Strategia #3 candidata: ROTAZIONE cross-sectional momentum (multi-crypto).
|
|
Una sola strategia che usa l'INTERO paniere: ad ogni ribilanciamento alloca il
|
|
capitale agli asset con momentum migliore (long-only). Cattura la dispersione tra
|
|
crypto (gli alt forti corrono molto piu' di BTC nei bull) senza shortare nulla.
|
|
|
|
Onesto: i pesi a close[i] usano solo rendimenti passati; il rendimento del bar
|
|
i->i+1 e' realizzato con quei pesi. Fee sul turnover. Allineamento per timestamp.
|
|
"""
|
|
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 get_df, available_assets, FEE_RT # noqa: E402
|
|
|
|
LEV = 3.0
|
|
GROSS = 0.45 # esposizione lorda = LEV*POS del singolo (0.15*3) per confronto equo
|
|
|
|
|
|
def build_panel(assets: list[str], tf: str) -> pd.DataFrame:
|
|
"""Matrice close allineata per timestamp (inner join)."""
|
|
closes = {}
|
|
for a in assets:
|
|
df = get_df(a, tf)
|
|
s = pd.Series(df["close"].values,
|
|
index=pd.to_datetime(df["timestamp"], unit="ms", utc=True))
|
|
closes[a] = s[~s.index.duplicated()]
|
|
panel = pd.DataFrame(closes).dropna()
|
|
return panel
|
|
|
|
|
|
def simulate_rotation(panel: pd.DataFrame, lookback=30, top_k=2,
|
|
fee_rt=FEE_RT, gross=GROSS, abs_filter=True,
|
|
oos_frac=0.0) -> dict:
|
|
"""Ad ogni barra: ranking per rendimento passato `lookback`; pesi uguali sui
|
|
top_k con momentum>0 (se abs_filter); altrimenti cash. gross = esposizione tot.
|
|
oos_frac>0: parte a investire solo dall'ultimo frac del campione."""
|
|
P = panel.values
|
|
T, N = P.shape
|
|
rets = np.zeros_like(P)
|
|
rets[1:] = P[1:] / P[:-1] - 1
|
|
years = panel.index.year.values
|
|
start = max(lookback + 1, int(T * (1 - oos_frac)) if oos_frac else lookback + 1)
|
|
cap = peak = 1000.0
|
|
max_dd = 0.0
|
|
w = np.zeros(N)
|
|
yearly: dict[int, float] = {}
|
|
turn_total = 0.0
|
|
for i in range(start, T - 1):
|
|
mom = P[i] / P[i - lookback] - 1
|
|
order = np.argsort(mom)[::-1]
|
|
new_w = np.zeros(N)
|
|
chosen = [j for j in order if (mom[j] > 0 or not abs_filter)][:top_k]
|
|
if chosen:
|
|
for j in chosen:
|
|
new_w[j] = gross / len(chosen)
|
|
# fee sul turnover (one-way = fee_rt/2 su ogni variazione di peso)
|
|
turnover = np.abs(new_w - w).sum()
|
|
cap -= cap * turnover * (fee_rt / 2)
|
|
turn_total += turnover
|
|
w = new_w
|
|
port_ret = float(np.dot(w, rets[i + 1])) # rendimento bar i->i+1
|
|
cap = max(cap * (1 + port_ret), 10.0)
|
|
peak = max(peak, cap); max_dd = max(max_dd, (peak - cap) / peak)
|
|
yearly[years[i]] = yearly.get(years[i], 0.0) + port_ret * 100
|
|
return {
|
|
"ret": (cap / 1000 - 1) * 100,
|
|
"dd": max_dd * 100,
|
|
"turnover": turn_total,
|
|
"yearly": yearly,
|
|
"pos_years": sum(1 for v in yearly.values() if v > 0),
|
|
"n_years": len(yearly),
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
assets = available_assets()
|
|
print(f"ROTATION cross-sectional momentum — fee {FEE_RT*100:.2f}% RT, gross {GROSS} | OOS 30%")
|
|
print(f" Paniere: {assets}")
|
|
for tf in ["1d", "4h"]:
|
|
panel = build_panel(assets, tf)
|
|
print(f"\n === {tf} === panel {panel.shape[0]} barre, {panel.index[0].date()} -> {panel.index[-1].date()}")
|
|
print(f" {'config':<22s}{'FULL%':>9s}{'OOS%':>9s}{'DD%':>6s}{'Turn':>7s}{'AnniP':>8s}")
|
|
for lb in [20, 30, 60, 90]:
|
|
for k in [1, 2, 3]:
|
|
full = simulate_rotation(panel, lookback=lb, top_k=k)
|
|
oos = simulate_rotation(panel, lookback=lb, top_k=k, oos_frac=0.30)
|
|
anni = f"{full['pos_years']}/{full['n_years']}"
|
|
print(f" lb{lb:<3d} top{k:<14d}{full['ret']:>+9.0f}{oos['ret']:>+9.0f}"
|
|
f"{full['dd']:>6.0f}{full['turnover']:>7.0f}{anni:>8s}")
|