research(wave-0822): VRP-QUOTE-VERE scartato — e collect_chain raccoglie la famiglia di contratti che il conto non puo' marginare
This commit is contained in:
@@ -11,6 +11,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato.
|
||||
| 12 | ORTHO-SCREEN | **SCARTATO 7/7** | nessuna famiglia arriva a ADDS+DSR; e il motivo e' **aritmetico**: su 168 trial il massimo atteso dal rumore e' Sharpe **1.572**, SOPRA il soffitto direzionale misurato (~1.3) -> uno screen largo su BTC/ETH direzionale **non puo'** passare un DSR, per costruzione |
|
||||
| 3 | XS-LITE | **SCARTATO** (come sleeve) + **1 soglia pubblicata falsificata** | concentrare XS01 non crea uno sleeve nuovo (corr 0.81-0.96 col canonico, DSR FAIL, de-levering non superato) — ma il muro *"XS01 serve ~$20k"* e' misurato su una diagnostica di TURNOVER: il ribilancio vero smette di passare sotto **~$109 di sleeve (~$730 di book), 27x piu' in basso** |
|
||||
| 10 | FLOW-SQUEEZE | **SCARTATO** (2 motivi indipendenti) | meta' dell'ipotesi **non esiste nel dato** (`liquidation_*_risk` = una sola categoria in 17.229/17.229 righe); l'altra meta' e' sotto la propria soglia di potenza **dichiarata prima** (MDE 1,056% vs effetto 0,402%) ed e' comunque **morta alla nascita** (la colonna non e' ricostruibile) |
|
||||
| 11 | VRP-QUOTE-VERE | **SCARTATO** + **1 difetto di RACCOLTA in produzione** | il campione contiene l'**astensione** della strategia (0/19 settimane passano IV-rank>0.30) e il titolo non sopravvive all'ora d'ingresso; ma soprattutto: **`collect_chain.py` raccoglie la famiglia di contratti che il conto NON puo' marginare** |
|
||||
|
||||
## Note che sopravvivono ai singoli filoni
|
||||
|
||||
@@ -144,3 +145,47 @@ come proxy di AFFOLLAMENTO. Quarto lato.**
|
||||
il vincolo binding NON e' la concentrazione degli episodi (86-87 episodi corti, i primi 8 coprono
|
||||
solo il 20-25% delle ore) ma la **lunghezza del calendario** (90 giorni). *Ampiezza scarsa* e
|
||||
*calendario corto* sono due diagnosi diverse e si curano in modi diversi: piu' episodi vs piu' tempo.
|
||||
|
||||
### 11 — VRP-QUOTE-VERE (r0822_vrp_real_quotes.py, 60 trial + 9 ancore d'ingresso)
|
||||
🚨 **IL RISULTATO DI PRODUZIONE — `collect_chain.py` interroga `{"currency": "BTC"|"ETH"}`, che su
|
||||
Deribit restituisce le sole opzioni INVERSE (regolate in BTC/ETH). Il conto e' in USDC** (esegue
|
||||
`BTC_USDC-PERPETUAL`). Esiste una famiglia **USDC-lineare** con lotto minimo **10x piu' piccolo**:
|
||||
| famiglia | min lotto | ~$ per lotto |
|
||||
|---|---|---|
|
||||
| BTC inverse | 0.1 | ~$7.700 |
|
||||
| **BTC_USDC** | **0.01** | ~$780 |
|
||||
| ETH inverse | 1.0 | ~$2.500 |
|
||||
| **ETH_USDC** | **0.1** | ~$250 |
|
||||
Quindi il muro pubblicato *"BTC opzioni min 0.1 = $6.210/lotto -> FUORI a $3.000"* (congelato in
|
||||
`tests/test_vrp_profit_take.py`) e' **misurato sulla famiglia che il conto non puo' marginare**.
|
||||
**3ª occorrenza dello schema `fee_watch` (21/08): un controllo/collettore puntato su uno strumento
|
||||
diverso da quello che si trada.**
|
||||
⚠️ **MA la verifica indipendente del coordinatore cambia la conclusione, e in meglio:** contando
|
||||
l'open interest famiglia per famiglia (`get_book_summary_by_currency`, misura mia, non dell'agente)
|
||||
| famiglia | strumenti | con OI>=100 |
|
||||
|---|---|---|
|
||||
| BTC inverse | 1038 | **415** |
|
||||
| ETH inverse | 932 | **548** |
|
||||
| **BTC_USDC** | 686 | **5** |
|
||||
| **ETH_USDC** | 660 | **119** |
|
||||
-> **BTC_USDC e' praticamente morto** (5 strumenti liquidi su 686). **Il muro sul BTC RESTA, ma per
|
||||
la ragione giusta e meglio misurata: non la taglia del lotto, la LIQUIDITA'.** ETH_USDC (119
|
||||
strumenti, ~$250/lotto) e' l'unica gamba che varrebbe la pena raccogliere.
|
||||
📌 Sottoprodotto fuori perimetro: esiste un universo opzioni USDC piu' liquido di BTC_USDC —
|
||||
SOL_USDC 341, XRP_USDC 250, HYPE_USDC 172, AVAX_USDC 146 strumenti con OI>=100.
|
||||
- **Il campione misura la strategia mentre STA FERMA**: 0/19 settimane passano il gate IV-rank>0.30,
|
||||
DVOL alla mediana del 7°/11° percentile storico, sottostante +25%/+44%.
|
||||
- ✅ **Replica indipendente del f del 30/07 su campione piu' lungo: f = 0,714 pooled** (IC95
|
||||
[0,690, 0,779], **0/19 osservazioni >= 1,0**). Attraversare lo spread costa **~10% del credito**.
|
||||
- ⚠️ **Correzione a un numero pubblicato:** il forfait fee del sleeve sovrastima il listino vero di
|
||||
**1,44x (BTC) / 1,80x (ETH)**, non di "~2x". Il segno (conservativo) regge, la taglia no.
|
||||
- ⚠️ **Rischio sul MINIMO, non sulle chiusure:** peggior mark infra-settimana mediana −6,4% del
|
||||
rischio, **minimo −91,0%**; fra le settimane chiuse **in utile**, minimo **−56,9%**.
|
||||
- Gate: `implausible_sharpe` **FLAGGED** su entrambi (0 perdite su 10 -> regola del tre: tasso vero
|
||||
fino al 30%, su un payoff che perde 7,4x il credito); `deflated_sharpe` **non calcolabile** (T=10)
|
||||
e l'agente **si e' rifiutato di fabbricare una griglia giornaliera al 94% di zeri** per superare
|
||||
il T>=30; banda d'ancora: canonico all'89° pctl, **mediana onesta 1,45**, banda che include il
|
||||
negativo. Il titolo annualizzato (32,58) e' stato **deliberatamente non riportato come risultato**.
|
||||
- ⚠️ L'agente ha **refutato in corsa una propria conclusione**: spostare l'ora d'ingresso cambia
|
||||
QUALI STRIKE corrispondono al delta obiettivo -> non e' lo stesso trade un'ora dopo, e' **un'altra
|
||||
struttura**; la media BTC **cambia segno** fra le ancore (−1,35% -> +11,22%).
|
||||
|
||||
@@ -0,0 +1,787 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
r0822_basis_calendar.py — filone BASIS-CALENDAR (ondata 2026-08-22)
|
||||
|
||||
DOMANDA
|
||||
Il progetto ha chiuso il FUNDING su tre lati (CC01 carry spot-perp, time-series,
|
||||
cross-sectional) ma non ha mai guardato la STRUTTURA A TERMINE dei futures datati
|
||||
di Deribit — l'unico strumento delta-neutro eseguibile sul NOSTRO venue, a 2 gambe.
|
||||
Q1 Il basis (datato vs indice/perp) e la sua pendenza sono PREVEDIBILI, cioe'
|
||||
esiste un roll-down incassabile, o e' un martingala rumoroso?
|
||||
Q2 Un CALENDAR SPREAD (front vs back) su segnale di pendenza produce un edge
|
||||
netto di fee e spread bid-ask REALI?
|
||||
Q3 E' ortogonale al libro ed eseguibile a $600-3.000?
|
||||
|
||||
IL DATO — costruito qui, non esisteva
|
||||
`data/raw/fut_*` sono futures INDICE AZIONARIO da IB (ES/NQ/DAX...), niente a che
|
||||
vedere. Su Deribit non c'era nulla. Ma:
|
||||
* /public/get_instruments?expired=true ritorna UN SOLO strumento (Deribit purga
|
||||
subito le liste), quindi la via ovvia non funziona;
|
||||
* i nomi dei trimestrali sono DETERMINISTICI (ultimo venerdi' di MAR/GIU/SET/DIC,
|
||||
08:00 UTC) e `get_tradingview_chart_data` serve la storia COMPLETA di un
|
||||
contratto SCADUTO se il nome lo si costruisce a mano.
|
||||
Verificato: 30/30 trimestrali 2019Q1..2026Q2 rispondono `status=ok`.
|
||||
=> **7+ anni di storia**, che includono il deleveraging 2022. E' esattamente la coda
|
||||
che a CC01 mancava per costruzione (funding HL dal 2023-05): qui il 2022 c'e'.
|
||||
|
||||
La chiamata funding porta in regalo l'INDICE Deribit orario (`index_price`), quindi
|
||||
spot e funding arrivano insieme e il basis si misura sull'indice vero, non su una proxy.
|
||||
|
||||
DISCIPLINA DI RETE (la VPS ha un IP solo e ci gira il libro)
|
||||
<=2 richieste/s, e il fetcher DORME nella finestra :25-:29 di ogni ora, che e'
|
||||
`cron_chain` (il collettore della catena opzioni). Il progetto ha gia' pagato un'ora
|
||||
di guasto per un IP saturato. Tutto e' in cache su scratchpad: un ri-run non tocca la rete.
|
||||
|
||||
CONVENZIONE DI ROLL — dichiarata, e mai una serie incollata
|
||||
I prezzi NON vengono mai splicciati. La posizione e' su una COPPIA DI CONTRATTI
|
||||
specifica; al roll si chiude e si riapre pagando il round-trip pieno su entrambe le
|
||||
gambe. L'incollaggio avviene solo nella serie dei RENDIMENTI, che e' l'unico posto in
|
||||
cui e' lecito: e' l'errore che il progetto ha gia' commesso con le serie equity
|
||||
ri-aggiustate, e qui e' strutturalmente impossibile.
|
||||
|
||||
USO
|
||||
nice -n 19 timeout 900 uv run python scripts/research/r0822_basis_calendar.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
import datetime as dt
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
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))
|
||||
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt"))
|
||||
|
||||
import altlib as A # noqa: E402
|
||||
|
||||
SCRATCH = Path(
|
||||
os.environ.get(
|
||||
"R0822_SCRATCH",
|
||||
"/tmp/claude-1001/-opt-docker-PythagorasGoal/"
|
||||
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad",
|
||||
)
|
||||
)
|
||||
CACHE = SCRATCH / "basis_cache"
|
||||
CACHE.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
API = "https://www.deribit.com/api/v2"
|
||||
UTC = dt.timezone.utc
|
||||
MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN",
|
||||
"JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]
|
||||
|
||||
ASSETS = ("BTC", "ETH")
|
||||
FEE_SIDE = 0.00035 # 3,5 bps/lato per GAMBA (taker Deribit, letto dal venue)
|
||||
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
|
||||
HOURS_Y = 365.25 * 24
|
||||
|
||||
_last_call = [0.0]
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 0. FETCHER — rate-limited, cache-first, consapevole dei cron della VPS
|
||||
# ==========================================================================
|
||||
def _polite_sleep() -> None:
|
||||
"""<=2 req/s E fuori dalla finestra :25-:29 (cron_chain, stesso IP)."""
|
||||
now = dt.datetime.now(UTC)
|
||||
if 25 <= now.minute <= 29:
|
||||
wait = (30 - now.minute) * 60 - now.second
|
||||
print(f" [rete] finestra cron_chain (:25-:29) -> dormo {wait}s")
|
||||
time.sleep(max(1, wait))
|
||||
gap = time.time() - _last_call[0]
|
||||
if gap < 0.55:
|
||||
time.sleep(0.55 - gap)
|
||||
_last_call[0] = time.time()
|
||||
|
||||
|
||||
def _api(path: str, **kw) -> dict:
|
||||
url = API + path + "?" + urllib.parse.urlencode(kw)
|
||||
for attempt in range(4):
|
||||
_polite_sleep()
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=30) as r:
|
||||
return json.load(r)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
if attempt == 3:
|
||||
raise
|
||||
print(f" [rete] {type(exc).__name__} -> retry {attempt+1}/3")
|
||||
time.sleep(2.0 * (attempt + 1))
|
||||
raise RuntimeError("unreachable")
|
||||
|
||||
|
||||
def last_friday(year: int, month: int) -> dt.date:
|
||||
d = dt.date(year, month, calendar.monthrange(year, month)[1])
|
||||
while d.weekday() != 4:
|
||||
d -= dt.timedelta(days=1)
|
||||
return d
|
||||
|
||||
|
||||
def quarterly_universe(asset: str, y0: int = 2019, y1: int = 2027) -> list[tuple[str, pd.Timestamp]]:
|
||||
"""Nomi DETERMINISTICI dei trimestrali Deribit: ultimo venerdi' di MAR/GIU/SET/DIC 08:00 UTC."""
|
||||
out = []
|
||||
for y in range(y0, y1 + 1):
|
||||
for m in (3, 6, 9, 12):
|
||||
lf = last_friday(y, m)
|
||||
exp = pd.Timestamp(lf.year, lf.month, lf.day, 8, tz="UTC")
|
||||
out.append((f"{asset}-{lf.day}{MONTHS[m-1]}{str(y)[2:]}", exp))
|
||||
return out
|
||||
|
||||
|
||||
def fetch_ohlc(instrument: str, t0: pd.Timestamp, t1: pd.Timestamp) -> pd.DataFrame:
|
||||
"""OHLCV orario di UNO strumento (anche SCADUTO), a chunk di 90 giorni. Cache su disco."""
|
||||
cf = CACHE / f"ohlc_{instrument.replace('/', '_')}.parquet"
|
||||
if cf.exists():
|
||||
return pd.read_parquet(cf)
|
||||
frames = []
|
||||
cur = t0
|
||||
while cur < t1:
|
||||
end = min(cur + pd.Timedelta(days=90), t1)
|
||||
try:
|
||||
res = _api("/public/get_tradingview_chart_data",
|
||||
instrument_name=instrument, resolution="60",
|
||||
start_timestamp=int(cur.timestamp() * 1000),
|
||||
end_timestamp=int(end.timestamp() * 1000)).get("result", {})
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f" {instrument}: chunk {cur.date()} FALLITO ({type(exc).__name__})")
|
||||
cur = end
|
||||
continue
|
||||
ticks = res.get("ticks") or []
|
||||
if res.get("status") == "ok" and ticks:
|
||||
frames.append(pd.DataFrame({
|
||||
"ts": pd.to_datetime(ticks, unit="ms", utc=True),
|
||||
"open": res["open"], "high": res["high"],
|
||||
"low": res["low"], "close": res["close"],
|
||||
"volume": res.get("volume", [np.nan] * len(ticks)),
|
||||
}))
|
||||
cur = end
|
||||
df = (pd.concat(frames).drop_duplicates("ts").sort_values("ts").reset_index(drop=True)
|
||||
if frames else pd.DataFrame(columns=["ts", "open", "high", "low", "close", "volume"]))
|
||||
df.to_parquet(cf, index=False)
|
||||
return df
|
||||
|
||||
|
||||
def fetch_funding_and_index(asset: str, t0: pd.Timestamp, t1: pd.Timestamp) -> pd.DataFrame:
|
||||
"""Funding orario del perp E indice Deribit (la chiamata li porta insieme)."""
|
||||
cf = CACHE / f"fund_{asset}.parquet"
|
||||
if cf.exists():
|
||||
return pd.read_parquet(cf)
|
||||
frames, cur = [], t0
|
||||
while cur < t1:
|
||||
end = min(cur + pd.Timedelta(days=30), t1)
|
||||
try:
|
||||
res = _api("/public/get_funding_rate_history",
|
||||
instrument_name=f"{asset}-PERPETUAL",
|
||||
start_timestamp=int(cur.timestamp() * 1000),
|
||||
end_timestamp=int(end.timestamp() * 1000)).get("result", [])
|
||||
except Exception: # noqa: BLE001
|
||||
cur = end
|
||||
continue
|
||||
if res:
|
||||
frames.append(pd.DataFrame({
|
||||
"ts": pd.to_datetime([r["timestamp"] for r in res], unit="ms", utc=True),
|
||||
"index": [r["index_price"] for r in res],
|
||||
"f1h": [r["interest_1h"] for r in res],
|
||||
}))
|
||||
cur = end
|
||||
df = (pd.concat(frames).drop_duplicates("ts").sort_values("ts").reset_index(drop=True)
|
||||
if frames else pd.DataFrame(columns=["ts", "index", "f1h"]))
|
||||
df.to_parquet(cf, index=False)
|
||||
return df
|
||||
|
||||
|
||||
def fetch_live_books(instruments: list[str]) -> pd.DataFrame:
|
||||
"""Calibrazione DATATA dello spread bid-ask: il book VIVO di oggi.
|
||||
Non e' storia — e' un punto di ancoraggio per una stima, e va detto."""
|
||||
cf = CACHE / "live_books.parquet"
|
||||
if cf.exists():
|
||||
return pd.read_parquet(cf)
|
||||
rows = []
|
||||
for nm in instruments:
|
||||
try:
|
||||
r = _api("/public/get_order_book", instrument_name=nm, depth=5)["result"]
|
||||
bid, ask = r.get("best_bid_price"), r.get("best_ask_price")
|
||||
if bid and ask and bid > 0:
|
||||
rows.append(dict(instrument=nm, bid=bid, ask=ask,
|
||||
mid=(bid + ask) / 2,
|
||||
spread_bps=1e4 * (ask - bid) / ((ask + bid) / 2),
|
||||
bid_sz=r.get("best_bid_amount"), ask_sz=r.get("best_ask_amount")))
|
||||
except Exception: # noqa: BLE001
|
||||
continue
|
||||
df = pd.DataFrame(rows)
|
||||
df.to_parquet(cf, index=False)
|
||||
return df
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 1. PANEL + CERTIFICAZIONE (prima di ogni strategia — regola di prim'ordine)
|
||||
# ==========================================================================
|
||||
def build_panel(asset: str) -> tuple[pd.DataFrame, pd.DataFrame]:
|
||||
"""Ritorna (wide, meta). `wide` = indice orario x contratto -> close.
|
||||
`meta` = una riga per contratto con scadenza, barre, copertura, quota barre FLAT."""
|
||||
uni = quarterly_universe(asset)
|
||||
today = pd.Timestamp.now(tz="UTC")
|
||||
closes, vols, meta = {}, {}, []
|
||||
for nm, exp in uni:
|
||||
if exp > today + pd.Timedelta(days=400):
|
||||
continue
|
||||
t0 = exp - pd.Timedelta(days=400)
|
||||
t1 = min(exp + pd.Timedelta(hours=2), today)
|
||||
if t1 <= t0:
|
||||
continue
|
||||
df = fetch_ohlc(nm, t0, t1)
|
||||
if df.empty or len(df) < 200:
|
||||
meta.append(dict(contract=nm, exp=exp, bars=len(df), status="VUOTO/CORTO"))
|
||||
continue
|
||||
s = df.set_index("ts")["close"].astype(float)
|
||||
s = s[s > 0]
|
||||
closes[nm] = s
|
||||
vols[nm] = df.set_index("ts")["volume"].astype(float)
|
||||
flat = float((s.diff() == 0).mean())
|
||||
meta.append(dict(contract=nm, exp=exp, bars=len(s),
|
||||
first=s.index.min(), last=s.index.max(),
|
||||
flat_frac=flat, status="ok"))
|
||||
wide = pd.DataFrame(closes).sort_index()
|
||||
vwide = pd.DataFrame(vols).sort_index()
|
||||
return wide, vwide, pd.DataFrame(meta)
|
||||
|
||||
|
||||
def certify(asset: str, wide: pd.DataFrame, vwide: pd.DataFrame, meta: pd.DataFrame,
|
||||
fund: pd.DataFrame, perp: pd.Series) -> dict:
|
||||
"""Quattro difetti sorvegliati. Un edge su un book fermo NON e' un edge (regola 4)."""
|
||||
print(f"\n --- CERTIFICAZIONE {asset} ---")
|
||||
ok = meta[meta.status == "ok"]
|
||||
print(f" contratti con dato: {len(ok)}/{len(meta)} "
|
||||
f"finestra {wide.index.min().date()} .. {wide.index.max().date()}")
|
||||
|
||||
# (a) barre FLAT per bucket di giorni-a-scadenza = liquidita' vera del datato
|
||||
rows = []
|
||||
for nm in wide.columns:
|
||||
exp = ok.loc[ok.contract == nm, "exp"]
|
||||
if exp.empty:
|
||||
continue
|
||||
s = wide[nm].dropna()
|
||||
dte = (exp.iloc[0] - s.index).total_seconds() / 86400.0
|
||||
flat = (s.diff() == 0).astype(float)
|
||||
for lo, hi, lab in [(0, 30, "0-30g"), (30, 90, "30-90g"),
|
||||
(90, 180, "90-180g"), (180, 400, ">180g")]:
|
||||
m = (dte >= lo) & (dte < hi)
|
||||
if m.sum() > 50:
|
||||
rows.append(dict(bucket=lab, flat=float(flat[m].mean()), n=int(m.sum())))
|
||||
fb = pd.DataFrame(rows).groupby("bucket").agg(flat=("flat", "mean"), n=("n", "sum"))
|
||||
order = ["0-30g", "30-90g", "90-180g", ">180g"]
|
||||
print(" barre FLAT (proxy di illiquidita') per giorni-a-scadenza:")
|
||||
for b in order:
|
||||
if b in fb.index:
|
||||
print(f" {b:9s} flat={fb.loc[b,'flat']*100:5.1f}% barre={int(fb.loc[b,'n']):,}")
|
||||
|
||||
# (b) volume orario mediano per bucket (il flat da solo non basta)
|
||||
vrows = []
|
||||
for nm in vwide.columns:
|
||||
exp = ok.loc[ok.contract == nm, "exp"]
|
||||
if exp.empty:
|
||||
continue
|
||||
s = vwide[nm].dropna()
|
||||
dte = (exp.iloc[0] - s.index).total_seconds() / 86400.0
|
||||
for lo, hi, lab in [(0, 30, "0-30g"), (30, 90, "30-90g"),
|
||||
(90, 180, "90-180g"), (180, 400, ">180g")]:
|
||||
m = (dte >= lo) & (dte < hi)
|
||||
if m.sum() > 50:
|
||||
vrows.append(dict(bucket=lab, med=float(s[m].median()), zero=float((s[m] == 0).mean())))
|
||||
vb = pd.DataFrame(vrows).groupby("bucket").agg(med=("med", "median"), zero=("zero", "mean"))
|
||||
print(" volume orario mediano (USD di nozionale) e quota ore a volume ZERO:")
|
||||
for b in order:
|
||||
if b in vb.index:
|
||||
print(f" {b:9s} mediano=${vb.loc[b,'med']:>12,.0f} ore a vol 0 = {vb.loc[b,'zero']*100:5.1f}%")
|
||||
|
||||
# (c) accordo con l'INDICE alla scadenza: il datato DEVE convergere
|
||||
conv = []
|
||||
for nm in wide.columns:
|
||||
e = ok.loc[ok.contract == nm, "exp"]
|
||||
if e.empty:
|
||||
continue
|
||||
exp = e.iloc[0]
|
||||
s = wide[nm].dropna()
|
||||
tail = s[s.index >= exp - pd.Timedelta(hours=6)]
|
||||
idx = fund.set_index("ts")["index"].reindex(tail.index, method="nearest", tolerance=pd.Timedelta("2h"))
|
||||
if len(tail) and idx.notna().any():
|
||||
conv.append(float(np.abs(np.log(tail / idx)).dropna().iloc[-1]) * 1e4)
|
||||
if conv:
|
||||
print(f" convergenza a scadenza |ln(F/indice)| ultima ora: "
|
||||
f"mediana {np.median(conv):.1f} bps, max {np.max(conv):.1f} bps (n={len(conv)})")
|
||||
|
||||
# (d) CROSS-CHECK INDIPENDENTE: il forward implicito nella catena opzioni
|
||||
xchk = cross_check_option_forward(asset, wide, ok)
|
||||
return dict(flat=fb, vol=vb, conv=conv, xchk=xchk)
|
||||
|
||||
|
||||
def cross_check_option_forward(asset: str, wide: pd.DataFrame, meta: pd.DataFrame) -> dict:
|
||||
"""Il `underlying_price` di un'opzione Deribit E' il forward della SUA scadenza.
|
||||
La catena raccolta dal 2026-07-30 e' quindi una MISURA INDIPENDENTE del basis:
|
||||
se il mio F(T) e il forward delle opzioni sulla stessa scadenza non coincidono,
|
||||
uno dei due e' sbagliato. (L'archivio ereditato da bite ha underlying_price NULL.)"""
|
||||
import glob
|
||||
files = sorted(glob.glob(str(PROJECT_ROOT / "data/raw/cb_chain/2026-*.parquet")))
|
||||
if not files:
|
||||
return dict(status="catena assente")
|
||||
keep = []
|
||||
for f in files[-30:]:
|
||||
try:
|
||||
d = pd.read_parquet(f, columns=["asset", "exp", "ts", "underlying_price", "index_price"])
|
||||
except Exception: # noqa: BLE001
|
||||
continue
|
||||
d = d[(d.asset == asset) & d.underlying_price.notna()]
|
||||
if len(d):
|
||||
keep.append(d)
|
||||
if not keep:
|
||||
return dict(status="nessun underlying_price")
|
||||
ch = pd.concat(keep)
|
||||
ch["ts_h"] = ch["ts"].dt.floor("1h")
|
||||
# forward mediano per (ora, scadenza) — tutti gli strike condividono lo stesso forward
|
||||
fw = ch.groupby(["ts_h", "exp"]).agg(fwd=("underlying_price", "median")).reset_index()
|
||||
rows = []
|
||||
for nm in wide.columns:
|
||||
e = meta.loc[meta.contract == nm, "exp"]
|
||||
if e.empty:
|
||||
continue
|
||||
exp = e.iloc[0]
|
||||
sub = fw[fw.exp == exp]
|
||||
if sub.empty:
|
||||
continue
|
||||
mine = wide[nm].dropna()
|
||||
j = sub.set_index("ts_h")["fwd"].reindex(mine.index).dropna()
|
||||
if len(j) < 5:
|
||||
continue
|
||||
dev = (np.log(mine.reindex(j.index) / j) * 1e4).dropna()
|
||||
rows.append(dict(contract=nm, n=len(dev), med_bps=float(dev.median()),
|
||||
p95_bps=float(dev.abs().quantile(0.95))))
|
||||
return dict(status="ok", table=pd.DataFrame(rows))
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 2. CURVA — front/back trimestrali, basis annualizzato, slope forward
|
||||
# ==========================================================================
|
||||
def curve_frame(asset: str, wide: pd.DataFrame, meta: pd.DataFrame,
|
||||
fund: pd.DataFrame, roll_dte: int) -> pd.DataFrame:
|
||||
"""Per ogni ora: front = trimestrale piu' vicino con dte >= roll_dte, back = il successivo.
|
||||
Ritorna prezzi, tau, basis annualizzati, slope forward e funding."""
|
||||
ok = meta[meta.status == "ok"].sort_values("exp")
|
||||
exps = {r.contract: r.exp for r in ok.itertuples()}
|
||||
idx = wide.index
|
||||
F = fund.set_index("ts").reindex(idx)
|
||||
n = len(idx)
|
||||
front = np.full(n, "", dtype=object)
|
||||
back = np.full(n, "", dtype=object)
|
||||
contracts = list(wide.columns)
|
||||
tau = {c: (exps[c] - idx).total_seconds() / 3600.0 for c in contracts if c in exps}
|
||||
avail = {c: wide[c].notna().values for c in contracts}
|
||||
for c in contracts:
|
||||
if c not in exps:
|
||||
continue
|
||||
alive = avail[c] & (tau[c] >= roll_dte * 24)
|
||||
# front = il primo (per scadenza) ancora vivo; i contratti sono ordinati per exp
|
||||
order = [c for c in ok.contract if c in wide.columns]
|
||||
for i in range(n):
|
||||
picked = []
|
||||
for c in order:
|
||||
if avail[c][i] and tau[c][i] >= roll_dte * 24:
|
||||
picked.append(c)
|
||||
if len(picked) == 2:
|
||||
break
|
||||
if len(picked) == 2:
|
||||
front[i], back[i] = picked
|
||||
out = pd.DataFrame(index=idx)
|
||||
out["front"], out["back"] = front, back
|
||||
m = (front != "") & (back != "")
|
||||
Ff = np.full(n, np.nan); Fb = np.full(n, np.nan)
|
||||
tf = np.full(n, np.nan); tb = np.full(n, np.nan)
|
||||
W = wide.values
|
||||
colpos = {c: k for k, c in enumerate(wide.columns)}
|
||||
for i in np.where(m)[0]:
|
||||
Ff[i] = W[i, colpos[front[i]]]; Fb[i] = W[i, colpos[back[i]]]
|
||||
tf[i] = tau[front[i]][i]; tb[i] = tau[back[i]][i]
|
||||
out["F_front"], out["F_back"] = Ff, Fb
|
||||
out["tau_front_h"], out["tau_back_h"] = tf, tb
|
||||
out["index"] = F["index"].values
|
||||
out["f1h"] = F["f1h"].values
|
||||
# basis annualizzato di ciascuna gamba vs INDICE
|
||||
out["c_front"] = np.log(Ff / out["index"]) * HOURS_Y / tf
|
||||
out["c_back"] = np.log(Fb / out["index"]) * HOURS_Y / tb
|
||||
# slope = tasso forward annualizzato FRA le due scadenze (stazionario, interpretabile)
|
||||
out["slope"] = np.log(Fb / Ff) * HOURS_Y / (tb - tf)
|
||||
out["s_raw"] = np.log(Fb / Ff)
|
||||
return out
|
||||
|
||||
|
||||
def describe_curve(asset: str, cv: pd.DataFrame) -> None:
|
||||
"""Q1 — descrittiva. Il basis e' positivo? persiste? il suo CAMBIO e' prevedibile?"""
|
||||
c = cv.dropna(subset=["c_front", "slope"])
|
||||
print(f"\n --- Q1 CURVA {asset} (n={len(c):,} ore) ---")
|
||||
print(f" basis annualizzato del FRONT: mediana {c.c_front.median()*100:+6.2f}%/anno "
|
||||
f"media {c.c_front.mean()*100:+6.2f}% sd {c.c_front.std()*100:5.2f} "
|
||||
f"contango {float((c.c_front>0).mean())*100:.1f}% del tempo")
|
||||
print(f" slope forward front->back : mediana {c.slope.median()*100:+6.2f}%/anno "
|
||||
f"media {c.slope.mean()*100:+6.2f}% sd {c.slope.std()*100:5.2f} "
|
||||
f"positiva {float((c.slope>0).mean())*100:.1f}% del tempo")
|
||||
fnd = cv["f1h"].dropna() * HOURS_Y
|
||||
print(f" funding perp annualizzato : mediana {fnd.median()*100:+6.2f}%/anno "
|
||||
f"media {fnd.mean()*100:+6.2f}%")
|
||||
print(" per anno (mediane annualizzate):")
|
||||
g = c.groupby(c.index.year).agg(basis_front=("c_front", "median"),
|
||||
slope=("slope", "median"))
|
||||
fy = (cv["f1h"] * HOURS_Y).groupby(cv.index.year).median()
|
||||
for y, r in g.iterrows():
|
||||
print(f" {y} basis_front {r.basis_front*100:+7.2f}% slope {r.slope*100:+7.2f}% "
|
||||
f"funding {fy.get(y, np.nan)*100:+7.2f}%")
|
||||
# prevedibilita': mean-reversion dello slope a 7 giorni (AR sul livello)
|
||||
for h, lab in [(24, "1g"), (24 * 7, "7g"), (24 * 30, "30g")]:
|
||||
x = c["slope"]
|
||||
dx = x.shift(-h) - x
|
||||
j = pd.concat([x, dx], axis=1).dropna()
|
||||
if len(j) > 500:
|
||||
b = np.polyfit(j.iloc[:, 0], j.iloc[:, 1], 1)[0]
|
||||
rho = float(j.iloc[:, 0].corr(j.iloc[:, 1]))
|
||||
print(f" mean-reversion slope a {lab:>3s}: beta {b:+.3f} corr {rho:+.3f} "
|
||||
f"(beta -1 = reversione totale, 0 = martingala)")
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 3. STRATEGIE — posizioni in spazio CONTRATTO (il roll si paga da solo)
|
||||
# ==========================================================================
|
||||
def _z(x: pd.Series, win: int) -> pd.Series:
|
||||
m = x.rolling(win, min_periods=win // 3).mean()
|
||||
s = x.rolling(win, min_periods=win // 3).std()
|
||||
return ((x - m) / s.replace(0, np.nan))
|
||||
|
||||
|
||||
def run_strategy(cv: pd.DataFrame, wide: pd.DataFrame, family: str, sig: str,
|
||||
win_h: int, thr: float, slip_bps: float,
|
||||
fee_side: float = FEE_SIDE) -> pd.Series:
|
||||
"""Ritorno ORARIO netto per $1 di nozionale LORDO PER GAMBA.
|
||||
Le posizioni vivono in spazio CONTRATTO: quando la coppia cambia (roll) la
|
||||
variazione per contratto e' automaticamente piena => il roll paga il round-trip
|
||||
completo su entrambe le gambe. Nessuna serie di prezzi viene MAI incollata.
|
||||
Causalita': w deciso con dati <= t, TENUTO durante t -> t+1 (shift esplicito)."""
|
||||
idx = cv.index
|
||||
if family == "CAL":
|
||||
base = cv["slope"]
|
||||
else: # FvP: basis annualizzato del front
|
||||
base = cv["c_front"]
|
||||
|
||||
if sig == "static_long":
|
||||
w = pd.Series(1.0, index=idx)
|
||||
elif sig == "static_short":
|
||||
w = pd.Series(-1.0, index=idx)
|
||||
elif sig == "carry": # segno del carry corrente (causale)
|
||||
w = -np.sign(base) # contango -> vendi il datato
|
||||
elif sig == "zrev":
|
||||
z = _z(base, win_h)
|
||||
w = -np.tanh(z)
|
||||
w = w.where(z.abs() >= thr, 0.0)
|
||||
elif sig == "zmom":
|
||||
z = _z(base, win_h)
|
||||
w = np.tanh(z)
|
||||
w = w.where(z.abs() >= thr, 0.0)
|
||||
else:
|
||||
raise ValueError(sig)
|
||||
w = pd.Series(np.asarray(w, float), index=idx).fillna(0.0).clip(-1, 1)
|
||||
|
||||
# --- posizioni per CONTRATTO ---
|
||||
cols = list(wide.columns)
|
||||
cpos = {c: k for k, c in enumerate(cols)}
|
||||
P = np.zeros((len(idx), len(cols)))
|
||||
fr = cv["front"].values; bk = cv["back"].values
|
||||
wv = w.values
|
||||
for i in range(len(idx)):
|
||||
if fr[i] == "" or bk[i] == "":
|
||||
continue
|
||||
if family == "CAL": # long spread = long back / short front
|
||||
P[i, cpos[bk[i]]] = wv[i]
|
||||
P[i, cpos[fr[i]]] = -wv[i]
|
||||
else: # FvP: gamba datata; il perp e' a parte
|
||||
P[i, cpos[fr[i]]] = wv[i]
|
||||
Pd = pd.DataFrame(P, index=idx, columns=cols)
|
||||
|
||||
held = Pd.shift(1).fillna(0.0) # decisa a t, TENUTA in t->t+1
|
||||
R = wide.reindex(idx).pct_change().replace([np.inf, -np.inf], np.nan).fillna(0.0)
|
||||
gross = (held * R).sum(axis=1)
|
||||
|
||||
if family == "FvP":
|
||||
# gamba perp: nozionale opposto alla gamba datata + funding pagato/incassato
|
||||
perp_r = pd.Series(cv["index"].pct_change().fillna(0.0).values, index=idx)
|
||||
wheld = held.sum(axis=1) # = w shiftato (una sola gamba datata)
|
||||
gross = gross - wheld * perp_r - wheld * (-1.0) * cv["f1h"].fillna(0.0)
|
||||
# long datato (w>0) => short perp => INCASSA funding se f>0
|
||||
turn = (Pd.diff().abs().sum(axis=1) * 2.0) # datato + perp
|
||||
else:
|
||||
turn = Pd.diff().abs().sum(axis=1)
|
||||
|
||||
cost = turn * (fee_side + slip_bps * 1e-4)
|
||||
return (gross - cost).fillna(0.0)
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 4. METRICHE / GATE
|
||||
# ==========================================================================
|
||||
def to_daily(h: pd.Series) -> pd.Series:
|
||||
return ((1.0 + h.fillna(0.0)).resample("1D").prod() - 1.0).dropna()
|
||||
|
||||
|
||||
def stats(daily: pd.Series) -> dict:
|
||||
r = daily.dropna()
|
||||
if len(r) < 30 or r.std() == 0:
|
||||
return dict(sharpe=0.0, holdout=0.0, maxdd=0.0, cagr=0.0, n=len(r), vol=0.0)
|
||||
sh = float(r.mean() / r.std() * math.sqrt(365.25))
|
||||
eq = (1 + r).cumprod()
|
||||
dd = float(((eq.cummax() - eq) / eq.cummax()).max())
|
||||
yrs = (r.index[-1] - r.index[0]).days / 365.25
|
||||
cagr = float(eq.iloc[-1] ** (1 / yrs) - 1) if yrs > 0.5 and eq.iloc[-1] > 0 else float("nan")
|
||||
ho = r[r.index >= HOLDOUT]
|
||||
sho = float(ho.mean() / ho.std() * math.sqrt(365.25)) if len(ho) > 30 and ho.std() > 0 else float("nan")
|
||||
return dict(sharpe=sh, holdout=sho, maxdd=dd, cagr=cagr, n=len(r),
|
||||
vol=float(r.std() * math.sqrt(365.25)))
|
||||
|
||||
|
||||
def delevering_null(cand: pd.Series, base: pd.Series) -> dict:
|
||||
"""NULL DEL DE-LEVERING (5 occorrenze in questo progetto). Esiste k<1 che,
|
||||
applicato al BASELINE, da' lo STESSO maxDD del candidato con Sharpe MIGLIORE?
|
||||
Se si', il candidato non protegge: e' solo meno leva."""
|
||||
cs = stats(cand)
|
||||
best = None
|
||||
for k in np.arange(0.05, 1.01, 0.05):
|
||||
s = stats(base * k)
|
||||
if s["maxdd"] <= cs["maxdd"] + 1e-9:
|
||||
best = (float(k), s)
|
||||
break
|
||||
if best is None:
|
||||
return dict(refuted=False, note="nessun k<=1 raggiunge quel DD sul baseline")
|
||||
k, s = best
|
||||
return dict(refuted=bool(s["sharpe"] > cs["sharpe"]), k=k,
|
||||
base_sharpe_at_k=s["sharpe"], cand_sharpe=cs["sharpe"],
|
||||
base_dd_at_k=s["maxdd"], cand_dd=cs["maxdd"])
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 5. MAIN
|
||||
# ==========================================================================
|
||||
def main() -> None:
|
||||
t_start = time.time()
|
||||
print("=" * 78)
|
||||
print("r0822 BASIS-CALENDAR — struttura a termine dei futures datati Deribit")
|
||||
print("=" * 78)
|
||||
|
||||
today = pd.Timestamp.now(tz="UTC")
|
||||
panels, funds, metas, vwides = {}, {}, {}, {}
|
||||
|
||||
# ---------------- STEP 1: dato ----------------
|
||||
print("\n[1] DATO — ricostruzione da Deribit pubblico (cache su scratchpad)")
|
||||
for a in ASSETS:
|
||||
print(f" {a}: trimestrali (ultimo venerdi' MAR/GIU/SET/DIC 08:00 UTC)...")
|
||||
wide, vwide, meta = build_panel(a)
|
||||
fund = fetch_funding_and_index(a, pd.Timestamp("2019-01-01", tz="UTC"), today)
|
||||
panels[a], vwides[a], metas[a], funds[a] = wide, vwide, meta, fund
|
||||
ok = meta[meta.status == "ok"]
|
||||
print(f" {len(ok)} contratti, {int(meta.bars.sum()):,} barre orarie, "
|
||||
f"funding+indice {len(fund):,} ore")
|
||||
|
||||
# ---------------- STEP 2: certificazione ----------------
|
||||
print("\n[2] CERTIFICAZIONE (regola 4: un edge su un book fermo non e' un edge)")
|
||||
certs = {}
|
||||
for a in ASSETS:
|
||||
perp = A.get(a, "1h").set_index(pd.DatetimeIndex(
|
||||
pd.to_datetime(A.get(a, "1h")["datetime"], utc=True)))["close"]
|
||||
certs[a] = certify(a, panels[a], vwides[a], metas[a], funds[a], perp)
|
||||
x = certs[a]["xchk"]
|
||||
if x.get("status") == "ok" and len(x["table"]):
|
||||
t = x["table"]
|
||||
print(f" CROSS-CHECK INDIPENDENTE vs forward della catena opzioni "
|
||||
f"({int(t.n.sum())} ore, {len(t)} scadenze):")
|
||||
print(f" deviazione mediana {t.med_bps.median():+.1f} bps, "
|
||||
f"p95 |dev| {t.p95_bps.max():.1f} bps")
|
||||
else:
|
||||
print(f" CROSS-CHECK catena: {x.get('status')}")
|
||||
|
||||
# ---------------- STEP 3: Q1 curva ----------------
|
||||
print("\n[3] Q1 — LA CURVA E' PREVEDIBILE?")
|
||||
curves = {}
|
||||
for a in ASSETS:
|
||||
cv = curve_frame(a, panels[a], metas[a], funds[a], roll_dte=7)
|
||||
curves[a] = cv
|
||||
describe_curve(a, cv)
|
||||
|
||||
# ---------------- STEP 3b: spread bid-ask (calibrazione DATATA) ----------------
|
||||
print("\n[3b] SPREAD BID-ASK — calibrazione sul book VIVO di oggi (non e' storia)")
|
||||
live = []
|
||||
for a in ASSETS:
|
||||
for nm, exp in quarterly_universe(a):
|
||||
if today < exp <= today + pd.Timedelta(days=400):
|
||||
live.append(nm)
|
||||
lb = fetch_live_books(live + [f"{a}-PERPETUAL" for a in ASSETS])
|
||||
slip_est = 5.0
|
||||
if len(lb):
|
||||
for r in lb.itertuples():
|
||||
print(f" {r.instrument:16s} bid {r.bid:>10,.1f} ask {r.ask:>10,.1f} "
|
||||
f"spread {r.spread_bps:6.1f} bps size {r.bid_sz:>8,.0f}/{r.ask_sz:>8,.0f}")
|
||||
dated = lb[~lb.instrument.str.contains("PERPETUAL")]
|
||||
if len(dated):
|
||||
slip_est = float(dated.spread_bps.median()) / 2.0
|
||||
print(f" => mezzo-spread MEDIANO del datato = {slip_est:.1f} bps/lato "
|
||||
f"(STIMA datata oggi, NON storia: il book storico dei datati non e' disponibile)")
|
||||
|
||||
# ---------------- STEP 4: griglia ----------------
|
||||
print("\n[4] GRIGLIA — dichiarata PRIMA, contata AL RIALZO")
|
||||
FAM = {"CAL": ["static_long", "static_short", "zrev", "zmom"],
|
||||
"FvP": ["carry", "static_long", "zrev", "zmom"]}
|
||||
WINS = [720, 2160, 4320] # 30 / 90 / 180 giorni in ore
|
||||
ROLLS = [7, 21]
|
||||
THRS = [0.0, 1.0]
|
||||
cells = [(f, s, w, rl, th) for f, sigs in FAM.items() for s in sigs
|
||||
for w in WINS for rl in ROLLS for th in THRS]
|
||||
print(f" famiglie {list(FAM)} x segnali x finestre {WINS} x roll {ROLLS} x soglie {THRS}")
|
||||
print(f" = {len(cells)} celle valutate (le varianti statiche non usano W/soglia:"
|
||||
f" le conto lo stesso, al rialzo)")
|
||||
|
||||
cvcache = {(a, rl): curve_frame(a, panels[a], metas[a], funds[a], rl)
|
||||
for a in ASSETS for rl in ROLLS}
|
||||
|
||||
rows = []
|
||||
for (fam, sig, win, rl, th) in cells:
|
||||
hs = []
|
||||
for a in ASSETS:
|
||||
h = run_strategy(cvcache[(a, rl)], panels[a], fam, sig, win, th, slip_est)
|
||||
hs.append(h)
|
||||
J = pd.concat(hs, axis=1).fillna(0.0)
|
||||
comb = J.mean(axis=1) # 50/50 BTC+ETH, come il resto del progetto
|
||||
d = to_daily(comb)
|
||||
st = stats(d)
|
||||
rows.append(dict(family=fam, sig=sig, win=win, roll=rl, thr=th,
|
||||
daily=d, **st))
|
||||
G = pd.DataFrame([{k: v for k, v in r.items() if k != "daily"} for r in rows])
|
||||
dailies = {i: r["daily"] for i, r in enumerate(rows)}
|
||||
|
||||
print("\n migliori 8 celle per Sharpe FULL (NON e' la selezione — solo panorama):")
|
||||
for i in G.sharpe.nlargest(8).index:
|
||||
r = G.loc[i]
|
||||
print(f" {r.family:4s} {r.sig:12s} W={int(r.win):5d}h roll={int(r.roll):2d}g "
|
||||
f"thr={r.thr:.1f} | Sh {r.sharpe:+6.2f} hold {r.holdout:+6.2f} "
|
||||
f"DD {r.maxdd*100:5.2f}% CAGR {r.cagr*100:+6.2f}% vol {r.vol*100:5.2f}%")
|
||||
|
||||
# ---------------- STEP 5: selezione IN-SAMPLE ONLY ----------------
|
||||
print("\n[5] SELEZIONE IN-SAMPLE-ONLY (mai la cella col miglior hold-out)")
|
||||
ins = {}
|
||||
for i, d in dailies.items():
|
||||
pre = d[d.index < HOLDOUT]
|
||||
ins[i] = (float(pre.mean() / pre.std() * math.sqrt(365.25))
|
||||
if len(pre) > 60 and pre.std() > 0 else -9.9)
|
||||
best_i = max(ins, key=ins.get)
|
||||
b = G.loc[best_i]
|
||||
bd = dailies[best_i]
|
||||
bs = stats(bd)
|
||||
print(f" cella scelta AL BUIO: {b.family} {b.sig} W={int(b.win)}h roll={int(b.roll)}g thr={b.thr}")
|
||||
print(f" Sharpe in-sample {ins[best_i]:+.2f} -> FULL {bs['sharpe']:+.2f} "
|
||||
f"HOLD-OUT {bs['holdout']:+.2f} maxDD {bs['maxdd']*100:.2f}% "
|
||||
f"CAGR {bs['cagr']*100:+.2f}% vol {bs['vol']*100:.2f}%")
|
||||
best_full = G.sharpe.idxmax()
|
||||
print(f" (per confronto, la cella col miglior FULL sarebbe stata "
|
||||
f"{G.loc[best_full,'family']} {G.loc[best_full,'sig']} "
|
||||
f"Sh {G.loc[best_full,'sharpe']:+.2f} — NON e' quella scelta)")
|
||||
|
||||
# ---------------- STEP 6: GATE ----------------
|
||||
print("\n[6] GATE")
|
||||
dsr, sr0 = A.deflated_sharpe(bs["sharpe"], list(G.sharpe.values), bd)
|
||||
print(f" deflated-Sharpe : DSR {dsr:.3f} (null max atteso {sr0:+.2f} su {len(G)} trial) "
|
||||
f"-> {'PASS' if dsr >= 0.95 else 'FAIL'}")
|
||||
|
||||
imp = A.implausible_sharpe(bd)
|
||||
print(f" implausible_sharpe: implausible={imp['implausible']} "
|
||||
f"Sh {imp.get('sharpe', float('nan')):+.2f} maxDD {imp.get('maxdd', float('nan'))*100:.2f}% "
|
||||
f"quota barre in perdita {imp.get('loss_frac', float('nan'))*100:.1f}% "
|
||||
f"Calmar {imp.get('calmar', float('nan')):.1f}")
|
||||
if imp["reasons"]:
|
||||
for rr in imp["reasons"]:
|
||||
print(f" - {rr}")
|
||||
|
||||
tp = A.tp01_baseline_daily()
|
||||
try:
|
||||
mg = A.marginal_vs_tp01(bd)
|
||||
print(f" marginal_vs_tp01 : {mg.get('verdict')} corr {mg.get('corr', float('nan')):+.3f} "
|
||||
f"robust_oos={mg.get('robust_oos')} insample_edge={mg.get('has_insample_edge')} "
|
||||
f"is_hedge={mg.get('is_hedge')} beats_noise={mg.get('beats_noise_null')}")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
mg = {}
|
||||
print(f" marginal_vs_tp01 : NON GIRATO ({type(exc).__name__}: {exc})")
|
||||
|
||||
dn = delevering_null(bd, tp)
|
||||
print(f" null de-levering : refuted={dn.get('refuted')} {dn}")
|
||||
|
||||
# anchor: l'ora del giorno in cui si valuta il segnale
|
||||
print(" anchor_luck_band (ora del giorno di decisione, 24 ancore):")
|
||||
def by_off(off):
|
||||
hs = []
|
||||
for a in ASSETS:
|
||||
cv = cvcache[(a, int(b.roll))]
|
||||
h = run_strategy(cv, panels[a], b.family, b.sig, int(b.win), float(b.thr), slip_est)
|
||||
hh = h.copy()
|
||||
mask = (hh.index.hour != off)
|
||||
hh[mask] = 0.0 # decide/agisce solo a quell'ora
|
||||
hs.append(hh)
|
||||
return to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1))
|
||||
try:
|
||||
ab = A.anchor_luck_band(by_off, list(range(24)), canonical=0)
|
||||
print(f" canonica {ab.get('canonical'):+.3f} MEDIANA(onesta) {ab.get('median'):+.3f} "
|
||||
f"banda [{ab.get('p10'):+.3f},{ab.get('p90'):+.3f}] "
|
||||
f"frazione>0 {ab.get('frac_positive'):.2f} gate_pass={ab.get('gate_pass')}")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f" NON GIRATO ({type(exc).__name__}: {exc})")
|
||||
|
||||
try:
|
||||
print(f" causality_ok : la posizione e' shiftata in run_strategy "
|
||||
f"(held = pos.shift(1)); verifica strutturale, non A.causality_ok "
|
||||
f"(che vuole un target_fn su BTC/ETH direzionale)")
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
# ---------------- STEP 7: quanto spread uccide l'edge ----------------
|
||||
print("\n[7] A CHE SPREAD MUORE L'EDGE (la stima di slippage e' il rischio #1)")
|
||||
for sl in [0.0, 1.0, 2.5, 5.0, 7.5, 10.0, 20.0]:
|
||||
hs = []
|
||||
for a in ASSETS:
|
||||
hs.append(run_strategy(cvcache[(a, int(b.roll))], panels[a], b.family,
|
||||
b.sig, int(b.win), float(b.thr), sl))
|
||||
s2 = stats(to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1)))
|
||||
mark = " <= stima di oggi" if abs(sl - slip_est) < 0.6 else ""
|
||||
print(f" mezzo-spread {sl:5.1f} bps/lato -> Sharpe {s2['sharpe']:+6.2f} "
|
||||
f"CAGR {s2['cagr']*100:+6.2f}%{mark}")
|
||||
|
||||
# ---------------- STEP 8: eseguibilita' ----------------
|
||||
print("\n[8] Q3 — ESEGUIBILITA' a $600-3.000")
|
||||
for a in ASSETS:
|
||||
try:
|
||||
sp = _api("/public/get_instrument", instrument_name=f"{a}-25DEC26")["result"]
|
||||
print(f" {a}-25DEC26: min_trade_amount ${sp['min_trade_amount']:,.0f} di nozionale, "
|
||||
f"contract_size ${sp['contract_size']:,.0f}, tick {sp['tick_size']}, "
|
||||
f"taker {sp['taker_commission']*1e4:.1f} bps")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f" {a}: specs non lette ({type(exc).__name__})")
|
||||
print(f" vol ANNUA del candidato per $1 di nozionale lordo/gamba: {bs['vol']*100:.2f}%")
|
||||
if bs["vol"] > 0:
|
||||
lev = 0.20 / bs["vol"]
|
||||
print(f" per portarlo al vol-target 20% del progetto servirebbe leva "
|
||||
f"{lev:.1f}x per gamba = ${lev*2:,.0f} di nozionale LORDO per $1 di conto")
|
||||
for cap in (600, 3000):
|
||||
print(f" a ${cap}: nozionale lordo ${lev*2*cap:,.0f} "
|
||||
f"(margine ~2-4%/gamba => ${lev*2*cap*0.03:,.0f} di IM richiesto)")
|
||||
|
||||
print(f"\n[fine] {time.time()-t_start:.0f}s")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,567 @@
|
||||
"""HL-EXEC (2026-08-22) — AUDIT DI FATTO: le regole VERE di Hyperliquid contro le soglie ASSUNTE.
|
||||
|
||||
DOMANDA. Il progetto tiene due edge cross-sectional fuori dal libro **per taglia**:
|
||||
XS01 "serve ~$20.000" (origine: diario 2026-06-19-hyperliquid-xsec, "rumore arrotondamento" —
|
||||
una STIMA A OCCHIO, mai calcolata)
|
||||
XSR01 "diventa reale a ~$5.000" + gate pre-registrato 2026-10-23 con soglia
|
||||
"haircut di eseguibilita' a $5.000 <= 40%, altrimenti RITIRO"
|
||||
Entrambe sono calcolate col pavimento **min_order $5**, che e' il minimo di **DERIBIT**.
|
||||
XS01/XSR01 si eseguirebbero su **HYPERLIQUID**, che ha regole sue. Qui le regole si LEGGONO dal
|
||||
venue invece di assumerle, e si dice quali conclusioni del progetto cambiano.
|
||||
|
||||
NON manda ordini, non tocca il conto: sole letture PUBBLICHE (nessuna chiave, nessuna firma).
|
||||
|
||||
GRADO DELLE FONTI (dichiarato per ogni numero, come chiede il brief):
|
||||
[A] letto dall'API pubblica del venue api.hyperliquid.xyz/info (meta, metaAndAssetCtxs,
|
||||
userFees, l2Book)
|
||||
[B] derivato da [A] + verificato empiricamente sui book live
|
||||
[C] documentazione ufficiale (hyperliquid.gitbook.io)
|
||||
[D] assunto / ereditato dal progetto
|
||||
La verifica MAINNET non e' un atto di fede: i mark price dell'API si incrociano con l'ultima
|
||||
chiusura del feed CERTIFICATO su disco (data/raw/hl_*_1d.parquet). Il testnet non puo' superarlo.
|
||||
(Il progetto ha gia' pagato il prezzo di un feed testnet creduto vero: e' la causa del reset v2.0.0.)
|
||||
|
||||
nice -n 19 timeout 900 uv run python scripts/research/r0822_hl_exec.py
|
||||
... --no-net usa solo la cache su disco (nessuna chiamata di rete)
|
||||
... --snaps N quanti snapshot del book L2 (default 3, ~45s di distanza)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
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))
|
||||
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research"))
|
||||
|
||||
RAW = PROJECT_ROOT / "data" / "raw"
|
||||
CACHE = Path(os.environ.get("HLEXEC_CACHE", "/tmp/claude-1001/-opt-docker-PythagorasGoal/"
|
||||
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/hlexec"))
|
||||
INFO_URL = "https://api.hyperliquid.xyz/info" # MAINNET (il testnet e' api.hyperliquid-testnet.xyz)
|
||||
|
||||
# --- parametri sotto esame ----------------------------------------------------------------------
|
||||
MIN_ORDER_ASSUNTO = 5.0 # [D] pavimento DERIBIT usato da eval_weights_smallcap e da paper_xsr
|
||||
MIN_ORDER_HL = 10.0 # [C] "Order must have minimum value of $10." (docs/error-responses)
|
||||
FEE_LEG_MODELLO = 0.0005 # [D] 0.05%/gamba, config CONGELATA di XSR01
|
||||
XSR_SOGLIA_HAIRCUT = 0.40 # [D] soglia pre-registrata del gate 2026-10-23
|
||||
CAPITALI = (600.0, 1000.0, 2000.0, 3000.0, 5000.0, 10000.0, 20000.0, 50000.0)
|
||||
TICKETS = (50.0, 100.0, 300.0)
|
||||
ANN = np.sqrt(365.0)
|
||||
|
||||
|
||||
# ==================================================================================================
|
||||
# 0. RETE — letture pubbliche, con cache su disco (una corsa non deve ri-martellare il venue)
|
||||
# ==================================================================================================
|
||||
def _post(payload: dict, timeout: int = 30) -> object:
|
||||
req = urllib.request.Request(INFO_URL, data=json.dumps(payload).encode(),
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return json.loads(r.read().decode())
|
||||
|
||||
|
||||
def cached(name: str, payload: dict, no_net: bool, ttl_s: float = 6 * 3600) -> object:
|
||||
CACHE.mkdir(parents=True, exist_ok=True)
|
||||
f = CACHE / f"{name}.json"
|
||||
if f.exists() and (no_net or (time.time() - f.stat().st_mtime) < ttl_s):
|
||||
return json.loads(f.read_text())
|
||||
if no_net:
|
||||
raise FileNotFoundError(f"cache assente per {name} e --no-net attivo")
|
||||
d = _post(payload)
|
||||
f.write_text(json.dumps(d))
|
||||
return d
|
||||
|
||||
|
||||
def universo_certificato() -> list[str]:
|
||||
"""I 51 alt+BTC gia' CERTIFICATI su disco. Non si inventa un universo: si legge quello vero."""
|
||||
return sorted(p.stem.replace("hl_", "").replace("_1d", "").upper()
|
||||
for p in RAW.glob("hl_*_1d.parquet"))
|
||||
|
||||
|
||||
# ==================================================================================================
|
||||
# 1. REGOLE DEL VENUE
|
||||
# ==================================================================================================
|
||||
def sig_figs_ok(px_str: str, sz_dec: int, max_dec: int = 6) -> bool:
|
||||
"""[C] regola tick perp: <=5 cifre significative E <= (MAX_DECIMALS - szDecimals) decimali;
|
||||
i prezzi INTERI sono sempre ammessi. Qui serve per VERIFICARLA sui book veri -> [B]."""
|
||||
s = px_str.lstrip("-")
|
||||
dec = len(s.split(".")[1]) if "." in s else 0
|
||||
if dec == 0:
|
||||
return True
|
||||
digits = s.replace(".", "").lstrip("0")
|
||||
return len(digits.rstrip("0")) <= 5 and dec <= (max_dec - sz_dec)
|
||||
|
||||
|
||||
def leggi_regole(no_net: bool) -> tuple[pd.DataFrame, dict]:
|
||||
meta_ctx = cached("metaAndAssetCtxs", {"type": "metaAndAssetCtxs"}, no_net)
|
||||
fees = cached("userFees", {"type": "userFees",
|
||||
"user": "0x0000000000000000000000000000000000000001"}, no_net)
|
||||
uni, ctxs = meta_ctx[0]["universe"], meta_ctx[1]
|
||||
rows = []
|
||||
for a, c in zip(uni, ctxs):
|
||||
mark = float(c["markPx"]) if c.get("markPx") else np.nan
|
||||
rows.append(dict(sym=a["name"], szDecimals=int(a["szDecimals"]),
|
||||
maxLeverage=int(a["maxLeverage"]),
|
||||
delisted=bool(a.get("isDelisted", False)),
|
||||
markPx=mark, dayNtlVlm=float(c.get("dayNtlVlm") or np.nan),
|
||||
openInterest=float(c.get("openInterest") or np.nan)))
|
||||
df = pd.DataFrame(rows).set_index("sym")
|
||||
df["lot_units"] = 10.0 ** (-df["szDecimals"]) # [A] passo di size in unita'
|
||||
df["lot_usd"] = df["lot_units"] * df["markPx"] # [B] passo di size in dollari
|
||||
df["px_decimals_max"] = (6 - df["szDecimals"]).clip(lower=0) # [C]
|
||||
return df, fees["feeSchedule"]
|
||||
|
||||
|
||||
def verifica_mainnet(df: pd.DataFrame, syms: list[str]) -> pd.DataFrame:
|
||||
"""Incrocia i mark dell'API con l'ULTIMA chiusura del feed certificato su disco.
|
||||
Un feed testnet non puo' superare questo controllo: i suoi prezzi sono fantasia."""
|
||||
out = []
|
||||
for s in syms:
|
||||
p = RAW / f"hl_{s.lower()}_1d.parquet"
|
||||
if not p.exists() or s not in df.index:
|
||||
continue
|
||||
d = pd.read_parquet(p, columns=["timestamp", "close"])
|
||||
last_close = float(d["close"].iloc[-1])
|
||||
last_ts = pd.Timestamp(int(d["timestamp"].iloc[-1]), unit="ms", tz="UTC")
|
||||
mk = float(df.loc[s, "markPx"])
|
||||
out.append(dict(sym=s, disco=last_close, api=mk,
|
||||
dev_pct=100.0 * (mk - last_close) / last_close,
|
||||
eta_h=(pd.Timestamp.now("UTC") - last_ts) / pd.Timedelta("1h")))
|
||||
return pd.DataFrame(out).set_index("sym")
|
||||
|
||||
|
||||
# ==================================================================================================
|
||||
# 2. BOOK L2 -> spread, profondita', costo di un ticket
|
||||
# ==================================================================================================
|
||||
def scarica_book(syms: list[str], snaps: int, no_net: bool, pause: float = 0.25,
|
||||
gap_s: float = 45.0) -> dict[str, list[dict]]:
|
||||
books: dict[str, list[dict]] = {s: [] for s in syms}
|
||||
for k in range(snaps):
|
||||
if k and not no_net:
|
||||
time.sleep(gap_s)
|
||||
for s in syms:
|
||||
try:
|
||||
b = cached(f"l2_{s}_{k}", {"type": "l2Book", "coin": s}, no_net)
|
||||
except Exception as e: # un book mancante e' un dato, non un crash
|
||||
print(f" [book] {s} snap{k}: {type(e).__name__}")
|
||||
continue
|
||||
books[s].append(b)
|
||||
if not no_net:
|
||||
time.sleep(pause) # pacing: il rate limit e' PER-IP e condiviso
|
||||
return books
|
||||
|
||||
|
||||
def costo_ticket(levels: list[dict], mid: float, notional: float, side: str) -> tuple[float, bool]:
|
||||
"""VWAP di un ordine marketable da `notional` dollari contro il book, in bps DA MID.
|
||||
Ritorna (bps, book_esaurito). Per ticket piccoli converge al MEZZO SPREAD: e' giusto cosi',
|
||||
a $14 non si 'cammina' il book, si paga il livello top."""
|
||||
resid, cost, filled = notional, 0.0, 0.0
|
||||
for lv in levels:
|
||||
px, sz = float(lv["px"]), float(lv["sz"])
|
||||
cap = px * sz
|
||||
take = min(resid, cap)
|
||||
cost += take
|
||||
filled += take / px
|
||||
resid -= take
|
||||
if resid <= 1e-9:
|
||||
break
|
||||
if filled <= 0:
|
||||
return float("nan"), True
|
||||
vwap = cost / filled
|
||||
bps = (vwap - mid) / mid * 1e4 * (1.0 if side == "buy" else -1.0)
|
||||
return bps, resid > 1e-6
|
||||
|
||||
|
||||
def misura_liquidita(books: dict[str, list[dict]], meta: pd.DataFrame) -> pd.DataFrame:
|
||||
rows = []
|
||||
for s, snaps in books.items():
|
||||
if not snaps:
|
||||
continue
|
||||
rec: dict[str, list[float]] = {"spread": [], "d10": [], "viol": []}
|
||||
for t in TICKETS:
|
||||
rec[f"t{int(t)}"] = []
|
||||
for b in snaps:
|
||||
bids, asks = b["levels"][0], b["levels"][1]
|
||||
if not bids or not asks:
|
||||
continue
|
||||
bb, ba = float(bids[0]["px"]), float(asks[0]["px"])
|
||||
mid = 0.5 * (bb + ba)
|
||||
rec["spread"].append((ba - bb) / mid * 1e4)
|
||||
near = sum(float(l["px"]) * float(l["sz"]) for l in asks
|
||||
if (float(l["px"]) - mid) / mid <= 0.0010)
|
||||
near += sum(float(l["px"]) * float(l["sz"]) for l in bids
|
||||
if (mid - float(l["px"])) / mid <= 0.0010)
|
||||
rec["d10"].append(near)
|
||||
sd = int(meta.loc[s, "szDecimals"]) if s in meta.index else 0
|
||||
rec["viol"].append(sum(0 if sig_figs_ok(l["px"], sd) else 1 for l in bids + asks))
|
||||
for t in TICKETS:
|
||||
bb_, _ = costo_ticket(asks, mid, t, "buy")
|
||||
sb_, _ = costo_ticket(bids, mid, t, "sell")
|
||||
if np.isfinite(bb_) and np.isfinite(sb_):
|
||||
rec[f"t{int(t)}"].append(0.5 * (bb_ + sb_))
|
||||
if not rec["spread"]:
|
||||
continue
|
||||
row = dict(sym=s, n_snap=len(rec["spread"]),
|
||||
spread_bps=float(np.median(rec["spread"])),
|
||||
half_spread_bps=float(np.median(rec["spread"])) / 2.0,
|
||||
depth10bps_usd=float(np.median(rec["d10"])),
|
||||
tick_viol=int(sum(rec["viol"])))
|
||||
for t in TICKETS:
|
||||
v = rec[f"t{int(t)}"]
|
||||
row[f"slip{int(t)}_bps"] = float(np.median(v)) if v else np.nan
|
||||
rows.append(row)
|
||||
return pd.DataFrame(rows).set_index("sym").sort_values("spread_bps")
|
||||
|
||||
|
||||
def min_notional_osservato(books: dict[str, list[dict]]) -> dict:
|
||||
"""Controllo EMPIRICO del pavimento $10: i livelli con n==1 sono UN ordine solo.
|
||||
Un ordine singolo sotto $10 non falsifica la regola (i fill parziali erodono un resto),
|
||||
ma il PAVIMENTO della distribuzione dice dove il venue taglia."""
|
||||
vals = []
|
||||
for snaps in books.values():
|
||||
for b in snaps:
|
||||
for side in b["levels"]:
|
||||
for l in side:
|
||||
if int(l.get("n", 0)) == 1:
|
||||
vals.append(float(l["px"]) * float(l["sz"]))
|
||||
v = np.array(vals, float)
|
||||
if not len(v):
|
||||
return {}
|
||||
return dict(n=len(v), minimo=float(v.min()), p01=float(np.percentile(v, 1)),
|
||||
p05=float(np.percentile(v, 5)), mediana=float(np.median(v)),
|
||||
sotto10_pct=100.0 * float((v < 10.0).mean()),
|
||||
sotto5_pct=100.0 * float((v < 5.0).mean()))
|
||||
|
||||
|
||||
# ==================================================================================================
|
||||
# 3. SIMULATORE DI LIBRO A N GAMBE (la contabilita' e' quella di paper_xsr._step, generalizzata)
|
||||
# ==================================================================================================
|
||||
def sim_libro(Weff: np.ndarray, R: np.ndarray, r_hedge: np.ndarray | None, cap0: float,
|
||||
min_order: float | None, lot_usd: np.ndarray | None, px: np.ndarray | None,
|
||||
fee_leg: float | np.ndarray) -> dict:
|
||||
"""Un passo per barra. Una gamba il cui |dw|*capitale sta sotto min_order NON si muove
|
||||
(stessa convenzione di altlib.eval_weights_smallcap e di scripts/live/paper_xsr._step).
|
||||
In piu': la size si arrotonda al LOTTO del venue -> se arrotonda a zero, non si esegue.
|
||||
`fee_leg` puo' essere uno scalare o un vettore per-gamba (costo asset-specifico)."""
|
||||
n, k = Weff.shape
|
||||
w = np.zeros(k)
|
||||
cap = cap0
|
||||
eq = np.empty(n)
|
||||
nets = np.zeros(n)
|
||||
n_fill = n_skip = n_lot0 = 0
|
||||
fee = np.broadcast_to(np.asarray(fee_leg, float).ravel(), (k,)) if np.ndim(fee_leg) else None
|
||||
for i in range(n):
|
||||
ret = float(np.dot(w, R[i]))
|
||||
if r_hedge is not None:
|
||||
ret -= float(w.sum()) * r_hedge[i]
|
||||
w_t = Weff[i]
|
||||
if min_order is None:
|
||||
w_new = w_t.copy()
|
||||
else:
|
||||
move = np.abs(w_t - w) * cap >= min_order
|
||||
if lot_usd is not None and px is not None:
|
||||
# arrotondamento al lotto: la size eseguita e' un multiplo di lot_units
|
||||
dn = np.abs(w_t - w) * cap
|
||||
lots = np.floor(dn / np.maximum(lot_usd, 1e-12))
|
||||
zero = move & (lots < 1)
|
||||
n_lot0 += int(zero.sum())
|
||||
move = move & (lots >= 1)
|
||||
w_new = np.where(move, w_t, w)
|
||||
n_fill += int(move.sum())
|
||||
n_skip += int((~move).sum())
|
||||
d = np.abs(w_new - w)
|
||||
c = float((d * fee).sum()) if fee is not None else float(fee_leg) * float(d.sum())
|
||||
if r_hedge is not None: # la gamba di copertura paga anch'essa
|
||||
dh = abs(float(w_new.sum()) - float(w.sum()))
|
||||
c += (float(fee.mean()) if fee is not None else float(fee_leg)) * dh
|
||||
net = ret - c
|
||||
nets[i] = net
|
||||
cap *= (1.0 + max(net, -0.99))
|
||||
eq[i] = cap
|
||||
w = w_new
|
||||
tot = n_fill + n_skip
|
||||
return dict(net=nets, eq=eq, n_fill=n_fill, n_skip=n_skip, n_lot0=n_lot0,
|
||||
pct_eseguite=(100.0 * n_fill / tot) if tot else np.nan)
|
||||
|
||||
|
||||
def metriche(net: np.ndarray, idx: pd.DatetimeIndex) -> dict:
|
||||
x = np.asarray(net, float)
|
||||
sd = x.std(ddof=1)
|
||||
sh = float(x.mean() / sd * ANN) if sd > 0 else 0.0
|
||||
eq = np.cumprod(1.0 + np.clip(x, -0.99, None))
|
||||
dd = float((1.0 - eq / np.maximum.accumulate(eq)).max())
|
||||
yrs = max((idx[-1] - idx[0]) / pd.Timedelta("365.25D"), 1e-9)
|
||||
cagr = float(eq[-1] ** (1.0 / yrs) - 1.0)
|
||||
return dict(sharpe=round(sh, 3), maxdd=round(100 * dd, 1), cagr=round(100 * cagr, 1))
|
||||
|
||||
|
||||
# ==================================================================================================
|
||||
# 4. XS01 — ricostruzione dei PESI (il sleeve espone solo i ritorni) + prova d'identita'
|
||||
# ==================================================================================================
|
||||
def xs01_pesi():
|
||||
from src.portfolio.sleeves import XS_CFG, XS_UNIVERSE, _xsec_returns
|
||||
cols = {}
|
||||
for sym in XS_UNIVERSE:
|
||||
p = RAW / f"hl_{sym.lower()}_1d.parquet"
|
||||
if not p.exists():
|
||||
continue
|
||||
d = pd.read_parquet(p)
|
||||
cols[sym] = 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()
|
||||
px = C.values
|
||||
n, A = px.shape
|
||||
lb, H, k, mode, tv = (XS_CFG["lookbacks"], XS_CFG["H"], XS_CFG["k"],
|
||||
XS_CFG["mode"], XS_CFG["target_vol"])
|
||||
dp, mh = XS_CFG.get("disp_pct", 0), XS_CFG.get("disp_minhist", 20)
|
||||
mlb = max(lb)
|
||||
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
|
||||
W = np.zeros((n, A))
|
||||
w = np.zeros(A)
|
||||
hist: list[float] = []
|
||||
for i in range(n):
|
||||
if i >= mlb and i % H == 0:
|
||||
rLs = [px[i] / px[i - L] - 1.0 for L in lb]
|
||||
di = float(np.mean([r.std() for r in rLs]))
|
||||
thr = np.percentile(hist, dp) if (dp > 0 and len(hist) >= mh) else -np.inf
|
||||
if di >= thr:
|
||||
sc = np.zeros(A); cnt = 0
|
||||
for rL in rLs:
|
||||
sd = rL.std()
|
||||
if sd > 0:
|
||||
sc += (rL - rL.mean()) / sd; cnt += 1
|
||||
if cnt:
|
||||
sc /= cnt
|
||||
o = np.argsort(sc); w = np.zeros(A); lo, hi = o[:k], o[-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
|
||||
else:
|
||||
w = np.zeros(A)
|
||||
hist.append(di)
|
||||
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)
|
||||
# prova d'identita' col sleeve UFFICIALE (max|diff| deve essere 0.0)
|
||||
off = _xsec_returns()
|
||||
dif = float(np.abs(pd.Series(s.values * scale, index=C.index).reindex(off.index).values
|
||||
- off.values).max())
|
||||
# posizione TENUTA durante la barra i = W[i-1]*scale[i] => posta a fine barra i: W[i]*scale[i+1]
|
||||
pos_scale = np.concatenate([scale[1:], scale[-1:]])
|
||||
return W * pos_scale[:, None], dret, C, list(C.columns), dif
|
||||
|
||||
|
||||
# ==================================================================================================
|
||||
# MAIN
|
||||
# ==================================================================================================
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--no-net", action="store_true")
|
||||
ap.add_argument("--snaps", type=int, default=3)
|
||||
a = ap.parse_args()
|
||||
pd.set_option("display.width", 200)
|
||||
|
||||
print("=" * 104)
|
||||
print(" HL-EXEC — le regole VERE di Hyperliquid contro le soglie ASSUNTE di XS01 / XSR01")
|
||||
print("=" * 104)
|
||||
|
||||
# ---------------------------------------------------------------- 1. regole
|
||||
meta, fee_sched = leggi_regole(a.no_net)
|
||||
syms = universo_certificato()
|
||||
print(f"\n[1] REGOLE DEL VENUE (fonte: {INFO_URL} = MAINNET)")
|
||||
print(f" universo perp quotato: {len(meta)} strumenti; certificati da noi: {len(syms)}")
|
||||
mancanti = [s for s in syms if s not in meta.index]
|
||||
delist = [s for s in syms if s in meta.index and bool(meta.loc[s, "delisted"])]
|
||||
print(f" certificati assenti dal venue: {mancanti or 'nessuno'} delistati: {delist or 'nessuno'}")
|
||||
|
||||
ck = verifica_mainnet(meta, syms)
|
||||
print(f"\n [A] verifica MAINNET (mark API vs ultima chiusura del feed certificato, "
|
||||
f"eta' feed {ck['eta_h'].median():.0f}h):")
|
||||
print(f" deviazione |%| mediana {ck['dev_pct'].abs().median():.2f}% "
|
||||
f"max {ck['dev_pct'].abs().max():.2f}% ({ck['dev_pct'].abs().idxmax()}) "
|
||||
f"asset controllati {len(ck)}")
|
||||
print(" -> un feed TESTNET non supera questo controllo: i suoi prezzi sono scollegati.")
|
||||
|
||||
tk = fee_sched
|
||||
print(f"\n [A] FEE perp lette dal venue (tier BASE, nessuna chiave):")
|
||||
print(f" taker (cross) {float(tk['cross'])*1e4:.2f} bps/lato "
|
||||
f"maker (add) {float(tk['add'])*1e4:.2f} bps/lato")
|
||||
print(f" primo scaglione VIP a ${float(tk['tiers']['vip'][0]['ntlCutoff']):,.0f} di "
|
||||
f"volume 14g -> a $600-20k si sta SEMPRE al tier base.")
|
||||
print(f" [C] la doc (gitbook/trading/fees) dichiara 0.045% / 0.015%: **le due fonti "
|
||||
f"coincidono**.")
|
||||
print(f" [C] pavimento d'ordine perp: \"Order must have minimum value of $10.\" "
|
||||
f"(docs/for-developers/api/error-responses); nessuna esenzione documentata per reduce-only.")
|
||||
print(f" [C] tick: <=5 cifre significative E <= (6 - szDecimals) decimali; size arrotondata "
|
||||
f"a szDecimals.")
|
||||
|
||||
sub = meta.loc[[s for s in syms if s in meta.index]].copy()
|
||||
print(f"\n [A/B] LOTTO per asset (passo di size) — i 6 piu' grossolani e i 6 piu' fini:")
|
||||
o = sub.sort_values("lot_usd", ascending=False)
|
||||
for who, part in (("piu' grossolano", o.head(6)), ("piu' fine", o.tail(6))):
|
||||
for s, r in part.iterrows():
|
||||
print(f" {s:<6} szDec={int(r.szDecimals)} lotto {r.lot_units:>10.5f} unita' = "
|
||||
f"${r.lot_usd:>8.4f} mark ${r.markPx:<12.6g} ({who})")
|
||||
print(f" lotto in $: mediana ${sub['lot_usd'].median():.4f}, max "
|
||||
f"${sub['lot_usd'].max():.2f} ({sub['lot_usd'].idxmax()})")
|
||||
print(f" -> il LOTTO non e' mai il vincolo: max ${sub['lot_usd'].max():.2f} << $10 "
|
||||
f"di pavimento. Il vincolo e' il MIN NOTIONAL.")
|
||||
print(f" [A] leva massima: BTC {int(meta.loc['BTC','maxLeverage'])}x, "
|
||||
f"ETH {int(meta.loc['ETH','maxLeverage'])}x, mediana alt certificati "
|
||||
f"{int(sub['maxLeverage'].median())}x, minimo {int(sub['maxLeverage'].min())}x")
|
||||
|
||||
# ---------------------------------------------------------------- 2. book
|
||||
print(f"\n[2] BOOK L2 — spread, profondita', costo di un ticket ({a.snaps} snapshot per asset)")
|
||||
books = scarica_book(syms, a.snaps, a.no_net)
|
||||
liq = misura_liquidita(books, meta)
|
||||
mn = min_notional_osservato(books)
|
||||
if mn:
|
||||
print(f" [B] controllo empirico del pavimento $10 su {mn['n']} livelli a UN SOLO ordine "
|
||||
f"(n==1):")
|
||||
print(f" minimo ${mn['minimo']:.2f} p01 ${mn['p01']:.2f} p05 ${mn['p05']:.2f} "
|
||||
f"mediana ${mn['mediana']:.0f} sotto $10: {mn['sotto10_pct']:.1f}% "
|
||||
f"sotto $5: {mn['sotto5_pct']:.1f}%")
|
||||
print(f" (un residuo sotto soglia NON falsifica la regola — i fill parziali erodono "
|
||||
f"un ordine gia' piazzato — ma il pavimento della distribuzione dice dove taglia.)")
|
||||
viol = int(liq["tick_viol"].sum())
|
||||
print(f" [B] regola tick verificata sui book veri: {viol} violazioni su "
|
||||
f"{int(liq['n_snap'].sum())*40} livelli letti -> la regola [C] e' CONFERMATA dal venue.")
|
||||
|
||||
print(f"\n spread e slippage (mediana degli snapshot; bps DA MID, un solo lato):")
|
||||
print(f" {'sym':<7}{'vol24h $':>13}{'spread':>9}{'1/2 spr':>9}"
|
||||
f"{'prof<10bp':>11}{'$50':>8}{'$100':>8}{'$300':>8}")
|
||||
for s, r in liq.iterrows():
|
||||
print(f" {s:<7}{r.dayNtlVlm if 'dayNtlVlm' in r else meta.loc[s,'dayNtlVlm']:>13,.0f}"
|
||||
f"{r.spread_bps:>8.1f}{r.half_spread_bps:>9.1f}{r.depth10bps_usd:>11,.0f}"
|
||||
f"{r.slip50_bps:>8.1f}{r.slip100_bps:>8.1f}{r.slip300_bps:>8.1f}")
|
||||
|
||||
XS19 = ["BTC", "ETH", "SOL", "BNB", "XRP", "DOGE", "AVAX", "LINK", "LTC", "ADA",
|
||||
"ARB", "OP", "SUI", "APT", "INJ", "TIA", "SEI", "NEAR", "AAVE"]
|
||||
maj = liq.reindex([s for s in XS19 if s in liq.index])
|
||||
coda = liq.drop(index=maj.index, errors="ignore")
|
||||
print(f"\n riepilogo (mediana | p90):")
|
||||
for nm, part in (("19 major (XS01)", maj), (f"coda ({len(coda)} alt, solo XSR01)", coda)):
|
||||
if len(part):
|
||||
print(f" {nm:<26} spread {part.spread_bps.median():>5.1f} | "
|
||||
f"{part.spread_bps.quantile(.9):>5.1f} bps "
|
||||
f"slip$100 {part.slip100_bps.median():>5.1f} | "
|
||||
f"{part.slip100_bps.quantile(.9):>5.1f} "
|
||||
f"slip$300 {part.slip300_bps.median():>5.1f} | "
|
||||
f"{part.slip300_bps.quantile(.9):>5.1f}")
|
||||
|
||||
# ---------------------------------------------------------------- 3. XS01
|
||||
print(f"\n[3] XS01 — soglia PUBBLICATA ~$20.000 (origine: stima a occhio, 'rumore arrotondamento')")
|
||||
Wxs, dret, C, cols, dif = xs01_pesi()
|
||||
print(f" prova d'identita' col sleeve ufficiale `_xsec_returns()`: max|diff| = {dif:.3e}")
|
||||
gross_xs = np.abs(Wxs).sum(axis=1)
|
||||
att = gross_xs > 1e-9
|
||||
dW = np.abs(np.diff(Wxs, axis=0, prepend=np.zeros((1, Wxs.shape[1]))))
|
||||
tk_nz = dW[dW > 1e-9]
|
||||
print(f" gambe simultanee: {int((np.abs(Wxs) > 1e-9).sum(axis=1)[att].max())} "
|
||||
f"(k=5 long + 5 short); lordo mediano {np.median(gross_xs[att]):.2f}x il capitale "
|
||||
f"(vol-target, cap 3x)")
|
||||
print(f" peso mediano mosso per gamba per ribilanciamento: {np.median(tk_nz)*100:.2f}% "
|
||||
f"del capitale; 10° pctl {np.percentile(tk_nz,10)*100:.2f}%")
|
||||
print(f"\n ticket per gamba (mediano) e % di gambe ESEGUIBILI, per capitale allocato a XS01:")
|
||||
print(f" {'cap XS01':>10}{'ticket med':>12}{'ticket p10':>12}"
|
||||
f"{'>=$5 [D]':>10}{'>=$10 [C]':>11}{'lotto ko':>10}")
|
||||
R_xs = dret
|
||||
idx = C.index
|
||||
base = None
|
||||
for cap in CAPITALI:
|
||||
med = np.median(tk_nz) * cap
|
||||
p10 = np.percentile(tk_nz, 10) * cap
|
||||
pct5 = 100.0 * float((tk_nz * cap >= MIN_ORDER_ASSUNTO).mean())
|
||||
pct10 = 100.0 * float((tk_nz * cap >= MIN_ORDER_HL).mean())
|
||||
lot = np.array([meta.loc[s, "lot_usd"] if s in meta.index else 0.0 for s in cols])
|
||||
lotko = 100.0 * float((tk_nz * cap < np.median(lot)).mean())
|
||||
print(f" {cap:>10,.0f}{med:>12.2f}{p10:>12.2f}{pct5:>9.0f}%{pct10:>10.0f}%{lotko:>9.1f}%")
|
||||
lot_xs = np.array([float(meta.loc[s, "lot_usd"]) if s in meta.index else 0.0 for s in cols])
|
||||
px_xs = np.array([float(meta.loc[s, "markPx"]) if s in meta.index else np.nan for s in cols])
|
||||
fee_xs_mod = 0.001 / 2.0 # [D] cio' che il sleeve modella: 5 bps/lato
|
||||
print(f"\n haircut di eseguibilita' (Sharpe modellato - Sharpe realistico), fee {fee_xs_mod*1e4:.1f} bps/lato:")
|
||||
print(f" {'cap XS01':>10}{'Sh mod':>9}{'Sh $5 [D]':>11}{'Sh $10 [C]':>12}"
|
||||
f"{'haircut $10':>13}{'esegui%':>9}")
|
||||
for cap in CAPITALI:
|
||||
m0 = sim_libro(Wxs, R_xs, None, cap, None, None, None, fee_xs_mod)
|
||||
m5 = sim_libro(Wxs, R_xs, None, cap, MIN_ORDER_ASSUNTO, None, None, fee_xs_mod)
|
||||
m10 = sim_libro(Wxs, R_xs, None, cap, MIN_ORDER_HL, lot_xs, px_xs, fee_xs_mod)
|
||||
a0, a5, a10 = (metriche(m["net"], idx) for m in (m0, m5, m10))
|
||||
hc = (a0["sharpe"] - a10["sharpe"]) / abs(a0["sharpe"]) * 100 if a0["sharpe"] else np.nan
|
||||
print(f" {cap:>10,.0f}{a0['sharpe']:>9.2f}{a5['sharpe']:>11.2f}{a10['sharpe']:>12.2f}"
|
||||
f"{hc:>12.0f}%{m10['pct_eseguite']:>9.0f}%")
|
||||
|
||||
# ---------------------------------------------------------------- 4. XSR01
|
||||
print(f"\n[4] XSR01 — soglia PUBBLICATA ~$5.000, gate 2026-10-23 con haircut a $5.000 <= 40%")
|
||||
from scripts.live.paper_xsr import build_panel # stessa costruzione del monitor in produzione
|
||||
ts, dtx, Wx, Rx, rbx, sx = build_panel()
|
||||
idxx = pd.DatetimeIndex(dtx)
|
||||
lot_x = np.array([float(meta.loc[s, "lot_usd"]) if s in meta.index else 0.0 for s in sx])
|
||||
px_x = np.array([float(meta.loc[s, "markPx"]) if s in meta.index else np.nan for s in sx])
|
||||
dWx = np.abs(np.diff(Wx, axis=0, prepend=np.zeros((1, Wx.shape[1]))))
|
||||
tkx = dWx[dWx > 1e-9]
|
||||
print(f" gambe {len(sx)} lordo mediano {np.median(np.abs(Wx).sum(axis=1)):.2f}x "
|
||||
f"peso mosso mediano/gamba {np.median(tkx)*100:.3f}% p10 {np.percentile(tkx,10)*100:.3f}%")
|
||||
print(f"\n {'cap':>9}{'ticket med':>12}{'Sh mod':>9}{'Sh $5 [D]':>11}{'Sh $10 [C]':>12}"
|
||||
f"{'haircut $5':>12}{'haircut $10':>13}{'esegui%':>9}")
|
||||
hc10_5000 = np.nan
|
||||
for cap in CAPITALI:
|
||||
m0 = sim_libro(Wx, Rx, rbx, cap, None, None, None, FEE_LEG_MODELLO)
|
||||
m5 = sim_libro(Wx, Rx, rbx, cap, MIN_ORDER_ASSUNTO, None, None, FEE_LEG_MODELLO)
|
||||
m10 = sim_libro(Wx, Rx, rbx, cap, MIN_ORDER_HL, lot_x, px_x, FEE_LEG_MODELLO)
|
||||
a0, a5, a10 = (metriche(m["net"], idxx) for m in (m0, m5, m10))
|
||||
h5 = (a0["sharpe"] - a5["sharpe"]) / abs(a0["sharpe"]) * 100 if a0["sharpe"] else np.nan
|
||||
h10 = (a0["sharpe"] - a10["sharpe"]) / abs(a0["sharpe"]) * 100 if a0["sharpe"] else np.nan
|
||||
if abs(cap - 5000.0) < 1e-9:
|
||||
hc10_5000 = h10
|
||||
print(f" {cap:>9,.0f}{np.median(tkx)*cap:>12.2f}{a0['sharpe']:>9.2f}"
|
||||
f"{a5['sharpe']:>11.2f}{a10['sharpe']:>12.2f}{h5:>11.0f}%{h10:>12.0f}%"
|
||||
f"{m10['pct_eseguite']:>9.0f}%")
|
||||
print(f" -> haircut a $5.000 col pavimento VERO ($10): {hc10_5000:.0f}% "
|
||||
f"(soglia pre-registrata {XSR_SOGLIA_HAIRCUT*100:.0f}%)")
|
||||
print(f" NB: e' la lettura del PARAMETRO sul backtest, NON il gate del 23/10 — "
|
||||
f"quello si decide sulla finestra FORWARD e anticiparlo sarebbe selezione.")
|
||||
|
||||
# -------- costo reale per gamba: fee VERA + mezzo spread misurato
|
||||
taker = float(tk["cross"])
|
||||
slip = liq["half_spread_bps"].reindex(sx).astype(float) / 1e4
|
||||
slip_med = float(np.nanmedian(slip.values))
|
||||
fee_real = taker + np.nan_to_num(slip.values, nan=slip_med)
|
||||
print(f"\n [B] COSTO REALE PER GAMBA = taker {taker*1e4:.2f} bps + mezzo spread misurato:")
|
||||
print(f" mediana {np.median(fee_real)*1e4:.1f} bps p90 {np.percentile(fee_real,90)*1e4:.1f} bps"
|
||||
f" max {fee_real.max()*1e4:.1f} bps ({sx[int(np.argmax(fee_real))]})")
|
||||
print(f" contro i {FEE_LEG_MODELLO*1e4:.1f} bps/gamba della config CONGELATA [D]")
|
||||
print(f"\n {'variante di costo':<44}{'Sharpe':>8}{'maxDD':>8}{'CAGR':>8}")
|
||||
varianti = [
|
||||
(f"[D] config congelata {FEE_LEG_MODELLO*1e4:.1f} bps piatti", FEE_LEG_MODELLO),
|
||||
(f"[A] solo taker vero {taker*1e4:.2f} bps piatti", taker),
|
||||
(f"[B] taker + 1/2 spread PER-ASSET (mediana {np.median(fee_real)*1e4:.1f})", fee_real),
|
||||
(f"[B] taker + spread PIENO per-asset (pessimista)", taker + 2 * np.nan_to_num(slip.values, nan=slip_med)),
|
||||
]
|
||||
for nm, f in varianti:
|
||||
m = sim_libro(Wx, Rx, rbx, 5000.0, None, None, None, f)
|
||||
a_ = metriche(m["net"], idxx)
|
||||
print(f" {nm:<44}{a_['sharpe']:>8.2f}{a_['maxdd']:>7.1f}%{a_['cagr']:>7.1f}%")
|
||||
print(f" (a capitale infinito = nessun vincolo di min-order: isola il COSTO dal PAVIMENTO)")
|
||||
|
||||
print("\n" + "=" * 104)
|
||||
print(" LIMITI DICHIARATI: gli spread sono UNO/POCHI snapshot di OGGI su un venue la cui")
|
||||
print(" liquidita' e' cresciuta -> sono la stima piu' FAVOREVOLE per i 2.6 anni di backtest.")
|
||||
print(" Il pavimento $10 e' [C] (doc + messaggio d'errore), non provato da un ordine: provarlo")
|
||||
print(" richiederebbe di mandarne uno, e questo audit non tocca il conto.")
|
||||
print("=" * 104)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -460,6 +460,7 @@ def main() -> None:
|
||||
P(f" {'hte':>4} {'rule':>5} {'Δ vs adj_r':>12} {'Δ vs jit_r':>12}"
|
||||
f" {'Δ vs rand':>12} {'Δ vs mid':>12} {'Δ vs mr7':>12}")
|
||||
npos = ntot = 0
|
||||
npos_c = ntot_c = 0 # solo i null PULITI (bilanciati nel segno): rand/mid/mr7
|
||||
for hte in HTE_GRID:
|
||||
e = ev[ev["hte"] == hte]
|
||||
for rl in RULES:
|
||||
@@ -467,12 +468,25 @@ def main() -> None:
|
||||
for p in (f"adj_{rl}", f"jit_{rl}") + PLACEBOS_GLOB:
|
||||
dd = e[f"tw_{rl}"].to_numpy(float) - e[f"tw_{p}"].to_numpy(float)
|
||||
m, lo, hi, _ = cluster_boot(dd, e["exp"].to_numpy())
|
||||
cells.append(f"{m:>11.5f}{'*' if (np.isfinite(lo) and lo > 0) else ' '}")
|
||||
ntot += 1
|
||||
npos += int(np.isfinite(lo) and lo > 0)
|
||||
sig = bool(np.isfinite(lo) and lo > 0)
|
||||
cells.append(f"{m:>11.5f}{'*' if sig else ' '}")
|
||||
ntot += 1; npos += int(sig)
|
||||
if p in PLACEBOS_GLOB:
|
||||
ntot_c += 1; npos_c += int(sig)
|
||||
P(f" {hte:>4} {rl:>5} " + " ".join(cells))
|
||||
P(f" celle (rule x hte x placebo) in cui lo SPECIALE batte il placebo con IC95>0:"
|
||||
f" {npos}/{ntot} (atteso per caso ~{0.025 * ntot:.0f})")
|
||||
P(f" ... contando SOLO i null bilanciati nel segno (rand/mid/mr7): {npos_c}/{ntot_c}"
|
||||
f" (atteso ~{0.025 * ntot_c:.0f}) <-- e' QUESTO il numero da citare")
|
||||
P("\n ⚠ UN PLACEBO SI CONTROLLA PRIMA DI USARLO: se il suo segno non e' bilanciato rispetto")
|
||||
P(" alla regola, il Δ non misura l'informazione, misura il ROVESCIAMENTO del segnale.")
|
||||
P(f" quota di eventi con sign(K_placebo-S) == sign(K_regola-S), su tutti gli hte:")
|
||||
for rl in RULES:
|
||||
agr = {p: float(np.nanmean(np.sign(ev[f"d_{rl}"]) == np.sign(ev[f"d_{p}"])))
|
||||
for p in (f"adj_{rl}", f"jit_{rl}") + PLACEBOS_GLOB}
|
||||
P(" " + rl + ": " + " ".join(f"{k}={v:.2f}" for k, v in agr.items()))
|
||||
P(" un null PULITO sta vicino a 0.50 (rand/mid/mr7). Sotto ~0.35 il placebo e' la")
|
||||
P(" strategia INVERTITA e il suo Δ va letto come ~2x il segnale, non come un test.")
|
||||
P(" LETTURA: `adj_r`/`jit_r` chiedono 'e' proprio QUELLO strike?'; `mid`/`mr7` chiedono")
|
||||
P(" 'serve l'open interest, o basta il centro recente del prezzo?'. La seconda e' la")
|
||||
P(" domanda che uccide o salva il filone.")
|
||||
@@ -554,7 +568,9 @@ def main() -> None:
|
||||
f" · WR-knob atteso 1/(1+RR) = {1 / (1 + rr):.1%}" if np.isfinite(rr) else " RR n.d.")
|
||||
P(f" expectancy NETTA per trade: {np.mean(net):+.5f} ({np.mean(net) * 100:+.3f}%)"
|
||||
f" | in R (R = distanza dal livello): {np.mean(net / np.maximum(dk, 1e-6)):+.3f} R")
|
||||
P(f" a fee ZERO: {np.mean(tw):+.5f} -> {'l edge lordo non esiste' if np.mean(tw) <= 0 else 'edge lordo positivo, la fee lo mangia?'}")
|
||||
P(f" a fee ZERO: {np.mean(tw):+.5f} · la fee costa {2 * FEE_SIDE_REAL:.5f}/trade ="
|
||||
f" {200 * FEE_SIDE_REAL / max(np.mean(tw), 1e-9):.0f}% del lordo"
|
||||
f" -> {'MORTE PER FEE' if np.mean(tw) <= 2 * FEE_SIDE_REAL else 'la fee NON e il vincolo'}")
|
||||
|
||||
dbest, info = strat_daily(ev, best["rule"], int(best["hte"]),
|
||||
None if best["band"] == "nessuna" else (0.002, 0.03),
|
||||
@@ -652,6 +668,10 @@ def main() -> None:
|
||||
def _fn(rule):
|
||||
return lambda h: strat_daily(ev, rule, int(h), (0.002, 0.03), "sign", "all",
|
||||
FEE_SIDE_REAL)[0]
|
||||
P(" ⚠ le 8 ancore hte NON sono 8 osservazioni indipendenti: guardano le STESSE 73 scadenze")
|
||||
P(" e lo STESSO percorso di prezzo. '8/8 positive' dice che il segno non dipende")
|
||||
P(" dall'ancora, NON che l'effetto sia significativo — quello lo dice il cluster-bootstrap")
|
||||
P(" della sezione [4], che sui placebo senza opzioni (mid/mr7) NON e' significativo.")
|
||||
for other in ("mr7", "mid", "oi", "rand"):
|
||||
dl = A.anchor_luck_delta(_fn("mp"), _fn(other), offsets=list(HTE_GRID))
|
||||
P(f" Δ appaiato mp-{other:<4}: mediana {dl.get('median_paired'):+.2f}"
|
||||
@@ -676,7 +696,10 @@ def main() -> None:
|
||||
P("\n[8] SINTESI")
|
||||
n_fact_pos = int((F.lo > 0).sum())
|
||||
P(f" fatto : {n_fact_pos}/{len(F)} celle (rule x hte) con pinning significativo a IC95")
|
||||
P(f" null loc. : {npos}/{ntot} confronti appaiati in cui il livello 'speciale' batte il placebo")
|
||||
P(f" null loc. : {npos}/{ntot} confronti appaiati vinti dal livello 'speciale';"
|
||||
f" {npos_c}/{ntot_c} contando solo i null PULITI")
|
||||
P(f" di questi, contro il placebo SENZA OPZIONI (media 7g dello spot): "
|
||||
f"{sum(1 for h in HTE_GRID for rl in RULES if (lambda t: np.isfinite(t[1]) and t[1] > 0)(cluster_boot((ev[ev.hte == h][f'tw_{rl}'] - ev[ev.hte == h]['tw_mr7']).to_numpy(float), ev[ev.hte == h]['exp'].to_numpy())))}/24")
|
||||
P(f" strategia : miglior Sharpe della griglia {mb['sharpe']:+.2f} su {mb['n']} giorni, DSR {dsr:.3f}")
|
||||
P(f" marginale : {mv.get('marginal_verdict')} (strutturalmente non ADDS-abile a 3,7 mesi)")
|
||||
P("=" * 100)
|
||||
|
||||
@@ -205,11 +205,15 @@ def _panel_index(start: str) -> pd.DatetimeIndex:
|
||||
_RG_CACHE: dict = {}
|
||||
|
||||
|
||||
def book_RG(w: dict, start: str = START_WIN) -> tuple[np.ndarray, np.ndarray]:
|
||||
def book_RG(w: dict, start: str = START_WIN,
|
||||
drift_mult: tuple = ()) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""(R, gap) giornalieri di un libro a pesi arbitrari sui 5 sleeve.
|
||||
Crypto TP01+SKH01: minimo ESATTO sul path orario condiviso (r0725_prop_coupled).
|
||||
XS01/VRP01/GTAA01: il loro minimo si SOMMA (worst simultaneo — convenzione del 25/07)."""
|
||||
key = (start,) + tuple(round(float(w.get(nm, 0.0)), 6) for nm in SLEEVES)
|
||||
XS01/VRP01/GTAA01: il loro minimo si SOMMA (worst simultaneo — convenzione del 25/07).
|
||||
`drift_mult` = (("XS01", 0.5), ...): taglia il DRIFT di quello sleeve (non la vol) — serve
|
||||
per lo stress giudiziale su uno sleeve scoperto DENTRO la finestra di misura."""
|
||||
dm = dict(drift_mult)
|
||||
key = (start, drift_mult) + tuple(round(float(w.get(nm, 0.0)), 6) for nm in SLEEVES)
|
||||
if key in _RG_CACHE:
|
||||
return _RG_CACHE[key]
|
||||
idx = _panel_index(start)
|
||||
@@ -218,15 +222,25 @@ def book_RG(w: dict, start: str = START_WIN) -> tuple[np.ndarray, np.ndarray]:
|
||||
M = np.zeros(len(idx))
|
||||
if w_tp > 0 or w_skh > 0:
|
||||
c = _crypto(w_tp, w_skh).reindex(idx)
|
||||
R += c["R"].values
|
||||
cr = c["R"].values
|
||||
if w_tp > 0 and dm.get("TP01", 1.0) != 1.0 or w_skh > 0 and dm.get("SKH01", 1.0) != 1.0:
|
||||
raise NotImplementedError("stress sul drift crypto: il minimo e' path-esatto, non separabile")
|
||||
R += cr
|
||||
M += c["m"].values
|
||||
for nm, fr in (("XS01", pc.xsec_daily_tuples()), ("VRP01", vrp_daily_tuples()),
|
||||
("GTAA01", gtaa_daily_tuples())):
|
||||
wt = w.get(nm, 0.0)
|
||||
if wt > 0:
|
||||
f = fr.reindex(idx)
|
||||
R += wt * f["R"].values
|
||||
M += wt * f["m"].values
|
||||
rv = np.nan_to_num(f["R"].values)
|
||||
mv = np.nan_to_num(f["m"].values)
|
||||
mult = dm.get(nm, 1.0)
|
||||
if mult != 1.0: # taglia il drift, lascia la forma della coda
|
||||
cut = (1.0 - mult) * rv.mean() # NB: media PRIMA del taglio
|
||||
rv = rv - cut
|
||||
mv = mv - cut
|
||||
R += wt * rv
|
||||
M += wt * mv
|
||||
R = np.nan_to_num(R)
|
||||
M = np.nan_to_num(M)
|
||||
_RG_CACHE[key] = (R, np.minimum(M, R) - R)
|
||||
@@ -299,21 +313,34 @@ def funded_sim(R: np.ndarray, G: np.ndarray, idx: np.ndarray, lev: float, firm:
|
||||
if (t + 1) % 365 == 0:
|
||||
alive_at[(t + 1) // 365] = float(alive.mean())
|
||||
return dict(p_alive=float(alive.mean()), alive_at=alive_at,
|
||||
e_payout=float(payout.mean()), payout=payout)
|
||||
e_payout=float(payout.mean()), payout=payout, alive=alive)
|
||||
|
||||
|
||||
def objective(w: dict, lev: float, firm: str, ev_idx: np.ndarray, fu_idx: np.ndarray,
|
||||
lens: str = "coupled", factor: float = DELUCK,
|
||||
notional: float = 100_000.0) -> dict:
|
||||
R, G = book_RG(w)
|
||||
notional: float = 100_000.0, drift_mult: tuple = ()) -> dict:
|
||||
R, G = book_RG(w, drift_mult=drift_mult)
|
||||
R = deluck(R, factor)
|
||||
e = eval_sim(R, G, ev_idx, lev, firm, lens)
|
||||
f = funded_sim(R, G, fu_idx, lev, firm, notional, lens)
|
||||
ann = float(R.mean() * 365.0)
|
||||
vol = float(R.std() * np.sqrt(365.0))
|
||||
# stimatore per-percorso di J: eval e funded girano su indici di bootstrap INDIPENDENTI,
|
||||
# quindi mean(passed_i * alive_i) stima p_pass*p_alive ed e' APPAIABILE fra configurazioni.
|
||||
jvec = (e["passed"] & f["alive"]).astype(float)
|
||||
return dict(J=e["p_pass"] * f["p_alive"], p_pass=e["p_pass"], p_alive=f["p_alive"],
|
||||
e_payout=f["e_payout"], sharpe=(ann / vol if vol > 0 else 0.0),
|
||||
vol=vol, drift=ann, t_pass=e["t_pass"])
|
||||
vol=vol, drift=ann, t_pass=e["t_pass"], jvec=jvec)
|
||||
|
||||
|
||||
def paired_delta(wA: dict, levA: float, wB: dict, levB: float, firm: str,
|
||||
ev_idx: np.ndarray, fu_idx: np.ndarray) -> tuple[float, float]:
|
||||
"""Differenza APPAIATA di J fra due configurazioni (stessi indici di bootstrap) + errore
|
||||
standard della differenza. Il livello di J ha SE ~0.01; la DIFFERENZA molto meno."""
|
||||
a = objective(wA, levA, firm, ev_idx, fu_idx)["jvec"]
|
||||
b = objective(wB, levB, firm, ev_idx, fu_idx)["jvec"]
|
||||
d = a - b
|
||||
return float(d.mean()), float(d.std(ddof=1) / np.sqrt(len(d)))
|
||||
|
||||
|
||||
# ================================================================== §2. IL CONTO ANALITICO
|
||||
@@ -358,7 +385,8 @@ def grid_scan(grid: list[dict], levs: tuple, firm: str, ev_idx: np.ndarray, fu_i
|
||||
for w in grid:
|
||||
for lev in levs:
|
||||
o = objective(w, lev, firm, ev_idx, fu_idx, lens, factor)
|
||||
rows.append(dict(w=wkey(w), lev=lev, **{k: v for k, v in o.items()}))
|
||||
rows.append(dict(w=wkey(w), lev=lev,
|
||||
**{k: v for k, v in o.items() if k != "jvec"}))
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
|
||||
@@ -422,6 +422,78 @@ def vrp_decomposizione(q: pd.DataFrame, cm7: dict) -> pd.DataFrame:
|
||||
return pd.DataFrame(righe)
|
||||
|
||||
|
||||
MARKFIT_OK = (0.85, 1.15) # banda del CONTROLLO, dichiarata prima di guardare i f
|
||||
|
||||
|
||||
def qualita(D: pd.DataFrame) -> pd.Series:
|
||||
"""Filtro dichiarato: si tiene un'osservazione solo se il CONTROLLO passa, cioe' se la IV di
|
||||
Deribit riprezza i suoi stessi mid entro +-15% sul credito, e se il credito conservativo e'
|
||||
positivo. NON e' un filtro sull'esito (il f), e' un filtro sulla variabile di controllo:
|
||||
dove il controllo fallisce non si sta misurando lo skew, si sta misurando una quota rotta."""
|
||||
return (D["f_markfit"].between(*MARKFIT_OK)) & (D["f_spread"] > 0) & D["f_term"].gt(0)
|
||||
|
||||
|
||||
def attribuzione_log(D: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Attribuzione ESATTA e additiva: log(f_tot) = log(f_term)+log(f_skew)+log(f_markfit)+
|
||||
log(f_spread), per costruzione (la catena dei rapporti telescopia). Le MEDIANE dei fattori
|
||||
non si moltiplicano fra loro; i log si sommano. Da qui la QUOTA del difetto per causa."""
|
||||
g = D[qualita(D)]
|
||||
parti = ["f_term", "f_skew", "f_markfit", "f_spread"]
|
||||
L = np.log(g[parti])
|
||||
tot = float(np.log(g["f_tot"]).mean())
|
||||
rows = []
|
||||
for c in parti:
|
||||
m = float(L[c].mean())
|
||||
rows.append(dict(fattore=c, media_log=round(m, 4),
|
||||
fattore_geometrico=round(float(np.exp(m)), 3),
|
||||
quota_del_difetto=round(m / tot, 3) if tot != 0 else np.nan))
|
||||
rows.append(dict(fattore="TOTALE", media_log=round(tot, 4),
|
||||
fattore_geometrico=round(float(np.exp(tot)), 3), quota_del_difetto=1.0))
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def panel_f(q: pd.DataFrame, cm7: dict) -> pd.DataFrame:
|
||||
"""La stessa decomposizione, ma a OGNI ora in cui lo spread e' costruibile (non solo ai 22
|
||||
ingressi settimanali di VRP01). Serve alla RISOLUZIONE: 22 punti non separano due cause da
|
||||
~10% l'una. ⚠ Le ore dentro la stessa scadenza NON sono indipendenti: il campione efficace
|
||||
resta il numero di SCADENZE, che si riporta accanto."""
|
||||
out = []
|
||||
for a in ASSETS:
|
||||
S7 = cm7[a]
|
||||
S, V = CB.spot_series(a), CB.dvol_series(a)
|
||||
p = q[(q["asset"] == a) & (q["option_type"] == "P")
|
||||
& (q["dte"] >= 4.0) & (q["dte"] <= 10.0)]
|
||||
for (hr, exp), g in p.groupby(["hr", "exp"], sort=False):
|
||||
if hr not in S7.index:
|
||||
continue
|
||||
legs = CB.pick_legs(g)
|
||||
if legs is None:
|
||||
continue
|
||||
ts = g["ts"].max()
|
||||
dte = float((exp - ts).total_seconds() / 86400.0)
|
||||
if not (4.0 <= dte <= 10.0):
|
||||
continue
|
||||
spot = float(S.asof(ts)); dvol = float(V.asof(ts)) / 100.0
|
||||
atm7 = float(S7.loc[hr, "atm"]) / 100.0
|
||||
T = dte / 365.25
|
||||
c_mod = CB.bs_put(spot, legs["k_short"], T, dvol) - CB.bs_put(spot, legs["k_long"], T, dvol)
|
||||
c_atm = CB.bs_put(spot, legs["k_short"], T, atm7) - CB.bs_put(spot, legs["k_long"], T, atm7)
|
||||
c_sml = (CB.bs_put(spot, legs["k_short"], T, legs["iv_short"] / 100.0)
|
||||
- CB.bs_put(spot, legs["k_long"], T, legs["iv_long"] / 100.0))
|
||||
c_mid = (legs["mid_short"] - legs["mid_long"]) * spot
|
||||
c_real = (legs["bid_short"] - legs["ask_long"]) * spot
|
||||
if not (c_mod > 0 and c_atm > 0 and c_sml > 0):
|
||||
continue
|
||||
hist = V[V.index < ts]
|
||||
ivr = float((hist < dvol * 100).mean()) if len(hist) else np.nan
|
||||
out.append(dict(asset=a, hr=hr, exp=exp, dte=dte, rr7=float(S7.loc[hr, "rr"]),
|
||||
atm7=float(S7.loc[hr, "atm"]), dvol=dvol * 100, ivrank=ivr,
|
||||
f_term=c_atm / c_mod, f_skew=c_sml / c_atm,
|
||||
f_markfit=c_mid / c_sml, f_spread=c_real / c_mid,
|
||||
f_tot=c_real / c_mod, f_tot_mid=c_mid / c_mod))
|
||||
return pd.DataFrame(out)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
def main() -> None:
|
||||
t_start = time.time()
|
||||
@@ -469,6 +541,14 @@ def main() -> None:
|
||||
print(f"\n {a} — corr fra dRR (variazione oraria dello skew) e il rendimento nella")
|
||||
print(" finestra indicata, ancorata al TIMESTAMP VERO delle quote (ts_max):")
|
||||
print(LL[LL["asset"] == a].to_string(index=False))
|
||||
fw = LL[LL["minuti"] > 0]
|
||||
print(f"\n LAG IN AVANTI: {len(fw)} test ({fw['minuti'].nunique()} orizzonti x {len(ASSETS)} asset).")
|
||||
print(f" |t| massimo osservato = {fw['t_dRR'].abs().max():.2f} su {len(fw)} test;")
|
||||
print(f" |t| atteso come MASSIMO di {len(fw)} test indipendenti sotto il nullo ~ "
|
||||
f"{float(np.abs(np.random.default_rng(0).standard_normal((4000, len(fw)))).max(axis=1).mean()):.2f}.")
|
||||
print(" -> il massimo in avanti e' quello che il rumore produce da solo. LAG INDIETRO:")
|
||||
print(f" |t| minimo = {LL[LL['minuti'] < 0]['t_dRR'].abs().min():.2f} su "
|
||||
f"{int((LL['minuti'] < 0).sum())} test, tutti dello STESSO segno.")
|
||||
print("\n Controllo POSITIVO — la stessa misura con l'ancora ASSUNTA a :30 dell'ora:")
|
||||
FF = pd.concat([leadlag_ancora_sbagliata(cm30[a], a) for a in ASSETS], ignore_index=True)
|
||||
print(FF.to_string(index=False))
|
||||
@@ -550,8 +630,13 @@ def main() -> None:
|
||||
z_all[a] = pd.Series(z.to_numpy(), index=pd.DatetimeIndex(pd.to_datetime(sub["datetime"], utc=True)))
|
||||
Z = pd.concat(z_all, axis=1).mean(axis=1)
|
||||
Z = Z.reindex(Bw.index).ffill()
|
||||
att = int((Bw != 0).sum())
|
||||
print(f" baseline TP01 sulla finestra dello skew: n={len(Bw)} giorni, "
|
||||
f"Sharpe {sh_of(Bw.to_numpy()):+.2f}, maxDD {dd_of(Bw.to_numpy()):.2%}")
|
||||
print(f" ⚠ barre ATTIVE del baseline: {att}/{len(Bw)} ({att/len(Bw):.1%}) — TP01 e' FLAT il resto")
|
||||
print(f" del tempo, e nella finestra il suo maxDD e' {dd_of(Bw.to_numpy()):.2%}: un gate di")
|
||||
print(" de-risk non ha NIENTE da proteggere qui. Il null de-levering girera' e dira'")
|
||||
print(" 'REFUTED', ma il motivo e' che manca il sinistro, non che il gate sia stato battuto.")
|
||||
for thr in (-0.5, -1.0, -1.5):
|
||||
gate = (Z.shift(1) > thr).astype(float) # de-risk quando lo skew si irripidisce (z basso)
|
||||
ov = (Bw * gate).to_numpy()
|
||||
@@ -565,8 +650,10 @@ def main() -> None:
|
||||
f"Sharpe {nul['sh_base_k']:+.2f} vs overlay {nul['sh_overlay']:+.2f} "
|
||||
f"-> {'SUPERATO' if nul['superato'] else 'REFUTED'}")
|
||||
if n_fire:
|
||||
gia_flat = int((gg == 0).sum())
|
||||
print(f" nei giorni de-riskati TP01 faceva in media {gg.mean()*100:+.3f}%/g "
|
||||
f"(gli altri {Bw[gate==1].mean()*100:+.3f}%/g)")
|
||||
f"(gli altri {Bw[gate==1].mean()*100:+.3f}%/g); di quei {n_fire} giorni TP01 era "
|
||||
f"GIA' FLAT in {gia_flat} ({gia_flat/n_fire:.0%}) -> il gate spegne cio' che era gia' spento")
|
||||
print("\n ridondanza col trend — corr fra lo z dello skew e il rendimento di TP01: "
|
||||
f"{float(pd.concat({'z': Z, 'b': Bw}, axis=1).dropna().corr().iloc[0,1]):+.3f}")
|
||||
print(" POTENZA: con 75 giorni un gate che scatta ~10 volte non e' misurabile. Il numero da")
|
||||
@@ -600,11 +687,67 @@ def main() -> None:
|
||||
print(" Se sta a 1, RR e BF di questa sessione sono un fatto di PREZZO, non un artefatto")
|
||||
print(" del fit di Deribit -> vale anche come controllo di confound per Q1/Q2.")
|
||||
|
||||
print("\n ATTRIBUZIONE ESATTA (in log: i fattori si SOMMANO, le mediane no)")
|
||||
Q = qualita(D)
|
||||
print(f" osservazioni che passano il CONTROLLO f_markfit in {MARKFIT_OK} e credito>0: "
|
||||
f"{int(Q.sum())}/{len(D)}")
|
||||
scartate = D[~Q][["asset", "ts", "f_markfit", "f_spread"]].copy()
|
||||
if len(scartate):
|
||||
scartate["ts"] = pd.DatetimeIndex(scartate["ts"]).strftime("%Y-%m-%d")
|
||||
print(" scartate (il controllo fallisce -> non si sta misurando lo skew):")
|
||||
print(scartate.round(3).to_string(index=False))
|
||||
print(attribuzione_log(D).to_string(index=False))
|
||||
print(f" f_tot mediana sul sottoinsieme che passa il controllo: {D[Q]['f_tot'].median():.3f}")
|
||||
|
||||
print("\n PANEL ORARIO — stessa decomposizione a OGNI ora costruibile (risoluzione)")
|
||||
PN = panel_f(q, cm7)
|
||||
QP = qualita(PN)
|
||||
print(f" osservazioni {len(PN)} ({PN.groupby('asset').size().to_dict()}), di cui passano il "
|
||||
f"controllo {int(QP.sum())}; scadenze distinte (campione EFFICACE) "
|
||||
f"{PN.groupby('asset')['exp'].nunique().to_dict()}")
|
||||
G = PN[QP]
|
||||
for a in ASSETS + ("TUTTI",):
|
||||
g = G if a == "TUTTI" else G[G["asset"] == a]
|
||||
if g.empty:
|
||||
continue
|
||||
print(f" {a:6s} n={len(g):5d} | f_term {g['f_term'].median():.3f} | "
|
||||
f"f_skew {g['f_skew'].median():.3f} | f_markfit {g['f_markfit'].median():.3f} | "
|
||||
f"f_spread {g['f_spread'].median():.3f} || f_tot {g['f_tot'].median():.3f}")
|
||||
print(" attribuzione log sul panel:")
|
||||
print(attribuzione_log(PN).to_string(index=False))
|
||||
|
||||
print("\n Il f dipende dal REGIME? (la domanda che decide se 0.73 e' conservativo o ottimista)")
|
||||
D2 = D.dropna(subset=["f_tot_mid", "rr7"])
|
||||
D2 = D[qualita(D)].dropna(subset=["f_tot_mid", "rr7"])
|
||||
for var in ("rr7", "atm7", "ivrank", "bf7"):
|
||||
c = float(D2["f_tot_mid"].corr(D2[var]))
|
||||
print(f" corr(f_tot_mid, {var:7s}) = {c:+.3f} [n={len(D2)}]")
|
||||
cg = float(G["f_tot_mid"].corr(G[var])) if var in G.columns else np.nan
|
||||
print(f" corr(f_tot_mid, {var:7s}) = {c:+.3f} [ingressi n={len(D2)}]"
|
||||
+ (f" {cg:+.3f} [panel n={len(G)}]" if np.isfinite(cg) else ""))
|
||||
print(" (positivo = il credito reale si avvicina al modello quando lo skew e' PIATTO;")
|
||||
print(" e' il verso atteso dal meccanismo, ma n=22 e il panel e' autocorrelato.)")
|
||||
print("\n IL PEZZO PIU' GRANDE (f_term) CAMBIA SEGNO COL REGIME — ed e' questo che decide")
|
||||
print(" se 0.73 e' conservativo o ottimista dove VRP01 tradera' DAVVERO.")
|
||||
G2 = G.copy()
|
||||
G2["contango"] = G2["atm7"] < G2["dvol"]
|
||||
print(f" ore del panel con ATM7 < DVOL30 (contango, il modello SOVRAPPREZZA): "
|
||||
f"{G2['contango'].mean():.1%}")
|
||||
for nome, m in (("contango (ATM7<DVOL)", G2["contango"]), ("backwardation (ATM7>DVOL)", ~G2["contango"])):
|
||||
g = G2[m]
|
||||
if g.empty:
|
||||
continue
|
||||
print(f" {nome:26s} n={len(g):5d} | f_term {g['f_term'].median():.3f} | "
|
||||
f"f_skew {g['f_skew'].median():.3f} | f_tot {g['f_tot'].median():.3f} | "
|
||||
f"IV-rank mediano {g['ivrank'].median():.3f}")
|
||||
qs = pd.qcut(G2["ivrank"], 4, labels=["q1", "q2", "q3", "q4"], duplicates="drop")
|
||||
st = G2.groupby(qs, observed=True).agg(n=("f_term", "size"), ivr=("ivrank", "median"),
|
||||
f_term=("f_term", "median"), f_skew=("f_skew", "median"),
|
||||
f_tot=("f_tot", "median"))
|
||||
print(" stratificato per quartile di IV-rank (DENTRO il campione: max 0.23, il gate e' 0.30):")
|
||||
print(st.round(3).to_string())
|
||||
print(" -> f_term sale con l'IV-rank; f_skew no. L'estrapolazione oltre 0.30 NON e' misurata")
|
||||
print(" e in questo campione non e' misurabile: l'unico episodio ad alta vol e' un RALLY")
|
||||
print(" (2026-08-21, +26% in una settimana), non un crash. Manca il regime che conta.")
|
||||
|
||||
print(f"\n IV-rank agli ingressi: min {D['ivrank'].min():.3f} mediana {D['ivrank'].median():.3f} "
|
||||
f"max {D['ivrank'].max():.3f}")
|
||||
n_gate = int((D["ivrank"] > 0.30).sum())
|
||||
|
||||
@@ -5,7 +5,11 @@ DOMANDA. Tre, dichiarate prima di misurare:
|
||||
Q2 il CARRY di vol (roll-down lungo la curva) e' INCASSABILE al bid-ask reale?
|
||||
Q3 vale come GATE DI RISCHIO sopra TP01/SKH01?
|
||||
|
||||
IPOTESI A PRIORI (registrate prima di guardare i numeri — servono a poterle smentire):
|
||||
IPOTESI A PRIORI (⚠️ da dove vengono: NON da una busta sigillata. Vengono dai precedenti gia'
|
||||
DOCUMENTATI del progetto — i 4 gate di de-risk risultati ridondanti col trend, il verdetto HEDGE
|
||||
su VRP-Z del 29/06, l'aritmetica fee/carry gia' fatta su VRP01 e gamma-scalping. Sono state
|
||||
scritte prima di far girare QUESTO script, ma dopo una fase esplorativa sui dati: si dichiara,
|
||||
perche' "l'avevo previsto" vale solo se si dice quanto costava prevederlo):
|
||||
Q1 contemporanea. Il front reagisce allo spot: un tuffo alza la vol a 7g piu' di quella a 30g.
|
||||
Se e' cosi', la pendenza e' un TERMOMETRO, non un segnale.
|
||||
Q2 no. Il roll-down fra 7g e 30g vale frazioni di punto-vol al giorno; il bid-ask ATM di
|
||||
@@ -77,6 +81,10 @@ CAPITALE = 635.0 # il conto vero
|
||||
|
||||
# Conteggio dei trial, AL RIALZO (sezione 6). Ogni voce e' una configurazione VALUTATA.
|
||||
TRIALS: list[tuple[str, int]] = []
|
||||
# I pochi numeri su cui poggia il verdetto, raccolti dalle sezioni che li misurano: il verdetto
|
||||
# NON li riscrive a mano (un numero ricopiato in una conclusione e' un numero che smette di
|
||||
# aggiornarsi quando i dati crescono).
|
||||
FACTS: dict = {}
|
||||
|
||||
|
||||
def hr(t: str = "") -> None:
|
||||
@@ -333,8 +341,11 @@ def sezione2(curve: pd.DataFrame) -> dict:
|
||||
idx = j.index
|
||||
r1 = lp.diff()
|
||||
sub(f"{a} — n ore = {len(j)} ({idx.min()} -> {idx.max()})")
|
||||
FACTS[f"back_sev_{a}"] = float((j.slope < -5).mean())
|
||||
FACTS[f"back_{a}"] = float((j.slope < 0).mean())
|
||||
print(f" pendenza iv_{BACK}-iv_{FRONT}: media {j.slope.mean():+.2f} pt-vol, "
|
||||
f"sd {j.slope.std():.2f}, backwardation nel {100*(j.slope<0).mean():.1f}% delle ore")
|
||||
f"sd {j.slope.std():.2f}, backwardation nel {100*(j.slope<0).mean():.1f}% delle ore, "
|
||||
f"SEVERA (< -5 pt-vol) nel {100*(j.slope<-5).mean():.1f}%")
|
||||
|
||||
print("\n (a) CROSS-CORRELAZIONE fra la VARIAZIONE oraria di pendenza e il ritorno orario.")
|
||||
print(" lag<0 = ritorno PRIMA della variazione (il prezzo guida) | lag>0 = DOPO (la")
|
||||
@@ -374,6 +385,7 @@ def sezione2(curve: pd.DataFrame) -> dict:
|
||||
print(f" corr(iv_{FRONT}, RV 24h passata) {float(ivf.corr(rvp)):+.3f} "
|
||||
f"corr(iv_{FRONT}, RV futura) {float(ivf.corr(rvf)):+.3f}")
|
||||
res[(a, "conf_past")], res[(a, "conf_fwd")] = c_pp, c_pf
|
||||
FACTS[f"conf_past_{a}"], FACTS[f"conf_fwd_{a}"] = c_pp, c_pf
|
||||
|
||||
print("\n (d) La correlazione col futuro SOPRAVVIVE se si controlla per il PASSATO?")
|
||||
print(" (regressione del ritorno futuro su pendenza + ritorno passato + RV passata;")
|
||||
@@ -412,6 +424,8 @@ def sezione2(curve: pd.DataFrame) -> dict:
|
||||
p = _block_pctl(x, y, c, L=max(3 * h, 7))
|
||||
c7 = float(np.corrcoef(x[:-7], y[:-7])[0, 1]) if len(x) > 20 else np.nan
|
||||
c7b = float(np.corrcoef(x[7:], y[7:])[0, 1]) if len(x) > 20 else np.nan
|
||||
if h == 5:
|
||||
FACTS[f"c5_{a}"], FACTS[f"c5_no7_{a}"] = c, c7
|
||||
print(f" h={h}g n={m.sum():3d} corr={c:+.3f} pctl vs null a blocchi={p:.3f}"
|
||||
f" | senza ultima settimana {c7:+.3f} | senza prima settimana {c7b:+.3f}")
|
||||
print(" ^ le due colonne di destra sono il test di RISOLUZIONE: se togliere 7")
|
||||
@@ -548,6 +562,7 @@ def sezione3(chain: pd.DataFrame, atm_exp: pd.DataFrame, curve: pd.DataFrame) ->
|
||||
f"un round-trip ATM 10-20g costa {sp:.3f} pt-vol")
|
||||
print(f" => servono {sp/rd:.1f} GIORNI di carry per pagare UN solo giro, "
|
||||
f"e nel frattempo si porta il vega.")
|
||||
FACTS[f"giorni_carry_{a}"] = sp / rd
|
||||
TRIALS.append(("Q2: 5 secchi di tenore x 2 asset (costo)", 10))
|
||||
|
||||
sub("(d) e a $635 la domanda non e' nemmeno questa: il LOTTO MINIMO")
|
||||
@@ -610,6 +625,8 @@ def sezione4(curve: pd.DataFrame) -> None:
|
||||
sub("(a) il fatto che decide da solo la sezione")
|
||||
W = pd.concat(pos, axis=1).loc[TS_START:]
|
||||
flat = (W.abs() < 0.05).mean()
|
||||
FACTS["tp01_flat"] = float(flat.mean())
|
||||
FACTS["n_giorni"] = int(len(W))
|
||||
print(f" Nella finestra {TS_START.date()} -> oggi ({len(W)} giorni), TP01 e' FLAT nel "
|
||||
f"{flat['BTC']:.1%} (BTC) / {flat['ETH']:.1%} (ETH) dei giorni.")
|
||||
print(f" Esposizione media: BTC {W['BTC'].mean():.3f}, ETH {W['ETH'].mean():.3f} "
|
||||
@@ -665,6 +682,8 @@ def sezione4(curve: pd.DataFrame) -> None:
|
||||
print(f" buy&hold sulla stessa finestra/ancore: mediana {bh['median']:+.2f} "
|
||||
f"[{bh['lo']:+.2f}, {bh['hi']:+.2f}] <-- il vero termine di paragone in un toro")
|
||||
dlt = A.anchor_luck_delta(cand, base, list(range(24)))
|
||||
FACTS[f"vs_bh_{a}"] = dlt["median_paired"]
|
||||
FACTS[f"vs_bh_n_{a}"] = (dlt["n_positive"], dlt["n_anchors"])
|
||||
print(f" mediana delle DIFFERENZE APPAIATE candidato-B&H: {dlt['median_paired']:+.2f} "
|
||||
f"(positiva in {dlt['n_positive']}/{dlt['n_anchors']} ancore)")
|
||||
TRIALS.append((f"Q3 {a}: 24 ancore x 2 varianti", 48))
|
||||
@@ -697,10 +716,12 @@ def sezione4(curve: pd.DataFrame) -> None:
|
||||
else:
|
||||
verdict = ("IL CANDIDATO NON SOPRAVVIVE (il de-levering fa meglio)"
|
||||
if best[1] >= sh_c else "candidato sopra il null di de-levering")
|
||||
FACTS[f"delev_{a}"] = bool(best[1] >= sh_c)
|
||||
print(f" {a}: candidato Sharpe {sh_c:+.2f} maxDD {dd_c:.2%} | buy&hold a leva "
|
||||
f"k={best[0]:.2f} -> stesso o minor DD ({best[2]:.2%}) con Sharpe {best[1]:+.2f}"
|
||||
f" => {verdict}")
|
||||
imp = A.implausible_sharpe(net)
|
||||
FACTS[f"implaus_{a}"] = bool(imp["implausible"])
|
||||
print(f" implausible_sharpe sul candidato di finestra: implausible="
|
||||
f"{imp['implausible']} {imp['reasons']}")
|
||||
TRIALS.append((f"Q3 {a}: 60 valori di k nel null de-levering", 60))
|
||||
@@ -760,6 +781,9 @@ def sezione5(curve: pd.DataFrame) -> None:
|
||||
print(f" era DVOL (2021-10 -> oggi, {len(era)} giorni): Sharpe {A._sh(era):+.3f} "
|
||||
f"maxDD {A._dd_ret(era):.2%}")
|
||||
rep = A.marginal_vs_tp01(cd)
|
||||
FACTS["proxy_marginal"] = rep.get("marginal_verdict")
|
||||
FACTS["proxy_sharpe"] = A._sh(era)
|
||||
FACTS["proxy_uplift"] = rep["blends"]["w25"]["uplift_full"]
|
||||
print(f" marginal_vs_tp01 -> {rep.get('marginal_verdict')} | corr a TP01 "
|
||||
f"{rep.get('corr_full')} | uplift w25 full {rep['blends']['w25']['uplift_full']:+.3f} "
|
||||
f"hold {rep['blends']['w25']['uplift_hold']}")
|
||||
@@ -807,6 +831,95 @@ def sezione6() -> None:
|
||||
print(" su cui questo filone ricade (VRP-Z: earns_slot=False).")
|
||||
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
# SEZIONE 7 — VERDETTO
|
||||
# ===========================================================================================
|
||||
def sezione7() -> None:
|
||||
F = FACTS
|
||||
hr("SEZIONE 7 — VERDETTO")
|
||||
print(f"""
|
||||
Q1 LA PENDENZA E' UN TERMOMETRO, NON UN SEGNALE — e la finestra non ha risoluzione.
|
||||
· La cross-correlazione oraria ha il picco a lag -1h/0h (il prezzo si muove PRIMA); da
|
||||
+1h in poi e' rumore (|corr| <= 0.05 su ogni lag fino a 24h).
|
||||
· corr(pendenza, RV 24h PASSATA) = {F.get('conf_past_BTC', float('nan')):+.2f} (BTC) / {F.get('conf_past_ETH', float('nan')):+.2f} (ETH)
|
||||
corr(pendenza, RV 24h FUTURA) = {F.get('conf_fwd_BTC', float('nan')):+.2f} (BTC) / {F.get('conf_fwd_ETH', float('nan')):+.2f} (ETH)
|
||||
La pendenza SA qualcosa della vol futura, ma sa DI PIU' di quella passata: e' un premio
|
||||
di vol con la gamba realizzata sostituita da una implicita, non una variabile nuova.
|
||||
· L'unica correlazione col ritorno futuro che sembra qualcosa (h=5g: {F.get('c5_BTC', float('nan')):+.2f} BTC /
|
||||
{F.get('c5_ETH', float('nan')):+.2f} ETH) NON SOPRAVVIVE alla rimozione dell'ULTIMA SETTIMANA:
|
||||
diventa {F.get('c5_no7_BTC', float('nan')):+.2f} / {F.get('c5_no7_ETH', float('nan')):+.2f}. Sette giorni tolti da un campione di 70 ne
|
||||
ribaltano il SEGNO — ed e' il test che conta piu' del percentile contro il null.
|
||||
E nella regressione controllata per il ritorno passato il t di Newey-West sta sotto 1.0
|
||||
a ogni orizzonte. Non e' "debole": e' sotto la risoluzione del campione.
|
||||
|
||||
Q2 IL CARRY C'E' MA NON PAGA IL BID-ASK, e non e' nemmeno incassato.
|
||||
· Roll-down da corto: ~0.07 (BTC) / ~0.13 (ETH) punti-vol al GIORNO.
|
||||
Round-trip ATM 10-20g: 1.26 (BTC) / 1.77 (ETH) punti-vol.
|
||||
=> {F.get('giorni_carry_BTC', float('nan')):.0f} giorni (BTC) / {F.get('giorni_carry_ETH', float('nan')):.0f} giorni (ETH) di carry per pagare UN giro.
|
||||
· E il roll NON viene incassato: beta(realizzato ~ implicito) e' NEGATIVO (~-1 a una
|
||||
settimana), cioe' la forma della curva torna indietro piu' di quanto la curva rotoli.
|
||||
La differenza realizzato-implicito e' dentro il rumore (|t| <= 0.9 a blocchi).
|
||||
· A $635 il vincolo binding e' prima di tutto il LOTTO MINIMO: una gamba ATM costa il 26%
|
||||
(BTC) / 10% (ETH) del conto in premio, la gamba corta vuole margine, e il carry lordo
|
||||
di una struttura ETH e' ~$0.19/giorno. Il bersaglio dichiarato e' 50 EUR/giorno.
|
||||
|
||||
Q3 GATE DI RISCHIO: RIDONDANTE COL TREND — 5a occorrenza, e NON MISURABILE su questa finestra.
|
||||
· TP01 e' FLAT nel {F.get('tp01_flat', float('nan')):.0%} dei giorni della finestra: non c'e' rischio da togliere.
|
||||
Un gate che spegne il 25% dei giorni trova TP01 gia' flat nel 79-90% di quei giorni, e
|
||||
corr(pendenza, esposizione TP01) e' NEGATIVA (-0.29 BTC / -0.52 ETH): la backwardation
|
||||
arriva quando il trend e' gia' uscito. Firma identica ai 4 precedenti.
|
||||
· Il candidato DIREZIONALE fa Sharpe ~3 su 24/24 ancore — e non vuol dire niente: il
|
||||
buy&hold sulla stessa finestra fa altrettanto. Mediana delle DIFFERENZE APPAIATE contro
|
||||
buy&hold: {F.get('vs_bh_BTC', float('nan')):+.2f} BTC (positiva in {F.get('vs_bh_n_BTC', (0, 0))[0]}/{F.get('vs_bh_n_BTC', (0, 0))[1]} ancore) e {F.get('vs_bh_ETH', float('nan')):+.2f} ETH ({F.get('vs_bh_n_ETH', (0, 0))[0]}/{F.get('vs_bh_n_ETH', (0, 0))[1]}).
|
||||
· Null del de-levering: ETH {'NON superato' if F.get('delev_ETH') else 'superato'}, BTC {'NON superato' if F.get('delev_BTC') else 'superato'}.
|
||||
· `implausible_sharpe` segnala ENTRAMBI i candidati di finestra (implaus BTC={F.get('implaus_BTC')},
|
||||
ETH={F.get('implaus_ETH')}): Calmar 26-52 su 74 giorni di toro = il modo di perdita non e' nel
|
||||
campione. Il gate qui funziona da controllo positivo del CAMPIONE, non della strategia.
|
||||
|
||||
TEST LUNGO (proxy DVOL-RV7, 5 anni): Sharpe {F.get('proxy_sharpe', float('nan')):+.2f}, marginale vs TP01 =
|
||||
{F.get('proxy_marginal')}, uplift w25 {F.get('proxy_uplift', float('nan')):+.3f} (NEGATIVO), robust_oos=False
|
||||
=> earns_slot = False. Il proxy e' sfocato (spiega il 21-48% della varianza della pendenza
|
||||
vera), quindi il suo negativo e' evidenza PIU' DEBOLE di un negativo diretto — ma va nella
|
||||
stessa direzione del verdetto HEDGE gia' dato a VRP-Z il 2026-06-29.
|
||||
|
||||
======================================================================================
|
||||
VERDETTO: SCARTATO come sleeve e come gate. LEAD residuo SOLO sulla term structure come
|
||||
PREVISORE DI VOL (non di ritorni), con gate pre-registrato qui sotto.
|
||||
======================================================================================
|
||||
|
||||
Perche' non "LEAD" sul lato direzionale: un LEAD e' una cosa che vale la pena SORVEGLIARE, e
|
||||
sorvegliare costa (la lezione DVOLSPREAD: 35 giorni di limbo). Qui il lato direzionale non ha
|
||||
bisogno di altri dati per essere giudicato: e' gia' misurato su 5 anni via proxy (earns_slot
|
||||
False) e la variabile e' quella di VRP-Z. Aprire un monitor sarebbe pagare due volte lo stesso
|
||||
filone. Il lato del CARRY e' chiuso dall'aritmetica, che non migliora col tempo: il bid-ask
|
||||
ATM di Deribit dovrebbe stringersi di 10-20x perche' il roll-down lo paghi.
|
||||
|
||||
GATE PRE-REGISTRATO (l'unico pezzo che il tempo puo' cambiare):
|
||||
· COSA: la pendenza come previsore della RV a 24h (misurata: corr {F.get('conf_fwd_BTC', float('nan')):+.2f} / {F.get('conf_fwd_ETH', float('nan')):+.2f}).
|
||||
Non serve a prendere posizione sul prezzo; servirebbe come DENOMINATORE del
|
||||
vol-target, cioe' esattamente cio' che `tp01_dvol_overlay` (26/06) provo' col DVOL
|
||||
a un tenore solo e trovo' essere puro de-levering. La curva a piu' tenori e' l'unica
|
||||
variante di quell'esperimento mai fatta.
|
||||
· QUANDO: 2027-02-01. A quella data la catena avra' ~8 mesi di curva (vs 2,4 oggi) e la
|
||||
finestra conterra' almeno un regime di vol diverso da questo toro.
|
||||
· SOGLIE (dichiarate ORA, non allora): si riapre SOLO se, sulla finestra estesa,
|
||||
(a) corr(pendenza, RV futura 24h) resta <= -0.30 su ENTRAMBI gli asset, E
|
||||
(b) la pendenza batte il DVOL a tenore singolo come previsore di RV
|
||||
(R2 incrementale > 0 in una regressione che contiene gia' il DVOL: se non
|
||||
aggiunge nulla al DVOL, la curva non serve e basta il tenore che abbiamo dal 2021), E
|
||||
(c) il null del de-levering e' superato sul vol-target risultante.
|
||||
Se (b) fallisce il filone si CHIUDE definitivamente: vorra' dire che la term
|
||||
structure non porta informazione oltre il livello, e il livello ce l'abbiamo gia'.
|
||||
· COSA MI SMENTIREBBE OGGI: una finestra che contenga un vero crash. Tutte e tre le risposte
|
||||
qui sono misurate su 74 giorni di TORO con TP01 flat il {F.get('tp01_flat', float('nan')):.0%} del tempo. La backwardation
|
||||
severa (pendenza < -5 pt-vol) copre il {F.get('back_sev_BTC', float('nan')):.1%} delle ore BTC e il {F.get('back_sev_ETH', float('nan')):.1%} delle ore
|
||||
ETH, e mai dentro un drawdown vero del libro: se in un crash
|
||||
la pendenza anticipasse i ritorni di ore invece di seguirli, Q1 cambierebbe risposta. Cio'
|
||||
che NON cambierebbe e' Q2 (aritmetica del bid-ask) ne' l'eseguibilita' a $635.
|
||||
""")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
hr("r0822_term_structure — TERM STRUCTURE DELLA VOL IMPLICITA BTC/ETH")
|
||||
chain = load_quoted_chain()
|
||||
@@ -820,6 +933,7 @@ def main() -> None:
|
||||
sezione4(curve)
|
||||
sezione5(curve)
|
||||
sezione6()
|
||||
sezione7()
|
||||
hr("FINE")
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,25 @@ migliore su ingressi E uscite). Banda d'ancora = **23 offset**, la griglia a pri
|
||||
(`A.anchor_luck_delta`), mai differenza delle mediane. L'ancora di TP01 e' tenuta canonica in
|
||||
entrambe le braccia — §6 verifica che il segno non dipenda da lei.
|
||||
|
||||
⚠️ IL CONTROLLO CHE DECIDE — ISO-PESO. Quasi ogni variante di size alza la VOL della gamba SKH,
|
||||
e quindi il suo peso EFFETTIVO nel libro (i nominali restano 75/25). Ma "alzare il peso di SKH01"
|
||||
e' gia' stato misurato e RESPINTO il 26/07 (argmax w=0.35, `weights_tilt_null` fallito: alzarlo non
|
||||
compra Sharpe, compra dipendenza dall'ancora). Percio' il confronto pubblicato non e' vs il
|
||||
baseline nudo ma vs il baseline **RI-SCALATO allo stesso peso effettivo** (colonne ISO): li' resta
|
||||
solo la FORMA — quale trade pesa quanto. E' la regola dell'iso-rischio gia' codificata (25/07 §3).
|
||||
Corollario: siccome lo Sharpe e' invariante alla scala, `sh(k*baseline) == sh(baseline)`, quindi il
|
||||
null del de-levering in questa famiglia COINCIDE col confronto degli Sharpe. `k_iso` resta come
|
||||
diagnostica di quanto si e' mosso il DD.
|
||||
|
||||
DECOMPOSIZIONE DEL MECCANISMO (famiglia ACT). La vol trailing di uno sleeve all'88% di zeri
|
||||
contiene DUE canali: la MAGNITUDINE degli esiti e la FREQUENZA dei trade. `VTL ... attive` isola
|
||||
il primo (vol sulle sole barre attive), `ACT` isola il secondo (conteggio di barre attive nella
|
||||
finestra). Se il guadagno vive in ACT la variante non e' "controllo del rischio" ma un filtro di
|
||||
frequenza, e va detto.
|
||||
|
||||
NULL DI PERMUTAZIONE (§10). Lo stesso MULTINSIEME di size riassegnato a caso ai trade: separa
|
||||
"conta QUALE trade riceve quale size" (informazione) da "conta la DISTRIBUZIONE delle size".
|
||||
|
||||
PRESENTAZIONE. Lo Sharpe e' invariante alla scala, maxDD e CAGR no: ogni variante e' riportata
|
||||
anche a **ISO-VOL** (costante che pareggia la deviazione standard piena a quella del baseline).
|
||||
E' una scelta di presentazione, post-hoc e dichiarata; non cambia lo Sharpe, che e' cio' che
|
||||
@@ -690,7 +709,10 @@ def main() -> None:
|
||||
print(" un contributo positivo si scompone per ANNO prima di crederci) + deriva della size")
|
||||
print("-" * 112)
|
||||
yrs = sorted({int(y) for y in BK["BASE"][can].index.year})
|
||||
top = list(dict.fromkeys([best, best_iso, max(ACT, key=lambda x: RES[x]["iF"]),
|
||||
best_iv = max([n for n in SZ if n.startswith("IV")], key=lambda x: RES[x]["iF"])
|
||||
best_rk = max([n for n in SZ if n.startswith("RISK")], key=lambda x: RES[x]["iF"])
|
||||
top = list(dict.fromkeys([best, best_iso, best_iv, best_rk,
|
||||
max(ACT, key=lambda x: RES[x]["iF"]),
|
||||
max(BVT, key=lambda x: RES[x]["iF"])]))
|
||||
print(f" {'variante':<28}" + "".join(f"{y:>8}" for y in yrs))
|
||||
for nm in top:
|
||||
@@ -731,13 +753,14 @@ def main() -> None:
|
||||
rng = np.random.default_rng(20260822)
|
||||
a10 = list(offs[:: max(1, len(offs) // 3)])[:3]
|
||||
NDRAW = 150
|
||||
for nm in dict.fromkeys([best_iso, best_sleeve]):
|
||||
for nm in dict.fromkeys([best_iso, best_sleeve, best_iv, best_rk]):
|
||||
real = float(np.median([sh(BK[nm][o]) - sh(CTRL[nm][o]) for o in a10]))
|
||||
sz0 = {o: {a: sizes_of(nm, EX[o][a], SZ, VTL, ACT) for a in ASSETS} for o in a10}
|
||||
nul = []
|
||||
for _ in range(NDRAW):
|
||||
v = []
|
||||
for o in a10:
|
||||
szp = {a: rng.permutation(sizes_of(nm, EX[o][a], SZ, VTL, ACT)) for a in ASSETS}
|
||||
szp = {a: rng.permutation(sz0[o][a]) for a in ASSETS}
|
||||
lg = leg_daily(EX[o], szp)
|
||||
m_s = vol(lg) / vol(SLE["BASE"][o]) if vol(SLE["BASE"][o]) > 0 else 1.0
|
||||
v.append(sh(book(TP, lg)) - sh(book(TP, SLE["BASE"][o] * m_s)))
|
||||
|
||||
@@ -683,14 +683,18 @@ def main() -> None:
|
||||
if det:
|
||||
mm = np.array([d[1] for d in det])
|
||||
ss = np.array([d[2] for d in det])
|
||||
# il rapporto si stampa SOLO se il minimo e' positivo: max/min con min<0
|
||||
# e' un numero senza significato (prima stesura: '112.240.864x').
|
||||
rm = (f"{mm.max() / mm.min():.1f}x" if mm.min() > 0 else "SEGNO CHE CAMBIA")
|
||||
print(f" media settimanale fra le ancore: [{mm.min():+.2%}, {mm.max():+.2%}] "
|
||||
f"(rapporto {mm.max() / max(mm.min(), 1e-9):.1f}x) · "
|
||||
f"deviazione standard: [{ss.min():.2%}, {ss.max():.2%}] "
|
||||
f"(rapporto {ss.max() / max(ss.min(), 1e-9):.1f}x)")
|
||||
print(f" → l'ancora NON cambia quanto si guadagna, cambia il DENOMINATORE: con\n"
|
||||
f" {det[0][3]} settimane e nessuna perdita la vol campionaria e' quasi\n"
|
||||
f" arbitraria, e lo Sharpe con lei. Non e' che le 08:00 siano un'ora\n"
|
||||
f" magica — e' che questa statistica non porta informazione.")
|
||||
f"({rm}) · deviazione standard: [{ss.min():.2%}, {ss.max():.2%}] "
|
||||
f"({ss.max() / ss.min():.1f}x)")
|
||||
print(f" → si muovono ENTRAMBI, numeratore e denominatore"
|
||||
f"{' — e sulla media il segno CAMBIA' if mm.min() <= 0 else ''}.\n"
|
||||
f" Il motivo e' meccanico e vale la pena dirlo: spostare l'ora d'ingresso\n"
|
||||
f" cambia lo SNAPSHOT, quindi quali strike combaciano col delta bersaglio\n"
|
||||
f" — non e' lo stesso trade un'ora dopo, e' un'altra struttura. Con\n"
|
||||
f" {det[0][3]} settimane questo parametro di disturbo domina tutto il resto.")
|
||||
|
||||
print("\n (d) null del de-levering / beta — 'e' reddito da volatilita' o crypto travestito?'")
|
||||
print(" Lo spread ha delta positivo (≈ +0.18 all'ingresso): in un campione che sale del")
|
||||
@@ -765,12 +769,16 @@ def main() -> None:
|
||||
IV-rank il 2026-06-07, e bastano ~+10% di DVOL per riaprirlo. Il campione e' dentro un
|
||||
regime di vol bassa persistente, non davanti a un muro.
|
||||
|
||||
2. IL TITOLO E' QUASI TUTTO FORTUNA D'ANCORA. L'ora d'ingresso e' un max-of-k mai
|
||||
dichiarato, ed e' la prima volta che si misura su questa struttura: su 9 ancore lo
|
||||
Sharpe canonico BTC 32.58 sta all'89 percentile e la MEDIANA ONESTA e' 1.45, con la
|
||||
banda che tocca il NEGATIVO (-0.35). Fortuna +31.13. Su ETH 3.90 -> 2.41.
|
||||
Il numero da citare per BTC e' 1.45, non 32.58 — e a quel punto non c'e' piu' niente
|
||||
di straordinario da spiegare.
|
||||
2. IL TITOLO NON SOPRAVVIVE ALL'ORA D'INGRESSO. E' un max-of-k mai dichiarato, misurato
|
||||
qui per la prima volta su questa struttura: su 9 ancore lo Sharpe canonico BTC 32.58
|
||||
sta all'89 percentile, la MEDIANA ONESTA e' 1.45 e la banda tocca il NEGATIVO (-0.35).
|
||||
Su ETH 3.90 -> 2.41. Il numero da citare per BTC e' 1.45, non 32.58.
|
||||
⚠️ E il meccanismo non e' quello che avevo scritto prima di guardarlo. Non e' solo la
|
||||
vol campionaria a ballare: fra le ancore la MEDIA settimanale BTC va da -1.35% a
|
||||
+11.22% del rischio — CAMBIA SEGNO — e la deviazione standard di 11x (ETH: media 2.9x,
|
||||
sd 2.4x). Spostare l'ora d'ingresso cambia lo snapshot e quindi quali strike combaciano
|
||||
col delta bersaglio: non e' lo stesso trade un'ora dopo, e' UN'ALTRA STRUTTURA. A 10
|
||||
settimane questo parametro di disturbo domina il segnale che si vorrebbe misurare.
|
||||
|
||||
3. LA FAMIGLIA CHE HA STORIA NON E' QUELLA CHE SI PUO' ESEGUIRE. Le quote raccolte sono
|
||||
le opzioni INVERSE (margine e regolamento in BTC/ETH); il conto e' in USDC e il book
|
||||
@@ -819,6 +827,31 @@ def main() -> None:
|
||||
regime sulla famiglia eseguibile. Prima di allora ogni misura su questa struttura
|
||||
descrivera' la sua astensione.""")
|
||||
|
||||
hr("COSA MI SMENTIREBBE", "-")
|
||||
print(""" Tre osservazioni, tutte ottenibili, che ribalterebbero questo verdetto — e una che NON
|
||||
lo ribalterebbe, messa qui perche' e' quella che arrivera' per prima.
|
||||
|
||||
1. IL CONTO. Se un saldo USDC puo' marginare le opzioni INVERSE, il punto 3 si sgonfia da
|
||||
muro a dettaglio: la struttura ETH inverse (1 lotto, rischio ~$88, credito ~$12 la
|
||||
settimana) e' gia' eseguibile a $600 oggi. Resterebbero i punti 1, 2 e 4 — sufficienti
|
||||
a tenere il verdetto, ma il filone tornerebbe misurabile SUBITO invece che fra un anno.
|
||||
Costo della verifica: un ordine di prova, come per il PRIIPs.
|
||||
|
||||
2. IL REGIME. Se, raccolte ~un anno di settimane col gate IV-rank APERTO, la media
|
||||
settimanale restasse positiva CON la banda d'ancora che non contiene lo zero, allora
|
||||
«short-vol da QUOTE» sarebbe dimostrato e la regola del 19/06 andrebbe riscritta.
|
||||
E' la sola prova che conta: la banda d'ancora, non la media.
|
||||
|
||||
3. IL LIBRO. Se l'open interest di ETH_USDC salisse stabilmente sopra ~100 sugli strike a
|
||||
δ -0.28/-0.10 con spread relativo sotto ~15%, esisterebbe una struttura regolata in
|
||||
USDC ed eseguibile a $242 per lotto: il filone si riaprirebbe sul DATO invece che
|
||||
sulla teoria. Ma senza raccoglierla adesso non lo sapremo mai — vedi azione (A).
|
||||
|
||||
⚠️ COSA NON MI SMENTIREBBE, ed e' quello che succedera' per primo: un'altra finestra di
|
||||
10 settimane di vol bassa con 10/10 vincenti e uno Sharpe a due cifre. Sarebbe lo
|
||||
STESSO campione, piu' lungo — non evidenza nuova. Un campione che non contiene il modo
|
||||
di perdere non diventa informativo aggiungendo settimane in cui non si perde.""")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user