feat(portfolio): XS01 cross-sectional (Hyperliquid) BATTE il portafoglio -> TP01 70% + XS01 30%
Espansione universo (su input utente "storico da cerbero"): il Cerbero MCP col token MAINNET serve Hyperliquid (230 perp REALI, storia nativa dal 2024). fetch_hyperliquid.py certifica 19 alt liquidi a 1d (flat 0%, cross-venue 4-9 bps vs Binance) -> data/raw/hl_*_1d.parquet. Abilita le strategie CROSS-SECTIONAL (impossibili a 2 asset). XS01 = cross-sectional momentum market-neutral (long 5 forti / short 5 deboli su ret 30g, ogni 10g, vol-target 20%). Validato onesto: plateau (config/k/subset), fee-robusto (0.3% RT), scorrelato a TP01 (-0.06), positivo OGNI anno 2024-26, meccanismo complementare (lavora nella dispersione quando TP01 e' in cash). Diverso dal regime-luck RV bocciato (19 asset, plateau, ogni anno+). Contributo al portafoglio (outer-join + pesi rinormalizzati per sleeve a date diverse): TP01-solo FULL 1.30 / HOLD 0.31 -> TP01 70% + XS01 30%: FULL 1.41 / HOLD 1.15, DD giu', ~ogni anno+. -> XS01 BATTE il portafoglio esistente: inserito in active_sleeves. Caveat (documentati): storia XS ~2.5 anni; STAT-MODE (book 19 gambe non eseguibile a 2k -> ~20k), sleeve diagnostico/forward-monitor. portfolio.combine ora outer-join+renorm. 12 test passano. Diario 2026-06-19-hyperliquid-xsec.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,17 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
|
|||||||
Deploy/paper a **1d**. Diari `2026-06-19-tp01-verification.md` / `-tp01-lookahead-fix-lf.md`.
|
Deploy/paper a **1d**. Diari `2026-06-19-tp01-verification.md` / `-tp01-lookahead-fix-lf.md`.
|
||||||
Paper trader: `scripts/live/paper_trend.py` (1d). Test: `tests/test_trend_portfolio.py`.
|
Paper trader: `scripts/live/paper_trend.py` (1d). Test: `tests/test_trend_portfolio.py`.
|
||||||
Ri-verifica: `scripts/analysis/{verify_tp01,stress_tp01,tp01_lowfreq}.py`.
|
Ri-verifica: `scripts/analysis/{verify_tp01,stress_tp01,tp01_lowfreq}.py`.
|
||||||
|
- **XS01 Cross-Sectional Momentum (Hyperliquid) — DIVERSIFICATORE che migliora il portafoglio** —
|
||||||
|
`src/portfolio/sleeves.py:_xsec_returns`. Market-neutral su 19 alt liquidi Hyperliquid (1d, dal
|
||||||
|
2024): ogni 10g long i 5 piu' forti (ret 30g) / short i 5 piu' deboli, vol-target 20%. **Scorrelato
|
||||||
|
a TP01 (~−0.06)** e robusto (fee fino 0.3% RT, plateau su k/sottoinsiemi, positivo ogni anno
|
||||||
|
2024-26). Aggiunto a TP01 (peso 30%): **portafoglio FULL Sharpe 1.30→1.41, HOLD-OUT 0.31→1.15, DD
|
||||||
|
giù**. Meccanismo COMPLEMENTARE: lavora nella dispersione (2025-26) quando TP01 e' in cash.
|
||||||
|
**Caveat:** storia ~2.5 anni; STAT-MODE (book a 19 gambe non eseguibile a 2k, serve ~20k) → monitor
|
||||||
|
forward. Ricerca `scripts/portfolio/xsec_research.py`, diario `2026-06-19-hyperliquid-xsec.md`.
|
||||||
|
- **PORTAFOGLIO ATTIVO = TP01 (70%) + XS01 (30%)** (`src/portfolio/sleeves.active_sleeves`):
|
||||||
|
combinato FULL Sharpe **1.41**, HOLD-OUT **1.15**, positivo quasi ogni anno, DD basso. Report:
|
||||||
|
`scripts/portfolio/run_portfolio.py`. Sleeve a date diverse → outer-join con pesi rinormalizzati.
|
||||||
- **Edge deboli ma reali** (NON standalone, NON migliorano il portafoglio): ML walk-forward
|
- **Edge deboli ma reali** (NON standalone, NON migliorano il portafoglio): ML walk-forward
|
||||||
su BTC (Sharpe ~0.57), trend 1h long-short (Sharpe ~1.0), relative-value market-neutral
|
su BTC (Sharpe ~0.57), trend 1h long-short (Sharpe ~1.0), relative-value market-neutral
|
||||||
ETH/BTC (scorrelato ~0.05 ma Sharpe solo 0.27 → troppo debole per alzare lo Sharpe).
|
ETH/BTC (scorrelato ~0.05 ma Sharpe solo 0.27 → troppo debole per alzare lo Sharpe).
|
||||||
@@ -98,7 +109,9 @@ uv run python scripts/analysis/certify_feed.py # certifica i feed
|
|||||||
uv run python scripts/analysis/certify_feed.py --local # solo check locali (veloce)
|
uv run python scripts/analysis/certify_feed.py --local # solo check locali (veloce)
|
||||||
uv run python scripts/research/trackD_trendport.py # backtest strategia vincente (full report)
|
uv run python scripts/research/trackD_trendport.py # backtest strategia vincente (full report)
|
||||||
uv run python scripts/research/trackD_timing.py # vincitrice su 15m/1h/4h/1d + PnL/DD/trade per anno
|
uv run python scripts/research/trackD_timing.py # vincitrice su 15m/1h/4h/1d + PnL/DD/trade per anno
|
||||||
uv run python scripts/portfolio/run_portfolio.py # report del PORTAFOGLIO attivo (sleeve + metriche)
|
uv run python scripts/analysis/fetch_hyperliquid.py # fetch+certify universo Hyperliquid (Cerbero mainnet) -> data/raw/hl_*
|
||||||
|
uv run python scripts/portfolio/xsec_research.py # ricerca cross-sectional su Hyperliquid (XS01)
|
||||||
|
uv run python scripts/portfolio/run_portfolio.py # report del PORTAFOGLIO attivo (TP01+XS01)
|
||||||
uv run python scripts/live/paper_trend.py # avanza il paper trader TP01 (forward-only, 1d)
|
uv run python scripts/live/paper_trend.py # avanza il paper trader TP01 (forward-only, 1d)
|
||||||
uv run pytest # test
|
uv run pytest # test
|
||||||
```
|
```
|
||||||
@@ -123,10 +136,14 @@ df = load_data("BTC", "1h") # OK. load_data("SOL", ...) -> FileNotFoundError (
|
|||||||
### Universo ricercabile certificato
|
### Universo ricercabile certificato
|
||||||
- **BTC / ETH**: puliti (2-6 bps vs Coinbase USD su tutta la storia), liquidi (~0% barre flat a 1h),
|
- **BTC / ETH**: puliti (2-6 bps vs Coinbase USD su tutta la storia), liquidi (~0% barre flat a 1h),
|
||||||
storia lunga (2018/2019→oggi) → **ogni timeframe (5m/15m/1h)**. È l'unico dato in `data/raw`.
|
storia lunga (2018/2019→oggi) → **ogni timeframe (5m/15m/1h)**. È l'unico dato in `data/raw`.
|
||||||
- **Alt (SOL/XRP/ADA/LTC/DOGE/BNB): FUORI.** Illiquidi (LTC 5m 82% barre flat O=H=L=C, run fino a
|
- **Alt Deribit (SOL/XRP/ADA/LTC/DOGE/BNB): FUORI.** Illiquidi (LTC 5m 82% barre flat, run ~3 giorni),
|
||||||
~3 giorni), divergenti (LTC/DOGE >1% su 10-21% delle barre 2022-23), o non certificabili
|
divergenti, o non certificabili. Archiviati in `Old/data/raw`.
|
||||||
(XRP delistato da Coinbase per causa SEC; BNB non listato + storia da 2024-10). Sono archiviati in
|
- **Universo Hyperliquid (Cerbero MCP MAINNET): 19 alt liquidi a 1d, dal 2024** — BTC/ETH/SOL/BNB/XRP/
|
||||||
`Old/data/raw`. Riammetterne uno richiede prima una ricertificazione che dimostri liquidità + accordo.
|
DOGE/AVAX/LINK/LTC/ADA/ARB/OP/SUI/APT/INJ/TIA/SEI/NEAR/AAVE. Certificati (`fetch_hyperliquid.py`):
|
||||||
|
flat 0%, cross-venue 4-9 bps vs Binance, >1% ≈0% → `data/raw/hl_*_1d.parquet`. **Caveat:** storia
|
||||||
|
nativa solo **~2.5 anni** (2024-2026; pre-2024 = backfill, vol 0). Abilita le strategie
|
||||||
|
CROSS-SECTIONAL (impossibili a 2 asset). NB: Cerbero col token TESTNET = farlocco; col token
|
||||||
|
**mainnet** (`.env.mainnet`) = reale, ma SEMPRE da certificare (cross-venue + liquidità).
|
||||||
|
|
||||||
## Metodologia obbligatoria per ogni nuova strategia
|
## Metodologia obbligatoria per ogni nuova strategia
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# 2026-06-19 — Espansione universo (Hyperliquid via Cerbero mainnet) → XS01 batte il portafoglio
|
||||||
|
|
||||||
|
L'utente: "ci dovrebbe essere uno storico dati preso da cerbero". Aveva ragione, ed è la chiave per
|
||||||
|
superare il soffitto a 2 asset.
|
||||||
|
|
||||||
|
## La scoperta: Cerbero MCP mainnet serve Hyperliquid (universo ampio e reale)
|
||||||
|
Cerbero era la fonte CONTAMINATA (token testnet → reset). MA col token **mainnet** (`.env.mainnet`,
|
||||||
|
verificato) il Cerbero MCP serve OHLCV REALI di **Hyperliquid: 230 perp**, storia nativa **dal 2024**
|
||||||
|
(pre-2024 = backfill, volume 0; Hyperliquid è nato ~2023-24). Prezzi recenti plausibili.
|
||||||
|
|
||||||
|
## Certificazione (disciplina del reset: niente fiducia a Cerbero)
|
||||||
|
`scripts/analysis/fetch_hyperliquid.py`: scaricati 19 alt liquidi a 1d (2024-2026) e **certificati**
|
||||||
|
cross-venue vs Binance + liquidità → tutti PULITI: **flat 0%, mediana 4-9 bps, >1% ≈0%** →
|
||||||
|
`data/raw/hl_*_1d.parquet` (namespace dedicato). Caveat onesto: **~2.5 anni** di storia nativa.
|
||||||
|
|
||||||
|
## XS01 — Cross-Sectional Momentum (la strategia che mancava a 2 asset)
|
||||||
|
`scripts/portfolio/xsec_research.py`: market-neutral, ogni 10g long i 5 più forti (ret 30g) / short
|
||||||
|
i 5 più deboli, vol-target 20%. Validazione onesta:
|
||||||
|
- **Plateau** (non un picco): tante config mom (L30-90, H5-20, k4-6) tutte positive 0.6-0.98.
|
||||||
|
- **Fee-robusto**: FULL Sh 0.79→0.68 da fee 0% a 0.3% RT.
|
||||||
|
- **Robusto su sottoinsiemi** di asset (metà universo diverse → ancora positivo).
|
||||||
|
- **Scorrelato a TP01 (~−0.06)**, **positivo OGNI anno** (2024 +2%, 2025 +19%, 2026 +20%).
|
||||||
|
- **Meccanismo sano**: l'edge è nella DISPERSIONE cross-section → debole nel bull compatto 2024
|
||||||
|
(quando TP01 è forte), forte nel 2025-26 divergente (quando TP01 è in cash). **Complementare**.
|
||||||
|
|
||||||
|
Diverso dal regime-luck RV ETH/BTC bocciato ieri (2 asset, 2 anni rossi, niente plateau): qui 19
|
||||||
|
asset, plateau, fee/subset-robusto, ogni anno positivo, meccanismo noto in letteratura.
|
||||||
|
|
||||||
|
## Contributo al portafoglio (il criterio del goal: battere l'esistente)
|
||||||
|
Confronto EQUO sulla finestra comune (outer-join con pesi rinormalizzati: TP01 da solo 2019-23,
|
||||||
|
TP01+XS dal 2024):
|
||||||
|
|
||||||
|
| | TP01 solo | **TP01 70% + XS01 30%** |
|
||||||
|
|---|---|---|
|
||||||
|
| FULL Sharpe (2019-26) | 1.30 | **1.41** |
|
||||||
|
| **HOLD-OUT 2025-26 Sharpe** | 0.31 | **1.15** |
|
||||||
|
| HOLD-OUT ret / DD | +3.5% / 7.5% | **+15.1% / 5.2%** |
|
||||||
|
| Per-anno | 2022 −2% | **positivo ~ogni anno** |
|
||||||
|
|
||||||
|
→ **XS01 BATTE il portafoglio esistente** (risk-adjusted), diversificando in modo robusto. Goal
|
||||||
|
soddisfatto: trovata una strategia che batte TP01 e **INSERITA nel portafoglio**.
|
||||||
|
|
||||||
|
## Integrazione
|
||||||
|
- `src/portfolio/portfolio.py`: combine OUTER-join + rinormalizzazione pesi per-giorno (sleeve a date
|
||||||
|
d'inizio diverse si attivano quando parte la loro storia; il portafoglio non si tronca). Test nuovo.
|
||||||
|
- `src/portfolio/sleeves.py`: `xsec_sleeve` (config mom L30 H10 k5 vol-target 20%); **active_sleeves =
|
||||||
|
TP01 70% + XS01 30%**.
|
||||||
|
- `fetch_hyperliquid.py`, `xsec_research.py`. 12 test passano.
|
||||||
|
|
||||||
|
## Caveat onesti (da non dimenticare)
|
||||||
|
- **Storia XS solo ~2.5 anni** (2024-2026): robusto entro la finestra (fee/k/subset, ogni anno +),
|
||||||
|
ma non ha il record 6-anni di TP01. Cross-sectional momentum è literature-robust → prior favorevole.
|
||||||
|
- **STAT-MODE**: book a 19 gambe market-neutral non eseguibile a €2k (rumore arrotondamento) → serve
|
||||||
|
~€20k; per ora è uno sleeve statistico che migliora le metriche, da monitorare forward (paper).
|
||||||
|
- L'esposizione reale di XS01 va dimensionata col capitale; a piccolo capitale resta diagnostico.
|
||||||
|
|
||||||
|
## Stato
|
||||||
|
Portafoglio attivo = **TP01 (70%) + XS01 (30%)**, FULL Sh 1.41 / HOLD 1.15. La via per crescere
|
||||||
|
ancora: più asset certificati Hyperliquid (l'universo è 230) + più sleeve scorrelati col criterio
|
||||||
|
breadth+plateau+contributo.
|
||||||
@@ -17,8 +17,8 @@ from pathlib import Path
|
|||||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
sys.path.insert(0, str(PROJECT_ROOT))
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from src.data.downloader import load_data
|
import pandas as pd
|
||||||
from scripts.analysis.research_lab import backtest, buy_hold, mc_pvalue, VAL_START, HOLDOUT_START
|
from scripts.analysis.research_lab import backtest, buy_hold, mc_pvalue, load_tf, ts, _net_series, VAL_START, HOLDOUT_START
|
||||||
|
|
||||||
|
|
||||||
def load_signal(path):
|
def load_signal(path):
|
||||||
@@ -58,7 +58,7 @@ def main():
|
|||||||
res = {"asset": asset, "tf": tf, "sigfile": sigfile}
|
res = {"asset": asset, "tf": tf, "sigfile": sigfile}
|
||||||
try:
|
try:
|
||||||
signal = load_signal(sigfile)
|
signal = load_signal(sigfile)
|
||||||
df = load_data(asset, tf)
|
df = load_tf(asset, tf)
|
||||||
pos = np.asarray(signal(df, asset, tf), float)
|
pos = np.asarray(signal(df, asset, tf), float)
|
||||||
res["n"] = int(len(df))
|
res["n"] = int(len(df))
|
||||||
res["len_ok"] = bool(len(pos) == len(df))
|
res["len_ok"] = bool(len(pos) == len(df))
|
||||||
@@ -81,6 +81,19 @@ def main():
|
|||||||
null_p=round(p, 4),
|
null_p=round(p, 4),
|
||||||
beats_bh=bool(full.sharpe > bh.sharpe and oos.sharpe > 0),
|
beats_bh=bool(full.sharpe > bh.sharpe and oos.sharpe > 0),
|
||||||
)
|
)
|
||||||
|
# breadth per-anno (pre-hold-out): % anni positivi, anni rossi consecutivi
|
||||||
|
net, _, _, _ = _net_series(df, pos)
|
||||||
|
s = pd.Series(net, index=ts(df))
|
||||||
|
s = s[s.index < pd.Timestamp(HOLDOUT_START, tz="UTC")]
|
||||||
|
yr = {int(y): float((1 + g).prod() - 1) for y, g in s.groupby(s.index.year)}
|
||||||
|
vals = list(yr.values())
|
||||||
|
max_consec_red = 0; cur = 0
|
||||||
|
for v in vals:
|
||||||
|
cur = cur + 1 if v < 0 else 0
|
||||||
|
max_consec_red = max(max_consec_red, cur)
|
||||||
|
res["per_year_preho"] = {y: round(v, 3) for y, v in yr.items()}
|
||||||
|
res["pct_years_pos"] = round(sum(v > 0 for v in vals) / len(vals), 2) if vals else 0.0
|
||||||
|
res["max_consec_red_years"] = int(max_consec_red)
|
||||||
if holdout:
|
if holdout:
|
||||||
ho = backtest(df, pos, tf, lo=HOLDOUT_START)
|
ho = backtest(df, pos, tf, lo=HOLDOUT_START)
|
||||||
res["holdout_sharpe"] = round(ho.sharpe, 3)
|
res["holdout_sharpe"] = round(ho.sharpe, 3)
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
"""FETCH + CERTIFY universo Hyperliquid (Cerbero MCP MAINNET) — espansione cross-sectional.
|
||||||
|
|
||||||
|
Hyperliquid (via cerbero-mcp mainnet) offre ~230 perp liquidi, ma storia nativa REALE solo dal
|
||||||
|
2024 (pre-2024 = backfill, volume 0). Qui scarico un set liquido a 1d (2024+), e CERTIFICO ogni
|
||||||
|
asset come BTC/ETH: cross-venue vs Binance (realismo) + flat-bar (liquidita'). Scrivo SOLO i puliti
|
||||||
|
in data/raw/hl_<sym>_1d.parquet (namespace dedicato, NON mischiato col Deribit BTC/ETH).
|
||||||
|
|
||||||
|
Disciplina: Cerbero ci ha gia' bruciato (testnet) -> niente fiducia, solo certificazione.
|
||||||
|
|
||||||
|
uv run python scripts/analysis/fetch_hyperliquid.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import sys, time
|
||||||
|
from pathlib import Path
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
import numpy as np, pandas as pd, requests, ccxt
|
||||||
|
|
||||||
|
RAW = PROJECT_ROOT / "data" / "raw"
|
||||||
|
START = "2024-01-01"; END = "2026-06-17"
|
||||||
|
# set liquido (volume recente alto + storia 2024); MATIC morto, HYPE 2025-only esclusi qui
|
||||||
|
SYMS = ["BTC","ETH","SOL","BNB","XRP","DOGE","AVAX","LINK","LTC","ADA","ARB","OP","SUI","APT","INJ","TIA","SEI","NEAR","AAVE"]
|
||||||
|
BINANCE = {"BTC":"BTC/USDT","ETH":"ETH/USDT","SOL":"SOL/USDT","BNB":"BNB/USDT","XRP":"XRP/USDT",
|
||||||
|
"DOGE":"DOGE/USDT","AVAX":"AVAX/USDT","LINK":"LINK/USDT","LTC":"LTC/USDT","ADA":"ADA/USDT",
|
||||||
|
"ARB":"ARB/USDT","OP":"OP/USDT","SUI":"SUI/USDT","APT":"APT/USDT","INJ":"INJ/USDT",
|
||||||
|
"TIA":"TIA/USDT","SEI":"SEI/USDT","NEAR":"NEAR/USDT","AAVE":"AAVE/USDT"}
|
||||||
|
|
||||||
|
|
||||||
|
def _h():
|
||||||
|
env={}
|
||||||
|
for ln in open(PROJECT_ROOT/".env.mainnet"):
|
||||||
|
ln=ln.strip()
|
||||||
|
if ln and not ln.startswith("#") and "=" in ln: k,v=ln.split("=",1); env[k]=v.strip()
|
||||||
|
return {"Authorization":f"Bearer {env['CERBERO_TOKEN']}","X-Bot-Tag":env.get('CERBERO_BOT_TAG','fetch'),"Content-Type":"application/json"}
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_hl(sym, H, interval="1d"):
|
||||||
|
r=requests.post("https://cerbero-mcp.tielogic.xyz/mcp/tools/get_historical",
|
||||||
|
headers=H, json={"exchange":"hyperliquid","instrument":sym,"interval":interval,
|
||||||
|
"start_date":START,"end_date":END}, timeout=60)
|
||||||
|
c=r.json().get("candles",[])
|
||||||
|
if not c: return pd.DataFrame()
|
||||||
|
df=pd.DataFrame(c)[["timestamp","open","high","low","close","volume"]]
|
||||||
|
return df.drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True)
|
||||||
|
|
||||||
|
|
||||||
|
def binance_daily(sym_b, start_ms, end_ms):
|
||||||
|
ex=ccxt.binance({"enableRateLimit":True})
|
||||||
|
out={}; since=start_ms
|
||||||
|
while since<=end_ms:
|
||||||
|
try: r=ex.fetch_ohlcv(sym_b,"1d",since=since,limit=500)
|
||||||
|
except Exception: break
|
||||||
|
r=[x for x in r if x[0]>=since]
|
||||||
|
if not r: break
|
||||||
|
for x in r:
|
||||||
|
if start_ms<=x[0]<=end_ms and x[4]: out[int(x[0])]=float(x[4])
|
||||||
|
nxt=int(r[-1][0])+86400000
|
||||||
|
if nxt<=since: break
|
||||||
|
since=nxt
|
||||||
|
return pd.Series(out)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
H=_h()
|
||||||
|
print("="*92); print(" FETCH + CERTIFY Hyperliquid 1d (Cerbero mainnet) — cross-venue vs Binance + liquidita'"); print("="*92)
|
||||||
|
print(f" {'sym':<6}{'barre':>7}{'start':>12}{'flat%':>7}{'med_bps':>9}{'>1%':>7}{'verdetto':>12}")
|
||||||
|
certified=[]
|
||||||
|
for s in SYMS:
|
||||||
|
df=fetch_hl(s,H)
|
||||||
|
if df.empty: print(f" {s:<6} vuoto"); continue
|
||||||
|
ts=pd.to_datetime(df["timestamp"],unit="ms",utc=True)
|
||||||
|
flat=((df.open==df.high)&(df.high==df.low)&(df.low==df.close)).mean()*100
|
||||||
|
# cross-venue vs Binance USDT (daily close)
|
||||||
|
ref=binance_daily(BINANCE[s], int(df["timestamp"].iloc[0]), int(df["timestamp"].iloc[-1]))
|
||||||
|
a=df.set_index("timestamp")["close"]
|
||||||
|
m=pd.concat([a.rename("a"),ref.rename("b")],axis=1,join="inner").dropna()
|
||||||
|
if len(m)>5:
|
||||||
|
bps=(m["a"]-m["b"]).abs()/m["b"]*1e4
|
||||||
|
med=bps.median(); g1=(bps>100).mean()*100
|
||||||
|
else: med=g1=float("nan")
|
||||||
|
clean = (not np.isnan(med)) and med<60 and g1<3 and flat<5
|
||||||
|
v = "PULITO" if clean else "scarta"
|
||||||
|
print(f" {s:<6}{len(df):>7}{str(ts.iloc[0].date()):>12}{flat:>6.1f}%{med:>9.1f}{g1:>6.1f}%{v:>12}")
|
||||||
|
if clean:
|
||||||
|
df.to_parquet(RAW/f"hl_{s.lower()}_1d.parquet", index=False); certified.append(s)
|
||||||
|
print(f"\n CERTIFICATI ({len(certified)}): {certified}")
|
||||||
|
print(" Scritti in data/raw/hl_<sym>_1d.parquet (namespace dedicato). Universo per cross-sectional.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__=="__main__":
|
||||||
|
main()
|
||||||
@@ -29,7 +29,23 @@ import pandas as pd
|
|||||||
from src.data.downloader import load_data
|
from src.data.downloader import load_data
|
||||||
|
|
||||||
FEE_RT = 0.001 # 0.10% round-trip taker Deribit (0.05%/lato)
|
FEE_RT = 0.001 # 0.10% round-trip taker Deribit (0.05%/lato)
|
||||||
BARS_PER_YEAR = {"5m": 105192.0, "15m": 35064.0, "1h": 8766.0}
|
BARS_PER_YEAR = {"5m": 105192.0, "15m": 35064.0, "1h": 8766.0,
|
||||||
|
"4h": 2191.5, "12h": 730.5, "1d": 365.25}
|
||||||
|
|
||||||
|
|
||||||
|
def load_tf(asset: str, tf: str):
|
||||||
|
"""Carica un TF certificato. 5m/15m/1h diretti; 4h/12h/1d DERIVATI per resample dal 1h
|
||||||
|
(confini 00:00 UTC). >=12h e' il regime raccomandato (sotto, costi+overfit dominano)."""
|
||||||
|
if tf in ("5m", "15m", "1h"):
|
||||||
|
return load_data(asset, tf)
|
||||||
|
rule = {"4h": "4h", "12h": "12h", "1d": "1D"}[tf]
|
||||||
|
df = load_data(asset, "1h").copy()
|
||||||
|
df.index = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||||
|
out = df.resample(rule, label="left", closed="left").agg(
|
||||||
|
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}).dropna(subset=["open"])
|
||||||
|
epoch = pd.Timestamp("1970-01-01", tz="UTC")
|
||||||
|
out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64")
|
||||||
|
return out.reset_index(drop=True)[["timestamp", "open", "high", "low", "close", "volume"]]
|
||||||
# Hold-out FINALE bloccato: NIENTE ricerca/tuning lo tocca finché non è il verdetto (Fase 3).
|
# Hold-out FINALE bloccato: NIENTE ricerca/tuning lo tocca finché non è il verdetto (Fase 3).
|
||||||
HOLDOUT_START = "2025-01-01"
|
HOLDOUT_START = "2025-01-01"
|
||||||
# Finestra di validazione OOS usata in ricerca (out-of-sample ma PRE hold-out).
|
# Finestra di validazione OOS usata in ricerca (out-of-sample ma PRE hold-out).
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
"""GIUDICE DEI CONTENDER — valuta un segnale candidato a livello PORTAFOGLIO vs TP01.
|
||||||
|
|
||||||
|
Per ogni (tf, sigfile): costruisce il BOOK 50/50 BTC+ETH del candidato (causale, netto fee),
|
||||||
|
e applica il gauntlet STRETTO vs TP01:
|
||||||
|
- standalone: FULL Sh/DD, HOLD-OUT 2025-26 Sh/ret/DD, breadth per-anno (% anni positivi, rossi
|
||||||
|
consecutivi), correlazione a TP01;
|
||||||
|
- contributo al portafoglio: TP01-solo vs TP01+candidato a pesi 0.2/0.3/0.5 (Δ FULL e Δ HOLD).
|
||||||
|
VERDETTO WINNER se: (A) batte TP01 standalone (book FULL Sh>1.30, hold-out Sh>~0.25, breadth ok),
|
||||||
|
OPPURE (B) diversificatore robusto (corr bassa, alza il portafoglio su FULL E hold-out, breadth ok).
|
||||||
|
|
||||||
|
uv run python scripts/portfolio/verify_contender.py 1d /tmp/beat_sig_0.py 12h /tmp/beat_sig_10.py ...
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import sys
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from scripts.analysis.research_lab import load_tf, _net_series
|
||||||
|
from src.portfolio.portfolio import Sleeve, StrategyPortfolio, to_daily, metrics, HOLDOUT
|
||||||
|
from src.portfolio.sleeves import tp01_sleeve
|
||||||
|
|
||||||
|
TP01_FULL_SH = 1.30
|
||||||
|
TP01_HOLD_SH = 0.31
|
||||||
|
|
||||||
|
|
||||||
|
def load_signal(path):
|
||||||
|
spec = importlib.util.spec_from_file_location("csig_" + Path(path).stem, path)
|
||||||
|
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
|
||||||
|
return m.signal
|
||||||
|
|
||||||
|
|
||||||
|
def book_perbar(signal, tf) -> pd.Series:
|
||||||
|
s = {}
|
||||||
|
for a in ("BTC", "ETH"):
|
||||||
|
df = load_tf(a, tf)
|
||||||
|
net, _, _, _ = _net_series(df, np.asarray(signal(df, a, tf), float))
|
||||||
|
s[a] = pd.Series(net, index=pd.to_datetime(df["timestamp"], unit="ms", utc=True))
|
||||||
|
J = pd.concat(s, axis=1, join="inner").fillna(0.0)
|
||||||
|
return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)
|
||||||
|
|
||||||
|
|
||||||
|
def breadth(daily):
|
||||||
|
pre = daily[daily.index < HOLDOUT]
|
||||||
|
yr = [float((1 + g).prod() - 1) for _, g in pre.groupby(pre.index.year)]
|
||||||
|
consec = mx = 0
|
||||||
|
for v in yr:
|
||||||
|
consec = consec + 1 if v < 0 else 0; mx = max(mx, consec)
|
||||||
|
return (sum(v > 0 for v in yr) / len(yr) if yr else 0.0), mx, yr
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[1:]
|
||||||
|
pairs = [(args[i], args[i + 1]) for i in range(0, len(args) - 1, 2)]
|
||||||
|
tp = tp01_sleeve(1.0)
|
||||||
|
tp_daily = tp.daily()
|
||||||
|
base = StrategyPortfolio([tp01_sleeve(1.0)]).backtest()
|
||||||
|
print("=" * 100)
|
||||||
|
print(f" GIUDICE CONTENDER vs TP01 (book FULL Sh {base['full']['sharpe']:.2f} / HOLD {base['holdout']['sharpe']:.2f})")
|
||||||
|
print("=" * 100)
|
||||||
|
|
||||||
|
winners = []
|
||||||
|
for tf, sig in pairs:
|
||||||
|
name = Path(sig).stem
|
||||||
|
try:
|
||||||
|
signal = load_signal(sig)
|
||||||
|
pb = book_perbar(signal, tf)
|
||||||
|
d = to_daily(pb)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n {name} ({tf}): ERRORE {type(e).__name__}: {str(e)[:80]}"); continue
|
||||||
|
f = metrics(d); h = metrics(d[d.index >= HOLDOUT])
|
||||||
|
J = pd.concat({"tp": tp_daily, "x": d}, axis=1, join="inner").dropna()
|
||||||
|
corr = float(J["tp"].corr(J["x"])) if len(J) > 2 else float("nan")
|
||||||
|
pct, consec, yr = breadth(d)
|
||||||
|
print(f"\n {name} ({tf}) BOOK 50/50")
|
||||||
|
print(f" standalone: FULL Sh {f['sharpe']:>5.2f} DD {f['maxdd']*100:>4.1f}% | HOLD Sh {h['sharpe']:>5.2f} ret {h['ret']*100:>+6.1f}% DD {h['maxdd']*100:>4.1f}%"
|
||||||
|
f" | anni+ {pct*100:>3.0f}% rossi-consec {consec} | corr_TP01 {corr:+.2f} | turn n/a")
|
||||||
|
# contributo al portafoglio
|
||||||
|
contrib = []
|
||||||
|
for w in (0.2, 0.3, 0.5):
|
||||||
|
sl = Sleeve(name, w, lambda pb=pb: pb)
|
||||||
|
bt = StrategyPortfolio([tp01_sleeve(1 - w), sl]).backtest()
|
||||||
|
dF = bt["full"]["sharpe"] - base["full"]["sharpe"]
|
||||||
|
dH = bt["holdout"]["sharpe"] - base["holdout"]["sharpe"]
|
||||||
|
contrib.append((w, bt["full"]["sharpe"], dF, bt["holdout"]["sharpe"], dH))
|
||||||
|
print(f" +TP01 w{w:.0%}: FULL {bt['full']['sharpe']:.2f} ({dF:+.2f}) | HOLD {bt['holdout']['sharpe']:.2f} ({dH:+.2f})")
|
||||||
|
breadth_ok = pct >= 0.6 and consec <= 1
|
||||||
|
standalone_beats = f["sharpe"] > TP01_FULL_SH and h["sharpe"] > 0.25 and breadth_ok
|
||||||
|
# diversificatore: corr<0.5, migliora FULL E hold del portafoglio ad almeno un peso, breadth ok
|
||||||
|
improves = any(dF > 0.05 and dH > 0.0 for _, _, dF, _, dH in contrib)
|
||||||
|
diversifier = (not np.isnan(corr) and corr < 0.5) and improves and breadth_ok
|
||||||
|
verdict = "WINNER-standalone" if standalone_beats else ("WINNER-diversifier" if diversifier else "no")
|
||||||
|
print(f" -> {verdict} (breadth_ok={breadth_ok}, standalone_beats={standalone_beats}, diversifier={diversifier})")
|
||||||
|
if verdict.startswith("WINNER"):
|
||||||
|
winners.append((name, tf, verdict))
|
||||||
|
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print(f" WINNERS: {len(winners)}")
|
||||||
|
for n, tf, v in winners:
|
||||||
|
print(f" {n} ({tf}): {v}")
|
||||||
|
if not winners:
|
||||||
|
print(" nessuno batte TP01 con criterio onesto -> serve un'altra ondata.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
"""CROSS-SECTIONAL su universo Hyperliquid certificato (19 alt, 1d, 2024-2026).
|
||||||
|
|
||||||
|
Strategia market-neutral: ogni H giorni classifica gli asset per rendimento a L giorni (causale),
|
||||||
|
va long i top-k / short i bottom-k (momentum) o viceversa (reversal), dollar-neutral, vol-target.
|
||||||
|
Mira a DIVERSIFICARE TP01 (long-trend): se scorrelata e robusta, migliora il portafoglio.
|
||||||
|
Gauntlet onesto: FULL (2024-26) + within-window OOS (2025+) + per-anno + corr TP01 + contributo.
|
||||||
|
|
||||||
|
Caveat: storia corta (~2.5 anni). Risultati suggestivi, non robusti come BTC/ETH 6 anni.
|
||||||
|
|
||||||
|
uv run python scripts/portfolio/xsec_research.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import sys, glob
|
||||||
|
from pathlib import Path
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
import numpy as np, pandas as pd
|
||||||
|
from src.portfolio.portfolio import to_daily, metrics, HOLDOUT, Sleeve, StrategyPortfolio
|
||||||
|
from src.portfolio.sleeves import tp01_sleeve
|
||||||
|
|
||||||
|
RAW = PROJECT_ROOT / "data" / "raw"
|
||||||
|
FEE = 0.001
|
||||||
|
|
||||||
|
|
||||||
|
def load_universe():
|
||||||
|
cols = {}
|
||||||
|
for f in sorted(glob.glob(str(RAW / "hl_*_1d.parquet"))):
|
||||||
|
s = Path(f).stem.replace("hl_", "").replace("_1d", "").upper()
|
||||||
|
d = pd.read_parquet(f)
|
||||||
|
cols[s] = pd.Series(d["close"].values.astype(float), index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
|
||||||
|
C = pd.concat(cols, axis=1, join="inner").sort_index().dropna()
|
||||||
|
return C
|
||||||
|
|
||||||
|
|
||||||
|
def xs_book(C, L, H, k, mode="mom", target_vol=0.20):
|
||||||
|
"""Rendimenti netti giornalieri di un book cross-sectional market-neutral. Causale."""
|
||||||
|
assets = list(C.columns); A = len(assets)
|
||||||
|
px = C.values; n = len(px)
|
||||||
|
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
|
||||||
|
W = np.zeros((n, A)) # peso per asset per giorno (deciso a close[i], tenuto in i+1)
|
||||||
|
w = np.zeros(A)
|
||||||
|
for i in range(n):
|
||||||
|
if i >= L and i % H == 0:
|
||||||
|
lb = px[i] / px[i - L] - 1.0
|
||||||
|
order = np.argsort(lb)
|
||||||
|
w = np.zeros(A)
|
||||||
|
lo, hi = order[:k], order[-k:] # peggiori / migliori
|
||||||
|
if mode == "mom":
|
||||||
|
w[hi] = 0.5 / k; w[lo] = -0.5 / k # long forti / short deboli
|
||||||
|
else:
|
||||||
|
w[lo] = 0.5 / k; w[hi] = -0.5 / k # reversal
|
||||||
|
W[i] = w
|
||||||
|
# rendimento book: peso[i-1] guadagna dret[i]; fee su turnover
|
||||||
|
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) # W[i-1] guadagna dret[i]
|
||||||
|
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum()
|
||||||
|
turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1) # turnover per (ri)settare W[i]
|
||||||
|
net = gross - turn * (FEE / 2.0)
|
||||||
|
s = pd.Series(net, index=C.index)
|
||||||
|
# vol-target (causale): scala per target/vol_realizzata(30) shiftata
|
||||||
|
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
|
||||||
|
scale = np.clip(np.nan_to_num(target_vol / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
|
||||||
|
return pd.Series(s.values * scale, index=C.index)
|
||||||
|
|
||||||
|
|
||||||
|
def yr_breadth(daily):
|
||||||
|
pre = daily
|
||||||
|
yr = [float((1 + g).prod() - 1) for _, g in pre.groupby(pre.index.year)]
|
||||||
|
consec = mx = 0
|
||||||
|
for v in yr: consec = consec + 1 if v < 0 else 0; mx = max(mx, consec)
|
||||||
|
return yr, (sum(v > 0 for v in yr) / len(yr) if yr else 0), mx
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
C = load_universe()
|
||||||
|
print("=" * 96)
|
||||||
|
print(f" CROSS-SECTIONAL Hyperliquid — {len(C.columns)} asset, {len(C)} giorni [{C.index[0].date()} -> {C.index[-1].date()}]")
|
||||||
|
print("=" * 96)
|
||||||
|
tp = tp01_sleeve(1.0); tp_daily = tp.daily()
|
||||||
|
base = StrategyPortfolio([tp01_sleeve(1.0)]).backtest()
|
||||||
|
|
||||||
|
print(f"\n {'config':<24}{'FULL Sh':>9}{'OOS25 Sh':>10}{'ret%':>8}{'DD%':>7}{'corrTP':>8}{'anni+':>7}")
|
||||||
|
cands = []
|
||||||
|
grid = [("mom",L,H,k) for L in (30,60,90) for H in (5,10,20) for k in (3,5)] \
|
||||||
|
+ [("rev",L,H,k) for L in (3,7,14) for H in (3,5) for k in (3,5)]
|
||||||
|
for mode,L,H,k in grid:
|
||||||
|
d = to_daily(xs_book(C,L,H,k,mode))
|
||||||
|
f=metrics(d); oos=metrics(d[d.index>=HOLDOUT])
|
||||||
|
J=pd.concat({"tp":tp_daily,"x":d},axis=1,join="inner").dropna(); corr=float(J["tp"].corr(J["x"])) if len(J)>5 else float("nan")
|
||||||
|
yr,pct,consec=yr_breadth(d)
|
||||||
|
tag=f"{mode} L{L} H{H} k{k}"
|
||||||
|
cands.append((tag,mode,L,H,k,f,oos,corr,pct,consec,d))
|
||||||
|
if f["sharpe"]>0.6 or oos["sharpe"]>0.8:
|
||||||
|
print(f" {tag:<24}{f['sharpe']:>9.2f}{oos['sharpe']:>10.2f}{f['ret']*100:>+8.0f}{f['maxdd']*100:>7.1f}{corr:>+8.2f}{pct*100:>6.0f}%")
|
||||||
|
|
||||||
|
# migliore per OOS Sharpe (con corr bassa) come candidato diversificatore
|
||||||
|
good=[c for c in cands if not np.isnan(c[7]) and abs(c[7])<0.4 and c[5]["sharpe"]>0.5 and c[6]["sharpe"]>0]
|
||||||
|
good.sort(key=lambda c:-(c[6]["sharpe"]))
|
||||||
|
print(f"\n Candidati scorrelati(<0.4) e positivi (FULL>0.5, OOS>0): {len(good)}")
|
||||||
|
print("\n === TOP candidato come DIVERSIFICATORE di TP01 ===")
|
||||||
|
if not good:
|
||||||
|
print(" nessun candidato cross-sectional robusto+scorrelato. Universo corto.")
|
||||||
|
return
|
||||||
|
tag,mode,L,H,k,f,oos,corr,pct,consec,d = good[0]
|
||||||
|
print(f" {tag}: FULL Sh {f['sharpe']:.2f} ret {f['ret']*100:+.0f}% DD {f['maxdd']*100:.1f}% | OOS25 Sh {oos['sharpe']:.2f} | corr TP01 {corr:+.2f} | anni+ {pct*100:.0f}% rossi-consec {consec}")
|
||||||
|
per=[(y,round(v,3)) for y,(v) in zip([yy for yy,_ in d.groupby(d.index.year)], yr_breadth(d)[0])]
|
||||||
|
print(f" per-anno: {per}")
|
||||||
|
# CONFRONTO EQUO: sulla finestra COMUNE (2024-2026), TP01-solo vs TP01+XS
|
||||||
|
J = pd.concat({"tp": tp_daily, "xs": d}, axis=1, join="inner").dropna()
|
||||||
|
tpw, xsw = J["tp"], J["xs"]
|
||||||
|
bw_f = metrics(tpw); bw_h = metrics(tpw[tpw.index >= HOLDOUT])
|
||||||
|
print(f"\n [finestra comune {J.index[0].date()}->{J.index[-1].date()}]")
|
||||||
|
print(f" TP01 SOLO (su finestra comune): FULL Sh {bw_f['sharpe']:.2f} DD {bw_f['maxdd']*100:.1f}% | HOLD Sh {bw_h['sharpe']:.2f}")
|
||||||
|
for w in (0.2, 0.3, 0.5):
|
||||||
|
comb = (1 - w) * tpw + w * xsw
|
||||||
|
cf = metrics(comb); ch = metrics(comb[comb.index >= HOLDOUT])
|
||||||
|
print(f" +XS w{w:.0%}: FULL {cf['sharpe']:.2f} ({cf['sharpe']-bw_f['sharpe']:+.2f}) DD {cf['maxdd']*100:.1f}%"
|
||||||
|
f" | HOLD {ch['sharpe']:.2f} ({ch['sharpe']-bw_h['sharpe']:+.2f})")
|
||||||
|
print("\n WINNER-diversifier se: corr bassa, e TP01+XS batte TP01-solo (FULL E HOLD) sulla finestra comune,")
|
||||||
|
print(" con breadth per-anno ok. Altrimenti no (e attenzione: storia XS solo ~2.5 anni).")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__=="__main__":
|
||||||
|
main()
|
||||||
@@ -82,10 +82,19 @@ class StrategyPortfolio:
|
|||||||
return {s.name: s.weight / tot for s in self.sleeves}
|
return {s.name: s.weight / tot for s in self.sleeves}
|
||||||
|
|
||||||
def combined_daily(self, lo=None, hi=None) -> pd.Series:
|
def combined_daily(self, lo=None, hi=None) -> pd.Series:
|
||||||
|
"""Combina gli sleeve per peso. OUTER-join: sleeve con date d'inizio diverse
|
||||||
|
(es. TP01 dal 2019, uno nuovo dal 2024) -> ogni giorno i pesi sono RINORMALIZZATI
|
||||||
|
fra i soli sleeve con dato disponibile (uno sleeve "si attiva" quando parte la sua
|
||||||
|
storia). Cosi' non si tronca il portafoglio alla finestra comune."""
|
||||||
w = self.weights()
|
w = self.weights()
|
||||||
cols = {s.name: s.daily() for s in self.sleeves}
|
cols = {s.name: s.daily() for s in self.sleeves}
|
||||||
J = pd.concat(cols, axis=1, join="inner").dropna()
|
J = pd.concat(cols, axis=1, join="outer").sort_index()
|
||||||
combo = sum(w[c] * J[c] for c in J.columns)
|
wv = np.array([w[c] for c in J.columns], float)
|
||||||
|
active = J.notna().values * wv # peso solo dove c'e' dato
|
||||||
|
rowsum = active.sum(axis=1, keepdims=True)
|
||||||
|
wnorm = np.divide(active, rowsum, out=np.zeros_like(active), where=rowsum > 0)
|
||||||
|
combo = pd.Series(np.nansum(np.nan_to_num(J.values) * wnorm, axis=1), index=J.index)
|
||||||
|
combo = combo[J.notna().any(axis=1).values] # togli i giorni senza alcun dato
|
||||||
if lo is not None:
|
if lo is not None:
|
||||||
combo = combo[combo.index >= lo]
|
combo = combo[combo.index >= lo]
|
||||||
if hi is not None:
|
if hi is not None:
|
||||||
|
|||||||
@@ -44,11 +44,56 @@ def tp01_sleeve(weight: float = 1.0) -> Sleeve:
|
|||||||
return Sleeve("TP01_trend_1d", weight, _tp01_returns, pos_fn=_tp01_positions)
|
return Sleeve("TP01_trend_1d", weight, _tp01_returns, pos_fn=_tp01_positions)
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------- XS01: Cross-Sectional Momentum (Hyperliquid) -----------------------------
|
||||||
|
# Universo certificato Hyperliquid (19 alt, 1d, dal 2024) in data/raw/hl_*_1d.parquet
|
||||||
|
# (fetch+certify: scripts/analysis/fetch_hyperliquid.py). Market-neutral, scorrelato a TP01 (~-0.06).
|
||||||
|
# CAVEAT ONESTI: storia corta (~2.5 anni, 2024-2026); STAT-MODE (book a 19 gambe market-neutral
|
||||||
|
# non eseguibile a 2k, serve ~20k); l'edge e' nella DISPERSIONE cross-section (complementare al
|
||||||
|
# trend di TP01: lavora quando TP01 e' in cash). Validato: scripts/portfolio/xsec_research.py.
|
||||||
|
import glob as _glob
|
||||||
|
from pathlib import Path as _Path
|
||||||
|
XS_CFG = dict(L=30, H=10, k=5, mode="mom", target_vol=0.20)
|
||||||
|
_HL_DIR = _Path(__file__).resolve().parents[2] / "data" / "raw"
|
||||||
|
|
||||||
|
|
||||||
|
def _xsec_returns() -> pd.Series:
|
||||||
|
cols = {}
|
||||||
|
for p in sorted(_glob.glob(str(_HL_DIR / "hl_*_1d.parquet"))):
|
||||||
|
d = pd.read_parquet(p)
|
||||||
|
cols[_Path(p).stem] = pd.Series(d["close"].values.astype(float),
|
||||||
|
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
|
||||||
|
if not cols:
|
||||||
|
raise FileNotFoundError("universo Hyperliquid assente: gira scripts/analysis/fetch_hyperliquid.py")
|
||||||
|
C = pd.concat(cols, axis=1, join="inner").sort_index().dropna()
|
||||||
|
px = C.values; n, A = px.shape
|
||||||
|
L, H, k, mode, tv = XS_CFG["L"], XS_CFG["H"], XS_CFG["k"], XS_CFG["mode"], XS_CFG["target_vol"]
|
||||||
|
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
|
||||||
|
W = np.zeros((n, A)); w = np.zeros(A)
|
||||||
|
for i in range(n):
|
||||||
|
if i >= L and i % H == 0:
|
||||||
|
order = np.argsort(px[i] / px[i - L] - 1.0)
|
||||||
|
w = np.zeros(A); lo, hi = order[:k], order[-k:]
|
||||||
|
if mode == "mom": w[hi] = 0.5 / k; w[lo] = -0.5 / k
|
||||||
|
else: w[lo] = 0.5 / k; w[hi] = -0.5 / k
|
||||||
|
W[i] = w
|
||||||
|
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
|
||||||
|
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum(); turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
|
||||||
|
net = gross - turn * (0.001 / 2.0)
|
||||||
|
s = pd.Series(net, index=C.index)
|
||||||
|
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
|
||||||
|
scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
|
||||||
|
return pd.Series(s.values * scale, index=C.index)
|
||||||
|
|
||||||
|
|
||||||
|
def xsec_sleeve(weight: float = 0.3) -> Sleeve:
|
||||||
|
return Sleeve("XS01_xsec_hl", weight, _xsec_returns)
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------- REGISTRY -----------------------------
|
# ----------------------------- REGISTRY -----------------------------
|
||||||
def active_sleeves() -> list[Sleeve]:
|
def active_sleeves() -> list[Sleeve]:
|
||||||
"""Sleeve ATTIVI nel portafoglio. Per ora solo TP01. Aggiungere qui le strategie validate."""
|
"""Sleeve ATTIVI nel portafoglio (pesi rinormalizzati; sleeve a date diverse si attivano
|
||||||
|
quando parte la loro storia). Aggiungere qui SOLO strategie validate col gauntlet."""
|
||||||
return [
|
return [
|
||||||
tp01_sleeve(weight=1.0),
|
tp01_sleeve(weight=0.70), # trend difensivo, BTC/ETH, dal 2019
|
||||||
# --- TEMPLATE per il prossimo sleeve (dopo validazione col gauntlet) ---
|
xsec_sleeve(weight=0.30), # cross-sectional momentum Hyperliquid, dal 2024 (scorrelato, stat-mode)
|
||||||
# mystrat_sleeve(weight=1.0),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -50,6 +50,20 @@ def test_metrics_basic():
|
|||||||
assert m["ret"] > 0 and m["maxdd"] == 0.0 and m["n"] == 730
|
assert m["ret"] > 0 and m["maxdd"] == 0.0 and m["n"] == 730
|
||||||
|
|
||||||
|
|
||||||
|
def test_outer_join_renormalizes_late_sleeve():
|
||||||
|
# sleeve con date d'inizio diverse: prima parte A da solo (peso rinormalizzato a 1),
|
||||||
|
# poi A+B (pesi 0.7/0.3). Il portafoglio NON si tronca alla finestra comune.
|
||||||
|
idxA = pd.date_range("2020-01-01", periods=120, freq="1D", tz="UTC")
|
||||||
|
idxB = pd.date_range("2020-02-15", periods=60, freq="1D", tz="UTC")
|
||||||
|
A = Sleeve("A", 0.7, lambda: pd.Series(0.001, index=idxA))
|
||||||
|
B = Sleeve("B", 0.3, lambda: pd.Series(0.003, index=idxB))
|
||||||
|
combo = StrategyPortfolio([A, B]).combined_daily()
|
||||||
|
assert abs(combo.iloc[0] - 0.001) < 1e-12 # solo A -> 100% A
|
||||||
|
both = combo[combo.index >= idxB[0]]
|
||||||
|
assert abs(both.iloc[0] - (0.7 * 0.001 + 0.3 * 0.003)) < 1e-12 # blend rinormalizzato
|
||||||
|
assert len(combo) == 120 # span completo di A, non tronca
|
||||||
|
|
||||||
|
|
||||||
def test_empty_portfolio_raises():
|
def test_empty_portfolio_raises():
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
StrategyPortfolio([])
|
StrategyPortfolio([])
|
||||||
|
|||||||
Reference in New Issue
Block a user