research(wave-0822): XSR-REPRO trova un difetto di produzione (i monitor forward registrano ~41 min/giorno); SKEW scompone il f di VRP01 — il 42% e' struttura a termine
This commit is contained in:
@@ -15,6 +15,8 @@ null de-levering superato + eseguibilita' al capitale dichiarato.
|
||||
| 7 | TERM-STRUCTURE | **SCARTATO** + 1 LEAD stretto (decisione 2027-02-01) | la pendenza e' un **termometro contemporaneo** (picco CCF a lag −1h/0h); il carry non paga il bid-ask (**18,6 giorni di roll-down per pagare UN round-trip**); come gate e' la **5ª occorrenza** della ridondanza col trend |
|
||||
| 6 | OI-PIN | **SCARTATO** | il max-pain batte uno strike casuale ma **non batte mai (0/24) la media a 7 giorni dello spot**, un livello che non usa NESSUN dato di opzioni: non e' pinning, e' reversione verso il centro recente con l'OI come stimatore rumoroso di quel centro |
|
||||
| 8 | VOL-SIZE | **LEAD** (gate 2026-12-22) + 1 falsificazione | dare a SKH01 una size per-trade regge a **23/23 ancore**, 8/8 anni, null di permutazione e trasferimento su V1 — ma vale **+0,07 di Sharpe di libro, un terzo della fortuna d'ancora del libro stesso (+0,196)**. Il vol-target di **libro** e' invece falsificato: compra peso SKH gia' respinto e peggiora l'eseguibilita' |
|
||||
| — | **XSR-REPRO** (integrita') | 🚨 **DIFETTO DI PRODUZIONE** | il numero 1.82 e' SPIEGATO e non era sbagliato (era su una **terza** lente, e su una barra non ancora chiusa) — ma cercandone la causa e' emerso che **`paper_xsr` registra ~41 minuti di mercato al giorno**, non un giorno. Tre gate pre-registrati leggono serie costruite cosi' |
|
||||
| 5 | SKEW | **SCARTATO** (Q1, Q2) + **LEAD** (Q3, gate 2027-02-22) | il prezzo muove lo skew (t 3,1-9,4 su 8/8 test), **non il contrario** (max |t| in avanti 2,35 contro 2,08 atteso dal rumore). Ma Q3 e' grosso: **il f=0,73 di VRP01 e' per il 42% STRUTTURA A TERMINE e solo per il 25% skew** |
|
||||
|
||||
## Note che sopravvivono ai singoli filoni
|
||||
|
||||
@@ -282,3 +284,81 @@ zeri nei due canali, il guadagno FULL viene dalla **FREQUENZA** (il solo filtro
|
||||
spiegazione e' sbagliata anche se il numero regge.** Entro **2026-12-22**, prima di qualunque
|
||||
modifica a peso/config di SKH01. Deploy solo se ISO dShFULL > 0 in >=20/23 ancore E
|
||||
`weights_tilt_null` passa. **Book, pesi, cron, config INVARIATI.**
|
||||
|
||||
### XSR-REPRO (r0822_xsr_repro.py) — filone di INTEGRITA', non di alpha
|
||||
✅ **Il numero pubblicato e' spiegato e NON era sbagliato.** 1.82 viene da una **terza** lente
|
||||
(`r0725_statarb_demean_skeptic.ret_from_pos`, divisore **fisso 50**), non da quella che ha girato i
|
||||
gate (`basket_from_positions`, divisore variabile). Sullo stato del dato del 25/07 ricostruito:
|
||||
**11 numeri pubblicati su 12 riprodotti** (Sharpe netta 1.8164, lorda 2.6956, DD −2.585%, lag
|
||||
1.82/1.19/0.81/0.50, Sharpe 2024 1.0297 / 2025 1.9752 / 2026 3.1078).
|
||||
📌 **Ma il numero di TITOLO e i numeri di GATE non sono mai stati la stessa serie:** la lente dei
|
||||
gate da' **1.79** — ed e' esattamente il "Sharpe 1.79" che il docstring dello scettico gia' citava.
|
||||
DSR ri-calcolato oggi sulla lente giusta: **0.983 PASS** (pubblicato 0.985) -> **il gate DSR regge**.
|
||||
✅ **L'ipotesi "il cron riscrive la storia" e' FALSIFICATA** da due prove indipendenti: (a) il
|
||||
registro del cron su 54 simboli, 25/07 vs 22/08, mostra crescita di **esattamente 28 barre per
|
||||
tutti** con `start_reale` invariato; (b) lo Sharpe degli **anni chiusi** si riproduce **al quarto
|
||||
decimale** attraverso quattro settimane di riscritture. **Cambia una sola barra: l'ultima.**
|
||||
🚨 **IL DIFETTO CHE CONTA — `paper_xsr` registra ~41 minuti di mercato al giorno.**
|
||||
`fetch_hyperliquid` gira alle 00:30 con `END = oggi` e scrive la barra del **giorno in corso**;
|
||||
`advance()` processa le barre con `ts > last_ts` e **porta `last_ts` su quella parziale** -> le 23
|
||||
ore e mezza restanti di **ogni** giorno non entrano in nessun rendimento registrato.
|
||||
**Misurato** (28 barre appaiate contro il replay): **1/28 identiche · corr −0,045 · vol registrata
|
||||
0,46% contro 2,74% ricalcolata** (la strategia e' progettata a 2,3%). Rapporto di varianza 0,0286
|
||||
-> ~41 min/giorno; stima indipendente dal volume: ~69 min.
|
||||
✅ **Verifica del coordinatore, indipendente:** l'ultima barra di `hl_btc_1d` ha volume **1.274**
|
||||
contro **66.926** e **102.198** dei due giorni precedenti.
|
||||
⚠️ **`monitor_health` lo dichiara OK e non puo' vederlo:** la serie e' fresca e senza buchi.
|
||||
**Una serie fresca, completa e SBAGLIATA passa ogni controllo di freschezza.**
|
||||
⚠️ Stessa **forma** di codice in `paper_statarb` (gate 27/09), che per giunta ha un docstring
|
||||
"barre 1d chiuse" mentre consuma `altlib.get(...,"1d") -> resample_tf`, che **non** scarta il giorno
|
||||
in corso. Segnalato come forma, non misurato -> **audit dedicato in corso su tutti e 6 i monitor**.
|
||||
⚠️ **Haircut, la seconda gamba del gate 23/10, e' piu' fragile della prima:** ticket per gamba
|
||||
**mediano $3,33 / medio $5,98**, **63% degli ordini sotto $5**; il pubblicato **$14,41 non ha alcuno
|
||||
script committato che lo riproduca** ed e' **~2,4x ottimista**. A min-order $10 la guardia
|
||||
formalmente passa ma si esegue **un ordine su cinque** — e il criterio pre-registrato non lo guarda.
|
||||
📌 **NUMERO ONESTO DA CITARE:** lente dei gate, sole barre chiuse -> **1.79** (finestra di scoperta)
|
||||
/ **1.63** (a oggi). **Va sempre citata la coppia (lente, ultima barra chiusa)**, mai il numero nudo.
|
||||
📌 **GATE 23/10 COMPROMESSO: PARZIALMENTE** — ammissione NO (storia chiusa stabile, DSR regge),
|
||||
**forward SI** (metrica primaria su una serie scorrelata dalla realta'), haircut SI (parametro senza
|
||||
script e ~2,4x ottimista). **La finestra forward non e' recuperabile a posteriori**; riparare
|
||||
`advance()` la **azzera**. Scelta fra gate letto su strumento rotto e gate rimandato con contatore
|
||||
nuovo: **va decisa PRIMA del 23/10, non quel giorno.**
|
||||
|
||||
### 5 — SKEW (r0822_skew.py, 37 trial dichiarati)
|
||||
- **Q1 (skew come segnale direzionale): SCARTATO, e non per mancanza di potenza — la potenza c'e' e
|
||||
falsifica.** Il **prezzo precede lo skew** con t da **3,13 a 9,39 su 8/8 test** e su entrambi gli
|
||||
asset; in avanti il max |t| su 16 test e' **2,35** contro **2,08** atteso dal rumore, e non replica
|
||||
sul secondo asset. L'unica cella con lead apparente crolla togliendo **4 giorni di rally su 24**.
|
||||
DSR **0,163 FAIL**; IC95 block-bootstrap sullo Sharpe: **[−2,90, +5,86]** (ampiezza 8,76).
|
||||
- **Q2 (skew come gate di de-risk): SCARTATO (non misurabile)** — nella finestra TP01 e' attivo
|
||||
**5 giorni su 75** con maxDD **0,14%**: non c'e' sinistro. Unica cosa leggibile: **5ª firma di
|
||||
ridondanza col trend** (il gate spegne 16 giorni in cui TP01 era gia' flat, **16/16**).
|
||||
- 📌 **Q3 — LEAD, ed e' il risultato della sessione. Il f=0,73 di VRP01 scomposto** (attribuzione
|
||||
esatta in log, i fattori si sommano):
|
||||
| causa | fattore | quota del difetto |
|
||||
|---|---|---|
|
||||
| **struttura a termine** | x0,869 | **42,3%** |
|
||||
| spread denaro-lettera | x0,904 | 30,5% |
|
||||
| **skew** | x0,920 | **25,0%** |
|
||||
| fit (controllo) | x0,992 | 2,3% |
|
||||
**La riga che conta: il sleeve prezza un'opzione a 7 GIORNI col DVOL a 30 GIORNI**, che nel **90%**
|
||||
delle ore sta ~3 punti di vol **sopra** l'ATM a 7 giorni -> sovrapprezza **ENTRAMBE** le gambe.
|
||||
Il "+0,8pp sulla gamba corta" pubblicato il 30/07 — che faceva sembrare **corretta** la gamba
|
||||
venduta — e' la **cancellazione di due errori da ~3pp** (−2,97 di termine, +2,79 di skew).
|
||||
⚠️ **E il progetto aveva gia' codificato la regola il 03/07** ("riprezzare term-structure-consistent
|
||||
prima di credere a un numero da struttura BS-flat") **e non l'ha applicata alla misura del 30/07**.
|
||||
📌 **Verso, e non e' quello che sembra:** `f_term` passa da **0,83** (IV-rank basso) a **0,93**
|
||||
(alto) e supera **1,0 in backwardation**, mentre `f_skew` resta piatto (0,91-0,93) -> **la parte
|
||||
maggiore del difetto si annulla proprio dove il gate IV-rank fa tradare il sleeve**, quindi
|
||||
f=0,73 e' plausibilmente **CONSERVATIVO li'**. Non misurato: **0/22** ingressi passano il gate, e
|
||||
l'unico episodio ad alta vol del campione e' un **rally**, non un crash. In un crash i due pezzi
|
||||
vanno in **direzioni contrarie** e il netto e' ignoto. **`VRP_CFG["f"]` NON cambiato.**
|
||||
Gate pre-registrato: rimisurare a **>=6 ingressi settimanali per asset con IV-rank>0,30**
|
||||
(il collettore li accumula da solo), revisione comunque **2027-02-22**.
|
||||
- ✅ Confound di modello **ESCLUSO e misurato**: `f_markfit` = **0,992** (la mark IV di Deribit
|
||||
riprezza i propri mid entro l'1%) -> RR/BF sono un fatto di **prezzo**, non l'output del fit.
|
||||
- ✅ **Controllo positivo cablato nello script:** con l'ancora **assunta** a :30 invece del `ts_max`
|
||||
osservato compare un **falso lead** a +15m (0,102 contro 0,031) — *"e' l'errore che avrei
|
||||
pubblicato senza quel controllo"*.
|
||||
- ✅ Smile vettorizzato (512s -> 2,0s) verificato **bit-exact** contro il ciclo lento su 21.244 celle.
|
||||
- ✅ **Convergenza indipendente** con l'agente TERM-STRUCTURE: **75 giorni utili** (l'altro trovava 74).
|
||||
|
||||
@@ -0,0 +1,530 @@
|
||||
"""ALT-OPT — la famiglia opzioni USDC-LINEARE di Deribit, mai guardata dal progetto (2026-08-22).
|
||||
|
||||
CONTESTO. Il filone VRP-QUOTE-VERE di questa stessa ondata ha trovato che `collect_chain.py`
|
||||
interroga `{"currency": "BTC"|"ETH"}`, che su Deribit restituisce le sole opzioni INVERSE
|
||||
(regolate in BTC/ETH). Il conto e' in USDC. Esiste accanto una famiglia USDC-LINEARE con lotti
|
||||
~10x piu' piccoli, e su alcuni sottostanti (SOL, XRP, HYPE, AVAX) e' molto piu' popolata di
|
||||
BTC_USDC. Il progetto non l'ha mai misurata.
|
||||
|
||||
DOMANDA. Esiste su questa famiglia una struttura a RISCHIO DEFINITO (put credit spread, la stessa
|
||||
geometria di VRP01: vendi ~-0.28, compri ~-0.10) eseguibile con qualche centinaio di dollari,
|
||||
con spread e liquidita' che non se la mangino?
|
||||
|
||||
COSA MISURA, in ordine, e si FERMA appena un vincolo la uccide:
|
||||
1. i fatti del venue letti dall'API pubblica (min_trade_amount, tick_size, contract_size,
|
||||
costo in $ di UN lotto), e la LIQUIDITA' vera: spread relativo (ask-bid)/mid ai delta che
|
||||
servono davvero, non sull'ATM, + profondita' in cima al book;
|
||||
2. il collaterale: margine della gamba corta secondo la formula pubblicata dal venue, e
|
||||
max-loss della struttura (i due numeri differiscono per un fatto — se il venue netta le
|
||||
gambe — che l'API PUBBLICA NON dice: e' dichiarato come tale, non indovinato);
|
||||
3. quanta storia c'e' (risposta: nessuna) e quindi cosa si potrebbe al massimo concludere.
|
||||
|
||||
COSA NON PUO' FARE, e va detto prima dei numeri: **non esiste un backtest possibile**. Il nostro
|
||||
collettore raccoglie solo le inverse BTC/ETH e l'archivio ereditato pure. Questo file misura le
|
||||
condizioni di UN ISTANTE. Il massimo verdetto ottenibile e' `LEAD` = "vale la pena raccogliere il
|
||||
dato", con il costo di raccolta stimato in chiamate/ora sul giro esistente.
|
||||
|
||||
VINCOLI DI PROGETTO CITATI, non aggirati:
|
||||
(a) "niente short-vol da modello in deploy" (19/06). Qui i prezzi sono VERI, non modellati —
|
||||
che e' un'altra cosa — ma la regola resta finche' non esiste un campione che contenga uno
|
||||
STRESS. Un istante di calma non e' un campione.
|
||||
(b) SOL e' escluso dall'universo DIREZIONALE per decisione dell'operatore del 22/08. Quella
|
||||
decisione riguarda TP01/SKH01 congelati come terza gamba di trend, e il suo stesso testo
|
||||
dichiara che un meccanismo diverso la riapre. Un put credit spread non e' direzionale nel
|
||||
senso di quella decisione. Ma il guardrail sul DATO vale lo stesso: nessun file nuovo in
|
||||
`data/raw/`, e i prezzi del sottostante qui vengono da `underlying_price` della catena,
|
||||
mai da `load_data`.
|
||||
(c) il gate IV-rank>0.30 e' l'unico alpha misurato di VRP01, e su questi sottostanti NON e'
|
||||
calcolabile: non abbiamo un giorno di storia di vol implicita. Senza quel gate la struttura
|
||||
non e' VRP01, e' vendere vol a caso.
|
||||
|
||||
CONVENZIONE DI UNITA' (la trappola di questo file): le opzioni INVERSE si quotano in CRIPTO per
|
||||
contratto, le USDC-LINEARI in USDC per unita' di sottostante. Confrontare i due premi senza
|
||||
convertire darebbe numeri sbagliati di un fattore ~10^4-10^5. Qui ogni importo in dollari passa
|
||||
per `prem_usd()`, e le colonne unit-free (spread relativo, costo/credito) sono le uniche
|
||||
direttamente confrontabili fra le due famiglie.
|
||||
|
||||
RETE: sole letture pubbliche, nessun ordine, pacing a ~2 richieste/s, e il file SI RIFIUTA di
|
||||
partire nella finestra :24-:29 (e' quando `cron_chain` usa lo stesso IP: il rate limit Deribit e'
|
||||
per-IP e il progetto ha gia' avuto un guasto per questo, 29/07).
|
||||
|
||||
nice -n 19 timeout 900 uv run python scripts/research/r0822_alt_options.py
|
||||
nice -n 19 timeout 900 uv run python scripts/research/r0822_alt_options.py --cache # riusa
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
|
||||
API = "https://www.deribit.com/api/v2/public"
|
||||
TIMEOUT = 20
|
||||
RPS = 2.0
|
||||
CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/"
|
||||
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/altopt_cache.json")
|
||||
|
||||
SHORT_DELTA, LONG_DELTA = -0.28, -0.10 # la geometria di VRP01, non ri-scelta qui
|
||||
DTE_LO, DTE_HI = 4.0, 45.0 # dal settimanale di VRP01 (4-10) fino a ~1.5 mesi
|
||||
OI_MIN = 100.0 # la stessa soglia di collect_chain.py
|
||||
CAPITAL = 635.0 # il conto vero
|
||||
TAKER_CAP = 0.125 # cap fee opzioni Deribit: 12.5% del premio
|
||||
|
||||
LINEAR = ("SOL_USDC", "XRP_USDC", "ETH_USDC", "HYPE_USDC", "AVAX_USDC", "BTC_USDC")
|
||||
INVERSE = ("BTC", "ETH") # termine di paragone: il progetto le conosce
|
||||
|
||||
# ---------------------------------------------------------------- rete (parca, sola lettura)
|
||||
|
||||
|
||||
class Budget:
|
||||
def __init__(self, rps: float = RPS) -> None:
|
||||
self.rps, self._next, self.calls, self.err, self.r429 = rps, 0.0, 0, 0, 0
|
||||
|
||||
def wait(self) -> None:
|
||||
now = time.monotonic()
|
||||
if now < self._next:
|
||||
time.sleep(self._next - now)
|
||||
self._next = max(now, self._next) + 1.0 / self.rps
|
||||
|
||||
|
||||
def guardia_finestra() -> None:
|
||||
m = datetime.now(UTC).minute
|
||||
if 24 <= m <= 29:
|
||||
raise SystemExit(f"minuto {m}: finestra di cron_chain (:25-:29). Rilancia dopo il :30.")
|
||||
|
||||
|
||||
def get(path: str, params: dict, b: Budget, tries: int = 3):
|
||||
for k in range(tries):
|
||||
b.wait()
|
||||
b.calls += 1
|
||||
try:
|
||||
r = requests.get(f"{API}/{path}", params=params, timeout=TIMEOUT)
|
||||
except Exception:
|
||||
b.err += 1
|
||||
time.sleep(1.5 * (k + 1))
|
||||
continue
|
||||
if r.status_code == 429:
|
||||
b.r429 += 1
|
||||
time.sleep(2.0 * (k + 1))
|
||||
continue
|
||||
if r.status_code != 200:
|
||||
b.err += 1
|
||||
time.sleep(1.0 * (k + 1))
|
||||
continue
|
||||
try:
|
||||
return r.json()["result"]
|
||||
except Exception:
|
||||
b.err += 1
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def fetch_all(b: Budget) -> dict:
|
||||
"""6 chiamate: instruments + book_summary per USDC, BTC, ETH. Niente per-strumento."""
|
||||
out = {}
|
||||
for cur in ("USDC", "BTC", "ETH"):
|
||||
out[f"inst_{cur}"] = get("get_instruments",
|
||||
{"currency": cur, "kind": "option", "expired": "false"}, b) or []
|
||||
out[f"summ_{cur}"] = get("get_book_summary_by_currency",
|
||||
{"currency": cur, "kind": "option"}, b) or []
|
||||
if not out["inst_USDC"]: # il venue potrebbe non accettare currency=USDC
|
||||
out["inst_USDC"] = get("get_instruments",
|
||||
{"currency": "any", "kind": "option", "expired": "false"}, b) or []
|
||||
out["summ_USDC"] = get("get_book_summary_by_currency",
|
||||
{"currency": "any", "kind": "option"}, b) or []
|
||||
return out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Black-Scholes (r=0, come il sleeve)
|
||||
|
||||
|
||||
def put_delta(S: float, K: float, T: float, sig: float) -> float:
|
||||
if not (S > 0 and K > 0 and T > 0 and sig > 0):
|
||||
return float("nan")
|
||||
d1 = (math.log(S / K) + 0.5 * sig * sig * T) / (sig * math.sqrt(T))
|
||||
return 0.5 * (1.0 + math.erf(d1 / math.sqrt(2.0))) - 1.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- unita'
|
||||
|
||||
|
||||
def is_inverse(fam: str) -> bool:
|
||||
return fam in INVERSE
|
||||
|
||||
|
||||
def prem_usd(price: float, r: dict) -> float:
|
||||
"""Premio in DOLLARI di `min_trade_amount` contratti quotati a `price`.
|
||||
|
||||
Lineare: il prezzo e' gia' in USDC per unita' di sottostante.
|
||||
Inverse: il prezzo e' in CRIPTO per contratto -> si moltiplica per il sottostante.
|
||||
"""
|
||||
k = r["csize"] * r["minamt"]
|
||||
return price * k * (r["S"] if is_inverse(r["fam"]) else 1.0)
|
||||
|
||||
|
||||
def loss_usd(Ks: float, Kl: float, r: dict) -> float:
|
||||
"""Perdita massima strutturale in dollari, per `min_trade_amount` contratti.
|
||||
|
||||
Lineare: esatta, (Ks-Kl) x quantita'. Inverse: il payoff e' in cripta (max(0,K-S)/S), il
|
||||
peggiore e' a S=Kl e vale (Ks-Kl)/Kl cripto = (Ks-Kl) dollari a quel prezzo -> stessa
|
||||
formula, ma APPROSSIMATA (il valore in dollari della perdita dipende da dove finisce S).
|
||||
"""
|
||||
return (Ks - Kl) * r["csize"] * r["minamt"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- margine (FONTE SECONDARIA)
|
||||
|
||||
|
||||
def im_short_put_usd(S: float, K: float, mark_usd: float, qty: float) -> float:
|
||||
"""Initial margin di una short put, in USDC, secondo la formula PUBBLICATA da Deribit.
|
||||
|
||||
⚠️ FONTE SECONDARIA, dichiarata: l'API PUBBLICA non espone il margine. Il numero che decide
|
||||
davvero — se Deribit NETTA le due gambe di uno spread in margine standard — non e' leggibile
|
||||
da qui: si conferma con `private/get_margins`, che richiede una chiave (lettura, ma non
|
||||
pubblica) e quindi NON e' stato usato.
|
||||
"""
|
||||
if not (S > 0 and K > 0):
|
||||
return float("nan")
|
||||
otm = max(0.0, S - K) / S # una put e' OTM quando K < S
|
||||
per_contratto = max(max(0.15 - otm, 0.10) * S, 0.075 * S) + max(0.0, mark_usd / max(qty, 1e-12))
|
||||
return per_contratto * qty
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- costruzione della vista
|
||||
|
||||
|
||||
def build(raw: dict) -> list[dict]:
|
||||
inst, summ = {}, {}
|
||||
for cur in ("USDC", "BTC", "ETH"):
|
||||
for i in raw.get(f"inst_{cur}") or []:
|
||||
inst[i["instrument_name"]] = i
|
||||
for s in raw.get(f"summ_{cur}") or []:
|
||||
summ[s["instrument_name"]] = s
|
||||
rows = []
|
||||
now_ms = datetime.now(UTC).timestamp() * 1000.0
|
||||
for name, i in inst.items():
|
||||
s = summ.get(name)
|
||||
if s is None:
|
||||
continue
|
||||
fam = name.split("-")[0]
|
||||
dte = (float(i.get("expiration_timestamp") or 0) - now_ms) / 86400_000.0
|
||||
bid, ask = s.get("bid_price"), s.get("ask_price")
|
||||
bid = float(bid) if bid else None # Deribit manda 0/None per "nessun lato"
|
||||
ask = float(ask) if ask else None
|
||||
iv, S = s.get("mark_iv"), s.get("underlying_price")
|
||||
K = float(i.get("strike") or 0)
|
||||
typ = "P" if i.get("option_type") == "put" else "C"
|
||||
T = max(dte, 0.0) / 365.25
|
||||
d = (put_delta(float(S), K, T, float(iv) / 100.0)
|
||||
if (S and iv and K and T > 0 and typ == "P") else float("nan"))
|
||||
rows.append(dict(
|
||||
name=name, fam=fam, typ=typ, K=K, dte=dte, S=float(S) if S else float("nan"),
|
||||
iv=float(iv) if iv else float("nan"), delta_bs=d,
|
||||
bid=bid, ask=ask, mid=((bid + ask) / 2 if (bid and ask) else None),
|
||||
mark=float(s.get("mark_price") or 0.0),
|
||||
oi=float(s.get("open_interest") or 0.0), vol=float(s.get("volume") or 0.0),
|
||||
tick=float(i.get("tick_size") or 0.0), steps=i.get("tick_size_steps") or [],
|
||||
minamt=float(i.get("min_trade_amount") or 0.0),
|
||||
csize=float(i.get("contract_size") or 0.0),
|
||||
taker=i.get("taker_commission"), settle=i.get("settlement_currency"),
|
||||
))
|
||||
return rows
|
||||
|
||||
|
||||
def rel_spread(r: dict) -> float:
|
||||
if r["bid"] is None or r["ask"] is None or not r["mid"]:
|
||||
return float("nan")
|
||||
return (r["ask"] - r["bid"]) / r["mid"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 1. i fatti del venue
|
||||
|
||||
|
||||
def famiglie(rows: list[dict]) -> dict:
|
||||
print("\n" + "=" * 104)
|
||||
print("1a. I FATTI DEL VENUE — public/get_instruments + get_book_summary_by_currency")
|
||||
print("=" * 104)
|
||||
print(f"{'famiglia':<10} {'strum':>6} {'OI>=100':>8} {'min_amt':>9} {'c_size':>7} "
|
||||
f"{'tick':>9} {'sottost.$':>11} {'$/lotto':>10} {'taker':>8} {'settle':>7}")
|
||||
tab = {}
|
||||
for f in sorted({r["fam"] for r in rows}):
|
||||
sub = [r for r in rows if r["fam"] == f]
|
||||
liq = [r for r in sub if r["oi"] >= OI_MIN]
|
||||
S = float(np.nanmedian([r["S"] for r in sub]))
|
||||
mins = {r["minamt"] for r in sub}
|
||||
cs = {r["csize"] for r in sub}
|
||||
a = sub[0]
|
||||
lot = a["minamt"] * a["csize"] * S
|
||||
tab[f] = dict(n=len(sub), nliq=len(liq), minamt=a["minamt"], csize=a["csize"],
|
||||
tick=a["tick"], S=S, lot=lot, taker=a["taker"])
|
||||
if f in LINEAR or f in INVERSE:
|
||||
avv = "" if (len(mins) == 1 and len(cs) == 1) else " <- min/c_size NON uniformi"
|
||||
print(f"{f:<10} {len(sub):>6} {len(liq):>8} {a['minamt']:>9.4g} {a['csize']:>7.4g} "
|
||||
f"{a['tick']:>9.5g} {S:>11.2f} {lot:>10.2f} {str(a['taker']):>8} "
|
||||
f"{str(a['settle']):>7}{avv}")
|
||||
print("\n `$/lotto` = min_trade_amount x contract_size x sottostante = il NOZIONALE di un")
|
||||
print(" lotto (NON il premio, che e' molto minore). E' la granularita' minima imposta dal venue.")
|
||||
print(" `taker` per le opzioni Deribit e' una frazione del SOTTOSTANTE con cap al 12.5% del")
|
||||
print(" premio: su un'ala molto OTM morde il CAP, non la percentuale.")
|
||||
return tab
|
||||
|
||||
|
||||
def replica(tab: dict) -> None:
|
||||
print("\n" + "-" * 104)
|
||||
print("1b. CONTROLLO POSITIVO — riproduco i conteggi dichiarati dal coordinatore (22/08)?")
|
||||
print("-" * 104)
|
||||
atteso = {"BTC": (1038, 415), "ETH": (932, 548), "BTC_USDC": (686, 5), "ETH_USDC": (660, 119),
|
||||
"SOL_USDC": (574, 341), "XRP_USDC": (522, 250), "HYPE_USDC": (396, 172),
|
||||
"AVAX_USDC": (294, 146), "TRX_USDC": (310, 115)}
|
||||
print(f"{'famiglia':<10} {'strum att':>10} {'strum oggi':>11} {'liq att':>8} {'liq oggi':>9}")
|
||||
for f, (n_a, l_a) in atteso.items():
|
||||
t = tab.get(f)
|
||||
if t is None:
|
||||
print(f"{f:<10} {n_a:>10} {'ASSENTE':>11} {l_a:>8} {'-':>9}")
|
||||
else:
|
||||
print(f"{f:<10} {n_a:>10} {t['n']:>11} {l_a:>8} {t['nliq']:>9}")
|
||||
print(" La catena si muove di ora in ora (scadenze che nascono, OI che cambia): scarti di")
|
||||
print(" poche unita' sono la misura, non un disaccordo. Uno scarto GRANDE sarebbe un problema.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 2. liquidita' ai delta utili
|
||||
|
||||
|
||||
def gambe(rows: list[dict], fam: str) -> list[dict]:
|
||||
puts = [r for r in rows if r["fam"] == fam and r["typ"] == "P"
|
||||
and DTE_LO <= r["dte"] <= DTE_HI and np.isfinite(r["delta_bs"])]
|
||||
out = []
|
||||
for e in sorted({round(r["dte"], 4) for r in puts}):
|
||||
g = [r for r in puts if round(r["dte"], 4) == e]
|
||||
s = min(g, key=lambda r: abs(r["delta_bs"] - SHORT_DELTA))
|
||||
lo = min(g, key=lambda r: abs(r["delta_bs"] - LONG_DELTA))
|
||||
if s["K"] > lo["K"]:
|
||||
out.append({"dte": e, "short": s, "long": lo, "n_strike": len(g)})
|
||||
return out
|
||||
|
||||
|
||||
def liquidita(rows: list[dict]) -> dict:
|
||||
print("\n" + "=" * 104)
|
||||
print(f"2. LA LIQUIDITA' AI DELTA CHE SERVONO ({SHORT_DELTA} corta / {LONG_DELTA} lunga),")
|
||||
print(f" NON sull'ATM. Finestra {DTE_LO:.0f}-{DTE_HI:.0f} giorni. Delta = BS su mark_iv, r=0.")
|
||||
print("=" * 104)
|
||||
print(f"{'famiglia':<10} {'scad':>5} {'2lati':>6} {'d corta':>8} {'d lunga':>8} "
|
||||
f"{'spr% corta':>11} {'spr% lunga':>11} {'credito$':>9} {'costo/cred':>11} "
|
||||
f"{'fee/cred':>9} {'maxloss$':>9}")
|
||||
res = {}
|
||||
for fam in list(LINEAR) + list(INVERSE):
|
||||
gg = gambe(rows, fam)
|
||||
if not gg:
|
||||
print(f"{fam:<10} {'0':>5} nessuna scadenza utilizzabile nella finestra")
|
||||
continue
|
||||
ss, ll, ds, dl, cred, cost, fee, ml = [], [], [], [], [], [], [], []
|
||||
due = neg = 0
|
||||
for g in gg:
|
||||
s, lo = g["short"], g["long"]
|
||||
rs, rl = rel_spread(s), rel_spread(lo)
|
||||
if not (np.isfinite(rs) and np.isfinite(rl)):
|
||||
continue
|
||||
due += 1
|
||||
c_mid = prem_usd(s["mid"], s) - prem_usd(lo["mid"], lo)
|
||||
c_exe = prem_usd(s["bid"], s) - prem_usd(lo["ask"], lo)
|
||||
if c_mid <= 0:
|
||||
neg += 1
|
||||
continue
|
||||
# fee taker Deribit: min(0.03% del sottostante, 12.5% del premio) per gamba, x2 gambe,
|
||||
# x2 (apertura + chiusura/esercizio). Conservativo ma e' il listino.
|
||||
def _fee(r: dict) -> float:
|
||||
notion = r["S"] * r["csize"] * r["minamt"]
|
||||
return min(float(r["taker"] or 0.0003) * notion, TAKER_CAP * prem_usd(r["mid"], r))
|
||||
ss.append(rs); ll.append(rl)
|
||||
ds.append(s["delta_bs"]); dl.append(lo["delta_bs"])
|
||||
cred.append(c_mid)
|
||||
cost.append((c_mid - c_exe) / c_mid)
|
||||
fee.append(2.0 * (_fee(s) + _fee(lo)) / c_mid)
|
||||
ml.append(loss_usd(s["K"], lo["K"], s) - c_exe)
|
||||
if not cred:
|
||||
print(f"{fam:<10} {len(gg):>5} {due:>6} nessuna struttura con credito positivo a mid "
|
||||
f"({neg} a credito<=0)")
|
||||
continue
|
||||
r = dict(n=len(gg), due=due, neg=neg, s=float(np.median(ss)), l=float(np.median(ll)),
|
||||
ds=float(np.median(ds)), dl=float(np.median(dl)), cred=float(np.median(cred)),
|
||||
cost=float(np.median(cost)), fee=float(np.median(fee)), ml=float(np.median(ml)))
|
||||
res[fam] = r
|
||||
print(f"{fam:<10} {r['n']:>5} {due:>6} {r['ds']:>8.3f} {r['dl']:>8.3f} "
|
||||
f"{r['s']*100:>10.1f}% {r['l']*100:>10.1f}% {r['cred']:>9.2f} "
|
||||
f"{r['cost']*100:>10.1f}% {r['fee']*100:>8.1f}% {r['ml']:>9.2f}")
|
||||
print("\n `2lati` = strutture in cui ENTRAMBE le gambe hanno bid E ask. Una riga presente non")
|
||||
print(" e' un dato presente: una gamba con un solo lato non e' negoziabile a un prezzo noto.")
|
||||
print(" `d corta`/`d lunga` = delta REALIZZATI (non i target): se la griglia degli strike e'")
|
||||
print(" grossolana la struttura che compreresti non e' quella che hai chiesto.")
|
||||
print(" `costo/cred` = quanto dell'incasso a mid resta sul tavolo attraversando lo spread su")
|
||||
print(" ENTRAMBE le gambe. Riferimento del progetto su BTC/ETH inverse: ~10% del credito.")
|
||||
print(" `fee/cred` = listino taker Deribit, 2 gambe x apertura+chiusura, col cap 12.5%.")
|
||||
print(" `credito$` e `maxloss$` sono per UN LOTTO MINIMO.")
|
||||
return res
|
||||
|
||||
|
||||
def profondita(rows: list[dict], fams: list[str], b: Budget) -> None:
|
||||
print("\n" + "=" * 104)
|
||||
print("2b. PROFONDITA' IN CIMA AL BOOK (public/get_order_book depth=5), sulle gambe scelte")
|
||||
print("=" * 104)
|
||||
print(f"{'famiglia':<10} {'gamba':<6} {'n':>3} {'lotti al best':>14} {'$ premio al best':>17} "
|
||||
f"{'ask/tick lunga':>15} {'max|delta venue-BS|':>21}")
|
||||
for fam in fams:
|
||||
gg = gambe(rows, fam)[:4] # max 4 scadenze/famiglia: budget di rete
|
||||
for lato, key in (("corta", "short"), ("lunga", "long")):
|
||||
lots, prem, dd, tk = [], [], [], []
|
||||
for g in gg:
|
||||
r = g[key]
|
||||
ob = get("get_order_book", {"instrument_name": r["name"], "depth": 5}, b)
|
||||
if not ob:
|
||||
continue
|
||||
side = ob.get("bids") if key == "short" else ob.get("asks")
|
||||
if not side:
|
||||
continue
|
||||
px, sz = float(side[0][0]), float(side[0][1])
|
||||
lots.append(sz / r["minamt"] if r["minamt"] else float("nan"))
|
||||
prem.append(prem_usd(px, r) * (sz / max(r["minamt"], 1e-12)))
|
||||
if r["tick"]:
|
||||
tk.append(px / r["tick"])
|
||||
gv = (ob.get("greeks") or {}).get("delta")
|
||||
if gv is not None and np.isfinite(r["delta_bs"]):
|
||||
dd.append(abs(float(gv) - r["delta_bs"]))
|
||||
if not lots:
|
||||
print(f"{fam:<10} {lato:<6} {'0':>3} book vuoto su tutte le scadenze provate")
|
||||
continue
|
||||
print(f"{fam:<10} {lato:<6} {len(lots):>3} {np.median(lots):>14.1f} "
|
||||
f"{np.median(prem):>17.2f} "
|
||||
f"{(np.median(tk) if tk else float('nan')):>15.1f} "
|
||||
f"{(max(dd) if dd else float('nan')):>21.4f}")
|
||||
print("\n `lotti al best` = size in cima al book / lotto minimo: quante strutture minime")
|
||||
print(" entrano senza muovere il prezzo. `ask/tick` sulla gamba LUNGA e' il controllo")
|
||||
print(" d'artefatto del 30/07: un'ala a 1-2 tick e' un prezzo di griglia, non un prezzo.")
|
||||
print(" `max|delta venue-BS|` valida il MIO calcolo del delta contro le greche del venue: se")
|
||||
print(" non e' piccolo, tutta la selezione degli strike qui sopra e' sbagliata.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 3. collaterale
|
||||
|
||||
|
||||
def collaterale(rows: list[dict], res: dict) -> None:
|
||||
print("\n" + "=" * 104)
|
||||
print("3. IL COLLATERALE — quanto conto serve per UNA struttura minima (conto reale $635)")
|
||||
print("=" * 104)
|
||||
print(f"{'famiglia':<10} {'IM corta+ala $':>15} {'max-loss $':>11} {'IM/conto':>9} "
|
||||
f"{'strutture @635 (IM)':>20} {'(max-loss)':>12}")
|
||||
for fam in res:
|
||||
gg = gambe(rows, fam)
|
||||
ims, mls = [], []
|
||||
for g in gg:
|
||||
s, lo = g["short"], g["long"]
|
||||
if s["mid"] is None or lo["ask"] is None:
|
||||
continue
|
||||
qty = s["csize"] * s["minamt"]
|
||||
im = im_short_put_usd(s["S"], s["K"], prem_usd(s["mark"], s), qty)
|
||||
ims.append(im + prem_usd(lo["ask"], lo)) # l'ala si PAGA: e' cassa, non margine
|
||||
mls.append(loss_usd(s["K"], lo["K"], s))
|
||||
if not ims:
|
||||
continue
|
||||
im, ml = float(np.median(ims)), float(np.median(mls))
|
||||
print(f"{fam:<10} {im:>15.2f} {ml:>11.2f} {im/CAPITAL:>8.1%} "
|
||||
f"{CAPITAL/im:>20.1f} {CAPITAL/ml:>12.1f}")
|
||||
print("\n `IM corta+ala` = margine iniziale della sola put venduta (formula PUBBLICATA dal")
|
||||
print(" venue, FONTE SECONDARIA) + il premio dell'ala, che si paga in contanti. E' il")
|
||||
print(" fabbisogno se il venue NON netta le gambe.")
|
||||
print(" `max-loss` = perdita massima strutturale = il fabbisogno se il venue LE NETTA.")
|
||||
print(" ⚠️ La differenza fra i due numeri e' l'unica cosa importante di questa tabella e NON")
|
||||
print(" E' LEGGIBILE dall'API pubblica: si chiude con `private/get_margins` (lettura, ma")
|
||||
print(" autenticata -> fuori dal perimetro dichiarato di questo file).")
|
||||
|
||||
|
||||
def costo_raccolta(tab: dict) -> None:
|
||||
print("\n" + "=" * 104)
|
||||
print("4. COSTO DI RACCOLTA — cosa costerebbe aggiungere una famiglia al giro esistente")
|
||||
print("=" * 104)
|
||||
print(" `collect_chain.py` oggi: ~650 chiamate in ~160s a 4 rps (BTC+ETH inverse), al :25.")
|
||||
print(f"{'famiglia':<10} {'chiamate/giro':>14} {'+s a 4 rps':>11} {'+% sul giro':>12}")
|
||||
for f in LINEAR:
|
||||
t = tab.get(f)
|
||||
if t:
|
||||
n = t["nliq"] + 2 # + get_instruments + get_book_summary
|
||||
print(f"{f:<10} {n:>14.0f} {n/4.0:>11.1f} {n/650*100:>11.0f}%")
|
||||
print("\n ⚠️ Il vincolo non e' la CPU ma il rate limit Deribit PER-IP, gia' costato un guasto")
|
||||
print(" il 29/07 (fallback silenzioso del feed 5m di SKH01). Ogni famiglia aggiunta allunga")
|
||||
print(" la finestra in cui il giro occupa l'IP.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- main
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--cache", action="store_true", help="riusa lo snapshot su disco (niente rete)")
|
||||
a = ap.parse_args()
|
||||
|
||||
print("=" * 104)
|
||||
print("ALT-OPT — opzioni USDC-LINEARI su Deribit: c'e' una struttura eseguibile a $635?")
|
||||
print("=" * 104)
|
||||
print("Sola lettura pubblica, nessun ordine. UNO SNAPSHOT: qui non esiste un backtest.")
|
||||
|
||||
b = Budget()
|
||||
if a.cache and CACHE.exists():
|
||||
raw = json.loads(CACHE.read_text())
|
||||
print(f"\n[cache] {CACHE}")
|
||||
else:
|
||||
guardia_finestra()
|
||||
t0 = time.time()
|
||||
raw = fetch_all(b)
|
||||
CACHE.parent.mkdir(parents=True, exist_ok=True)
|
||||
CACHE.write_text(json.dumps(raw))
|
||||
print(f"\n[rete] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429, "
|
||||
f"{time.time()-t0:.0f}s")
|
||||
|
||||
rows = build(raw)
|
||||
print(f"[dato] {len(rows)} strumenti con specifiche E quote — snapshot "
|
||||
f"{datetime.now(UTC):%Y-%m-%d %H:%M} UTC")
|
||||
if not rows:
|
||||
print("NESSUN DATO: il venue non ha risposto. Verdetto: non misurabile.")
|
||||
return 1
|
||||
|
||||
tab = famiglie(rows)
|
||||
replica(tab)
|
||||
res = liquidita(rows)
|
||||
|
||||
vive = [f for f in res if res[f]["due"] >= 2]
|
||||
if a.cache:
|
||||
print("\n2b. PROFONDITA': non girata (--cache non usa la rete).")
|
||||
elif not vive:
|
||||
print("\n2b. PROFONDITA': non girata — nessuna famiglia con >=2 strutture a due lati.")
|
||||
else:
|
||||
guardia_finestra()
|
||||
profondita(rows, vive, b)
|
||||
|
||||
collaterale(rows, res)
|
||||
costo_raccolta(tab)
|
||||
|
||||
print("\n" + "=" * 104)
|
||||
print("5. COSA QUESTO FILE NON PUO' DIRE (dichiarato prima di qualunque verdetto)")
|
||||
print("=" * 104)
|
||||
print(" * Non c'e' STORIA: il collettore raccoglie BTC/ETH inverse, l'archivio ereditato pure.")
|
||||
print(" Zero giorni di catena su SOL/XRP/HYPE/AVAX/*_USDC -> nessun backtest, nessun")
|
||||
print(" hold-out, nessun deflated-Sharpe. Il massimo verdetto ottenibile e' LEAD.")
|
||||
print(" * Non c'e' IV-RANK: il gate>0.30 e' l'unico alpha misurato di VRP01 e richiede la")
|
||||
print(" distribuzione storica della vol implicita del sottostante. Senza, la struttura non")
|
||||
print(" e' VRP01: e' vendere vol a caso. (Su BTC/ETH quel gate e' passato 0/19 settimane.)")
|
||||
print(" * Non c'e' STRESS nel campione: la regola 'niente short-vol da modello in deploy' e'")
|
||||
print(" del 19/06 e la condizione dichiarata per rivalutarla e' un crash CATTURATO.")
|
||||
print(" * Il margine e' da FORMULA PUBBLICATA, non dall'API: il netting delle gambe non e'")
|
||||
print(" leggibile pubblicamente.")
|
||||
print(" * E' UN ISTANTE: uno spread relativo mediano misurato a un'ora del sabato non e' la")
|
||||
print(" sua distribuzione. Servirebbe almeno un ciclo settimanale per sapere se e' tipico.")
|
||||
print(f"[rete, totale] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -283,24 +283,29 @@ def certify(asset: str, wide: pd.DataFrame, vwide: pd.DataFrame, meta: pd.DataFr
|
||||
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)
|
||||
# (b) volume orario per bucket (il flat da solo non basta).
|
||||
# ATTENZIONE: il `volume` di get_tradingview_chart_data e' in VALUTA BASE (BTC/ETH),
|
||||
# non in USD -> va moltiplicato per il prezzo, o si sbaglia di ~5 ordini di grandezza.
|
||||
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
|
||||
v = vwide[nm].dropna()
|
||||
px = wide[nm].reindex(v.index)
|
||||
usd = (v * px).dropna() # <- conversione in USD
|
||||
dte = (exp.iloc[0] - usd.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())))
|
||||
mm = (dte >= lo) & (dte < hi)
|
||||
if mm.sum() > 50:
|
||||
vrows.append(dict(bucket=lab, med=float(usd[mm].median()),
|
||||
zero=float((usd[mm] == 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:")
|
||||
print(" volume orario mediano in USD (volume base x prezzo) 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}%")
|
||||
print(f" {b:9s} mediano=${vb.loc[b,'med']:>12,.0f}/ora ore a vol 0 = {vb.loc[b,'zero']*100:5.1f}%")
|
||||
|
||||
# (c) accordo con l'INDICE alla scadenza: il datato DEVE convergere
|
||||
conv = []
|
||||
@@ -385,34 +390,36 @@ def curve_frame(asset: str, wide: pd.DataFrame, meta: pd.DataFrame,
|
||||
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)
|
||||
F = fund.set_index("ts").reindex(idx).ffill(limit=2)
|
||||
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}
|
||||
# front = il primo (per scadenza) ancora vivo con dte >= roll_dte; back = il successivo
|
||||
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
|
||||
order = [c for c in ok.contract if c in wide.columns] # ordine di SCADENZA
|
||||
if len(order) < 2:
|
||||
raise RuntimeError("meno di due contratti utilizzabili")
|
||||
|
||||
# ALIVE[i, k] = il k-esimo contratto (per scadenza) e' quotato a i E ha dte >= roll
|
||||
TAU = np.column_stack([(exps[c] - idx).total_seconds().values / 3600.0 for c in order])
|
||||
AV = np.column_stack([wide[c].notna().values for c in order])
|
||||
ALIVE = AV & (TAU >= roll_dte * 24)
|
||||
|
||||
any1 = ALIVE.any(axis=1)
|
||||
k1 = np.argmax(ALIVE, axis=1) # primo vivo = FRONT
|
||||
A2 = ALIVE.copy()
|
||||
A2[np.arange(n), k1] = False
|
||||
any2 = A2.any(axis=1)
|
||||
k2 = np.argmax(A2, axis=1) # secondo vivo = BACK
|
||||
m = any1 & any2
|
||||
|
||||
front = np.where(m, np.array(order, dtype=object)[k1], "")
|
||||
back = np.where(m, np.array(order, dtype=object)[k2], "")
|
||||
rows = np.arange(n)
|
||||
Wv = wide[order].values.astype(float)
|
||||
Ff = np.where(m, Wv[rows, k1], np.nan)
|
||||
Fb = np.where(m, Wv[rows, k2], np.nan)
|
||||
tf = np.where(m, TAU[rows, k1], np.nan)
|
||||
tb = np.where(m, TAU[rows, k2], np.nan)
|
||||
|
||||
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
|
||||
@@ -514,7 +521,8 @@ def signal_w(cv: pd.DataFrame, family: str, sig: str, win_h: int, thr: float) ->
|
||||
|
||||
def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str,
|
||||
win_h: int, thr: float, slip_bps: float,
|
||||
fee_side: float = FEE_SIDE, fi=None, bi=None, lag: int = 1) -> pd.Series:
|
||||
fee_side: float = FEE_SIDE, fi=None, bi=None, lag: int = 1,
|
||||
dec_hour: int | None = None) -> 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
|
||||
@@ -529,6 +537,11 @@ def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str,
|
||||
if fi is None or bi is None:
|
||||
fi, bi = pair_idx(cv, ctx)
|
||||
w = signal_w(cv, family, sig, win_h, thr)
|
||||
if dec_hour is not None:
|
||||
# ANCORA: la posizione si aggiorna SOLO a quell'ora del giorno e si TIENE il resto
|
||||
# (non "si guadagna solo a quell'ora", che sarebbe un'altra strategia).
|
||||
ws = pd.Series(w, index=idx).where(idx.hour == dec_hour)
|
||||
w = ws.ffill().fillna(0.0).values
|
||||
|
||||
P = np.zeros((n, ncol))
|
||||
rows = np.arange(n)
|
||||
@@ -564,6 +577,12 @@ def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str,
|
||||
# ==========================================================================
|
||||
# 4. METRICHE / GATE
|
||||
# ==========================================================================
|
||||
def combine(hs: list[pd.Series]) -> pd.Series:
|
||||
"""50/50 BTC+ETH sulle ore COMUNI (inner join, come tp01_baseline_daily)."""
|
||||
J = pd.concat(hs, axis=1, join="inner").fillna(0.0)
|
||||
return J.mean(axis=1)
|
||||
|
||||
|
||||
def to_daily(h: pd.Series) -> pd.Series:
|
||||
return ((1.0 + h.fillna(0.0)).resample("1D").prod() - 1.0).dropna()
|
||||
|
||||
@@ -698,9 +717,7 @@ def main() -> None:
|
||||
h = run_strategy(cvcache[(a, rl)], ctxs[a], fam, sig, win, th, slip_est,
|
||||
fi=f_i, bi=b_i)
|
||||
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)
|
||||
d = to_daily(combine(hs))
|
||||
st = stats(d)
|
||||
rows.append(dict(family=fam, sig=sig, win=win, roll=rl, thr=th,
|
||||
daily=d, **st))
|
||||
@@ -768,13 +785,10 @@ def main() -> None:
|
||||
hs = []
|
||||
for a in ASSETS:
|
||||
f_i, b_i = pidx[(a, int(b.roll))]
|
||||
h = run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, b.sig,
|
||||
int(b.win), float(b.thr), slip_est, fi=f_i, bi=b_i)
|
||||
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))
|
||||
hs.append(run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, b.sig,
|
||||
int(b.win), float(b.thr), slip_est,
|
||||
fi=f_i, bi=b_i, dec_hour=off))
|
||||
return to_daily(combine(hs))
|
||||
try:
|
||||
ab = A.anchor_luck_band(by_off, list(range(24)), canonical=0)
|
||||
if "median" in ab:
|
||||
@@ -807,7 +821,7 @@ def main() -> None:
|
||||
f_i, b_i = pidx[(a, int(b.roll))]
|
||||
hs.append(run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, b.sig,
|
||||
int(b.win), float(b.thr), slip_est, fi=f_i, bi=b_i, lag=lg))
|
||||
sl_ = stats(to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1)))
|
||||
sl_ = stats(to_daily(combine(hs)))
|
||||
lag_tab.append((lg, sl_["sharpe"], sl_["cagr"]))
|
||||
print(f" ritardo {lg:2d}h -> Sharpe {sl_['sharpe']:+6.2f} CAGR {sl_['cagr']*100:+6.2f}%")
|
||||
s1 = lag_tab[0][1]; s2 = lag_tab[1][1]
|
||||
@@ -827,7 +841,7 @@ def main() -> None:
|
||||
f_i, b_i = pidx[(a, int(b.roll))]
|
||||
hs.append(run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family,
|
||||
b.sig, int(b.win), float(b.thr), sl, fi=f_i, bi=b_i))
|
||||
s2 = stats(to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1)))
|
||||
s2 = stats(to_daily(combine(hs)))
|
||||
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}")
|
||||
|
||||
@@ -54,6 +54,7 @@ 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, 1500.0, 2000.0, 3000.0, 4000.0, 5000.0, 7500.0,
|
||||
10000.0, 20000.0, 50000.0)
|
||||
CAPITALI_XS = (100.0, 200.0, 300.0, 600.0, 1000.0, 2000.0, 3000.0, 5000.0, 10000.0)
|
||||
TICKETS = (50.0, 100.0, 300.0)
|
||||
ANN = np.sqrt(365.0)
|
||||
|
||||
@@ -257,6 +258,7 @@ def sim_libro(Weff: np.ndarray, R: np.ndarray, r_hedge: np.ndarray | None, cap0:
|
||||
eq = np.empty(n)
|
||||
nets = np.zeros(n)
|
||||
n_fill = n_skip = n_lot0 = 0
|
||||
ntl_req = ntl_done = 0.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]))
|
||||
@@ -275,6 +277,8 @@ def sim_libro(Weff: np.ndarray, R: np.ndarray, r_hedge: np.ndarray | None, cap0:
|
||||
n_lot0 += int(zero.sum())
|
||||
move = move & (lots >= 1)
|
||||
need = np.abs(w_t - w) > 1e-12 # una gamba gia' a posto NON e' un ordine saltato
|
||||
ntl_req += float((np.abs(w_t - w) * cap).sum())
|
||||
ntl_done += float((np.abs(w_t - w) * cap)[move].sum())
|
||||
w_new = np.where(move, w_t, w)
|
||||
n_fill += int((move & need).sum())
|
||||
n_skip += int(((~move) & need).sum())
|
||||
@@ -290,7 +294,8 @@ def sim_libro(Weff: np.ndarray, R: np.ndarray, r_hedge: np.ndarray | None, cap0:
|
||||
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)
|
||||
pct_eseguite=(100.0 * n_fill / tot) if tot else np.nan,
|
||||
pct_nozionale=(100.0 * ntl_done / ntl_req) if ntl_req > 0 else np.nan)
|
||||
|
||||
|
||||
def metriche(net: np.ndarray, idx: pd.DatetimeIndex) -> dict:
|
||||
@@ -393,6 +398,19 @@ def main() -> None:
|
||||
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)}")
|
||||
mv = [float(np.abs(pd.read_parquet(RAW / f"hl_{sy.lower()}_1d.parquet", columns=["close"])
|
||||
["close"].pct_change().dropna()).median()) * 100 for sy in ck.index]
|
||||
print(f" movimento |giornaliero| tipico di questi asset: {float(np.median(mv)):.2f}% -> "
|
||||
f"una deviazione mediana del {ck['dev_pct'].abs().median():.2f}% su un feed vecchio "
|
||||
f"{ck['eta_h'].median():.0f}h e' cio' che ci si attende (non e' un disaccordo di venue).")
|
||||
try:
|
||||
from src.data.downloader import load_data as _ld
|
||||
bd = float(_ld("BTC", "1h")["close"].iloc[-1])
|
||||
hl = float(meta.loc["BTC", "markPx"])
|
||||
print(f" cross-check su un TERZO venue: BTC Deribit certificato ${bd:,.0f} vs "
|
||||
f"HL API ${hl:,.0f} = {100*(hl-bd)/bd:+.2f}%")
|
||||
except Exception as e:
|
||||
print(f" (cross-check Deribit non disponibile: {type(e).__name__})")
|
||||
print(" -> un feed TESTNET non supera questo controllo: i suoi prezzi sono scollegati.")
|
||||
|
||||
tk = fee_sched
|
||||
@@ -497,8 +515,7 @@ def main() -> None:
|
||||
f"{'>=$5 [D]':>10}{'>=$10 [C]':>11}{'reb>=$10':>10}")
|
||||
R_xs = dret
|
||||
idx = C.index
|
||||
base = None
|
||||
for cap in CAPITALI:
|
||||
for cap in CAPITALI_XS:
|
||||
pct5 = 100.0 * float((tk_nz * cap >= MIN_ORDER_ASSUNTO).mean())
|
||||
pct10 = 100.0 * float((tk_nz * cap >= MIN_ORDER_HL).mean())
|
||||
pr10 = 100.0 * float((reb * cap >= MIN_ORDER_HL).mean())
|
||||
@@ -513,15 +530,16 @@ def main() -> None:
|
||||
print(f" (`turn=|dW|` prima dello scaling): e' il motivo per cui sta sotto il "
|
||||
f"{m_off['sharpe']:.2f} pubblicato.")
|
||||
print(f" {'cap XS01':>10}{'Sh mod':>9}{'Sh $5 [D]':>11}{'Sh $10 [C]':>12}"
|
||||
f"{'haircut $10':>13}{'ordini fatti':>14}")
|
||||
for cap in CAPITALI:
|
||||
f"{'haircut $10':>13}{'eseguito':>21}")
|
||||
for cap in CAPITALI_XS:
|
||||
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}%")
|
||||
f"{hc:>12.0f}%{m10['pct_eseguite']:>8.0f}% ord "
|
||||
f"{m10['pct_nozionale']:>4.0f}% ntl")
|
||||
|
||||
# ---------------------------------------------------------------- 4. XSR01
|
||||
print(f"\n[4] XSR01 — soglia PUBBLICATA ~$5.000, gate 2026-10-23 con haircut a $5.000 <= 40%")
|
||||
@@ -555,7 +573,7 @@ def main() -> None:
|
||||
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}{'ordini fatti':>14}")
|
||||
f"{'haircut $5':>12}{'haircut $10':>13}{'eseguito':>21}")
|
||||
hc10_5000 = np.nan
|
||||
for cap in CAPITALI:
|
||||
m0 = sim_libro(Wx, Rx, rbx, cap, None, None, None, FEE_LEG_MODELLO)
|
||||
@@ -568,9 +586,22 @@ def main() -> None:
|
||||
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}%")
|
||||
f"{m10['pct_eseguite']:>8.0f}% ord {m10['pct_nozionale']:>4.0f}% ntl")
|
||||
print(f" -> haircut a $5.000 col pavimento VERO ($10): {hc10_5000:.0f}% "
|
||||
f"(soglia pre-registrata {XSR_SOGLIA_HAIRCUT*100:.0f}%)")
|
||||
print(f"\n la colonna haircut NON e' monotona (+35% a $2k, -6% a $4k): a un capitale solo e'")
|
||||
print(f" una statistica RUMOROSA. Banda: haircut ricalcolato a pavimento $8/$10/$12 ->")
|
||||
print(f" {'cap':>9}{'h($8)':>9}{'h($10)':>9}{'h($12)':>9}{'ampiezza':>11} giudizio")
|
||||
for cap in CAPITALI:
|
||||
s0 = metriche(sim_libro(Wx, Rx, rbx, cap, None, None, None, FEE_LEG_MODELLO)["net"], idxx)["sharpe"]
|
||||
hs = []
|
||||
for mo in (8.0, 10.0, 12.0):
|
||||
sh = metriche(sim_libro(Wx, Rx, rbx, cap, mo, lot_x, px_x, FEE_LEG_MODELLO)["net"], idxx)["sharpe"]
|
||||
hs.append((s0 - sh) / abs(s0) * 100)
|
||||
amp = max(hs) - min(hs)
|
||||
g = ("ROTTO" if min(hs) > XSR_SOGLIA_HAIRCUT * 100 else
|
||||
"ok" if max(hs) < XSR_SOGLIA_HAIRCUT * 100 else "INDECIDIBILE")
|
||||
print(f" {cap:>9,.0f}{hs[0]:>8.0f}%{hs[1]:>8.0f}%{hs[2]:>8.0f}%{amp:>10.0f}pp {g}")
|
||||
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.")
|
||||
|
||||
@@ -596,9 +627,41 @@ def main() -> None:
|
||||
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(f"\n [B] SENSIBILITA' AL PAVIMENTO a $5.000 (l'haircut a un punto solo e' rumore:")
|
||||
print(f" qui si vede DOVE il parametro comincia a mordere)")
|
||||
print(f" {'pavimento $':>13}{'Sharpe':>9}{'haircut':>10}{'ord fatti':>11}{'ntl mosso':>11}")
|
||||
sh0 = metriche(sim_libro(Wx, Rx, rbx, 5000.0, None, None, None, FEE_LEG_MODELLO)["net"], idxx)["sharpe"]
|
||||
for mo in (0.0, 5.0, 10.0, 15.0, 20.0, 30.0, 50.0):
|
||||
m = sim_libro(Wx, Rx, rbx, 5000.0, mo if mo > 0 else None, None, None, FEE_LEG_MODELLO)
|
||||
aa = metriche(m["net"], idxx)
|
||||
print(f" {mo:>13.0f}{aa['sharpe']:>9.2f}"
|
||||
f"{(sh0-aa['sharpe'])/abs(sh0)*100:>9.0f}%"
|
||||
f"{m['pct_eseguite'] if np.isfinite(m['pct_eseguite']) else 100:>10.0f}%"
|
||||
f"{m['pct_nozionale'] if np.isfinite(m['pct_nozionale']) else 100:>10.0f}%")
|
||||
|
||||
print(f"\n [B] BREAK-EVEN DEL COSTO — quanto dovrebbe valere lo slippage perche' il "
|
||||
f"'rischio #1' fosse reale:")
|
||||
print(f" {'costo/gamba':>13}{'= taker + slip':>18}{'Sharpe':>9}{'CAGR':>8}")
|
||||
be = None
|
||||
for bps in (4.5, 5.0, 7.5, 10.0, 15.0, 20.0, 30.0, 40.0):
|
||||
aa = metriche(sim_libro(Wx, Rx, rbx, 5000.0, None, None, None, bps / 1e4)["net"], idxx)
|
||||
if be is None and aa["sharpe"] < 1.0:
|
||||
be = bps
|
||||
print(f" {bps:>11.1f} bps{max(bps-4.5,0):>13.1f} slip{aa['sharpe']:>9.2f}"
|
||||
f"{aa['cagr']:>7.1f}%")
|
||||
slip_mis = float(np.median(fee_real) - taker) * 1e4
|
||||
print(f" -> Sharpe scende sotto 1.0 a ~{be:.0f} bps/gamba = ~{be-4.5:.0f} bps di slippage,")
|
||||
print(f" cioe' **{(be-4.5)/max(slip_mis,1e-9):.0f}x** il mezzo spread misurato oggi "
|
||||
f"({slip_mis:.1f} bps).")
|
||||
|
||||
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.")
|
||||
tt = sorted(b["time"] for v in books.values() for b in v)
|
||||
print(f" LIMITI DICHIARATI (1) i {a.snaps} snapshot coprono {(tt[-1]-tt[0])/1000:.0f} SECONDI:")
|
||||
print(" NON sono osservazioni indipendenti, sono UNA fotografia intraday di oggi. E la")
|
||||
print(" liquidita' di HL e' cresciuta -> e' la stima piu' FAVOREVOLE per i 2.6 anni di")
|
||||
print(" backtest. Cio' che regge al limite e' il MARGINE (21x), non il livello.")
|
||||
print(" (2) il lotto in $ usa il mark di OGGI su 2.6 anni di storia: irrilevante perche' il")
|
||||
print(" lotto non e' mai vincolante (max $1.69 contro $10 di pavimento), ma va detto.")
|
||||
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)
|
||||
|
||||
@@ -302,11 +302,39 @@ def main() -> dict:
|
||||
|
||||
print("\n IL TEST DI SUPER-PROPORZIONALITA' (la domanda esatta del brief):")
|
||||
dpk = [delta_p[j] / KS[j] for j in range(len(KS))]
|
||||
rap = [dd_acc[j] / dd_close[j] for j in range(len(KS))]
|
||||
j1 = KS.index(1.00)
|
||||
print(f" Δ/k a k=0.50 {dpk[0]:+.3%} · 1.00 {dpk[j1]:+.3%} · 1.50 {dpk[KS.index(1.50)]:+.3%}"
|
||||
print(" Δ per UNITA' di leva (se il minimo intraday amplificasse piu' che")
|
||||
print(" proporzionalmente, questa riga CRESCEREBBE in modulo con k):")
|
||||
print(f" Δ/k k=0.50 {dpk[0]:+.3%} · 1.00 {dpk[j1]:+.3%} · 1.50 {dpk[KS.index(1.50)]:+.3%}"
|
||||
f" · 2.00 {dpk[KS.index(2.00)]:+.3%} · 5.00 {dpk[-1]:+.3%}")
|
||||
cresce = dpk[KS.index(2.00)] < dpk[j1] # piu' negativo = piu' costoso per unita' di leva
|
||||
print(f" -> Δ/k {'CRESCE in modulo con k = super-proporzionale' if cresce else 'NON cresce in modulo: l effetto e circa PROPORZIONALE'}.")
|
||||
print(f" rapporto maxDD acc./close: {' · '.join(f'{r:.3f}' for r in rap)}")
|
||||
super_prop = dpk[KS.index(2.00)] > dpk[j1] * 1.05
|
||||
sotto = dpk[KS.index(2.00)] < dpk[j1] * 0.95
|
||||
verdetto = ("SUPER-proporzionale" if super_prop else
|
||||
("SOTTO-proporzionale" if sotto else "proporzionale entro il rumore"))
|
||||
print(f"\n ❌ RISPOSTA: NO — Δ/k **CALA** in modulo (da {dpk[0]:.3%} a {dpk[-1]:.3%}) e il")
|
||||
print(f" RAPPORTO fra le due maxDD e' PIATTO ({min(rap):.3f}-{max(rap):.3f}) su tutta la")
|
||||
print(f" griglia. La lente accoppiata e' un RICARICO MOLTIPLICATIVO COSTANTE di ~"
|
||||
f"{(np.mean(rap)-1)*100:.1f}%")
|
||||
print(f" sul maxDD, non un'amplificazione: verdetto **{verdetto}**.")
|
||||
print(" Meccanismo: il maxDD di 25 anni e' una statistica MULTI-GIORNO (una discesa di")
|
||||
print(" settimane), il wick e' un fatto di UN giorno. Aggiungere qualche punto di tuffo")
|
||||
print(" a una discesa gia' lunga sposta poco, e la parte della distribuzione che")
|
||||
print(" conterebbe non c'e': nel 26,8% dei giorni il libro CHIUDE sul minimo (gap = 0)")
|
||||
print(" e quei giorni sono proprio i brutti. A leva alta Δ/k cala anche perche' i")
|
||||
print(" drawdown si allungano piu' in fretta di quanto il singolo tuffo cresca.")
|
||||
|
||||
print("\n ⚠️ MA C'E' UN'ECCEZIONE, ed e' quella che decide la frontiera: la PROBABILITA'")
|
||||
print(" di sfondare una SOGLIA FISSA e' amplificata assai piu' del maxDD stesso,")
|
||||
print(" perche' vicino alla soglia la densita' e' fitta.")
|
||||
print(f"\n {'k':>6}{'P(DD>30%) close':>17}{'P(DD>30%) acc.':>16}{'amplificazione':>16}")
|
||||
for j, k in enumerate(KS):
|
||||
pc = float((s0["dd"][:, j, 0] > DD_SOGLIA).mean())
|
||||
pa = float((s0["dd"][:, j, 1] > DD_SOGLIA).mean())
|
||||
amp = (pa / pc) if pc > 0 else float("nan")
|
||||
print(f" {k:>6.2f}{pc:>17.1%}{pa:>16.1%}"
|
||||
f"{(f'{amp:.2f}x' if np.isfinite(amp) else 'n.d.'):>16}")
|
||||
|
||||
print("\n IL NUMERO CHE DECIDE — L'HAIRCUT DI LEVA IMPLICITO NELLA LENTE:")
|
||||
print(" 'a quale k close-only corrisponde il rischio di DD che la lente accoppiata")
|
||||
@@ -397,6 +425,44 @@ def main() -> dict:
|
||||
print(f" <= {frac:.0%} dell'equity -> costo massimo {sl*frac:.0%} dell'equity per asset a k=1,")
|
||||
print(f" cioe' {sl*frac*2:.0%} se scattano entrambi; diventa il -100% a k = {1/(sl*frac*2):.1f}x.")
|
||||
|
||||
# ------------------------------------------------------------------ (3-bis)
|
||||
sezione("(3-bis) DOVE L'ACCOPPIAMENTO MORDE DAVVERO — la regola a UN GIORNO, sui giorni STORICI")
|
||||
print("\n Il finding del 25/07 ('close-only e' CIECA', 'l'indipendente RADDOPPIA i breach')")
|
||||
print(" e' misurato su una regola di DAILY-LOSS: una soglia valutata sul MINIMO di UN")
|
||||
print(" giorno. Il maxDD di un piano d'accumulo e' un'altra grandezza. Qui le due si")
|
||||
print(" guardano fianco a fianco, sui GIORNI VERI (nessun bootstrap): quante volte in")
|
||||
print(f" {len(D['R_del'])/365:.1f} anni una soglia di perdita giornaliera viene sfondata.")
|
||||
Rh, Gh = D["R_del"], gap
|
||||
rng2 = np.random.default_rng(909)
|
||||
Gsh = Gh.copy(); rng2.shuffle(Gsh)
|
||||
anni_st = len(Rh) / 365.0
|
||||
for T in (0.03, 0.05, 0.10):
|
||||
print(f"\n soglia daily-loss {T:.0%} (breach/anno)")
|
||||
print(f" {'k':>6}{'close-only':>13}{'accoppiata':>13}{'indip. (finto)':>17}"
|
||||
f"{'acc./close':>12}{'indip./acc.':>13}")
|
||||
for k in (1.00, 1.25, 1.50, 2.00, 3.00):
|
||||
nc = float((Rh * k <= -T).sum()) / anni_st
|
||||
na = float(((Rh + Gh) * k <= -T).sum()) / anni_st
|
||||
ni = float(((Rh + Gsh) * k <= -T).sum()) / anni_st
|
||||
r1 = (na / nc) if nc > 0 else float("inf")
|
||||
r2 = (ni / na) if na > 0 else float("nan")
|
||||
print(f" {k:>6.2f}{nc:>13.2f}{na:>13.2f}{ni:>17.2f}"
|
||||
f"{(f'{r1:.2f}x' if np.isfinite(r1) else 'INF'):>12}"
|
||||
f"{(f'{r2:.2f}x' if np.isfinite(r2) else 'n.d.'):>13}")
|
||||
print("\n 📌 IL RISULTATO TRASFERIBILE DEL FILONE: la lezione del 25/07 e' VERA e NON SI")
|
||||
print(" TRASFERISCE. Su una regola a UN GIORNO l'accoppiamento cambia il conto di un")
|
||||
print(" fattore (e close-only puo' dare ZERO dove il vero e' positivo: cecita' esatta);")
|
||||
print(" su una statistica MULTI-GIORNO come il maxDD di un piano d'accumulo lo stesso")
|
||||
print(" difetto vale un ricarico costante di pochi punti percentuali. Il gate")
|
||||
print(" pre-registrato ha importato una lezione dal contesto in cui e' stata misurata")
|
||||
print(" (conti FUNDED, barriera di daily-loss) a un contesto in cui la grandezza")
|
||||
print(" decisiva e' un'altra — stessa forma dell'errore che il progetto ha gia'")
|
||||
print(" codificato il 26/07 sulla barra parziale (SKH01 +0.38 / TP01 ±0.03: 'la")
|
||||
print(" conclusione non si trasferisce fra sleeve').")
|
||||
print(" ⚠️ Il che NON assolve close-only in generale: sul canale PROP/FUNDED, dove il")
|
||||
print(" vincolo binding e' esattamente una regola di daily-loss per-conto, la lente")
|
||||
print(" accoppiata resta obbligatoria e la leva li' e' un'altra decisione.")
|
||||
|
||||
# ------------------------------------------------------------------ (4)
|
||||
sezione("(4) IL GRADINO ESEGUIBILE — sopravvive alla lente? (€500/mese, muro netto)")
|
||||
s500 = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=500, bersaglio=BERSAGLIO, seed=SEED)
|
||||
@@ -443,10 +509,10 @@ def attacco2() -> None:
|
||||
f" <- lo scetticato dichiara 12,14%")
|
||||
print(f" -> VERIFICATO. E il numero non e' un artefatto della sua ricostruzione.")
|
||||
|
||||
print("\n ⚠️ MA IL MECCANISMO CHE DICHIARA E' INCOMPLETO, e la parte che manca e' la")
|
||||
print(" maggiore. Lo scetticato scrive: «il target vale sulla posizione quando c'e', e")
|
||||
print(" TP01 e' long-flat». Decomposizione:")
|
||||
convs, vols_pred, vols_real, sers = [], [], [], {}
|
||||
print("\n ⚠️ MA IL MECCANISMO CHE DICHIARA SPIEGA CIRCA META' DEL DIVARIO, e la meta'")
|
||||
print(" che manca porta a una conclusione operativa diversa. Lo scetticato scrive:")
|
||||
print(" «il target vale sulla posizione quando c'e', e TP01 e' long-flat». Misura:")
|
||||
convs, vols_pred, vols_real, sers, flats = [], [], [], {}, []
|
||||
for a in ("BTC", "ETH"):
|
||||
df = resample_1d(load_data(a, "1h"))
|
||||
c = df["close"].values.astype(float)
|
||||
@@ -461,6 +527,7 @@ def attacco2() -> None:
|
||||
sers[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"]))
|
||||
ok = np.isfinite(d) & np.isfinite(vol) & (vol > 0)
|
||||
convs.append(float(np.sqrt(np.mean(d[ok] ** 2))))
|
||||
flats.append(float((d[ok] == 0).mean()))
|
||||
# errore del previsore di vol: vol realizzata nei 30g SEGUENTI / vol prevista
|
||||
fwd = pd.Series(r).rolling(30).std().shift(-30).values * np.sqrt(365.25)
|
||||
okf = ok & np.isfinite(fwd)
|
||||
@@ -478,21 +545,35 @@ def attacco2() -> None:
|
||||
comb = (0.5 * J["BTC"] + 0.5 * J["ETH"]).std() * 365 ** .5
|
||||
div = comb / (0.5 * vb + 0.5 * ve)
|
||||
conv = float(np.mean(convs))
|
||||
per_leg = (0.5 * vb + 0.5 * ve) / 0.20
|
||||
p_flat = float(np.mean(flats))
|
||||
f_flat = float(np.sqrt(1.0 - p_flat))
|
||||
f_conv = conv / f_flat if f_flat > 0 else float("nan")
|
||||
print(f"\n gamba singola: BTC {vb:.2%} · ETH {ve:.2%} -> 50/50 combinato {comb:.2%}")
|
||||
print(f" fattore di DIVERSIFICAZIONE BTC/ETH = {div:.3f} (corr {J['BTC'].corr(J['ETH']):.2f})")
|
||||
print(f"\n CATENA: 20,00% ×{conv:.3f} (convinzione) ×{np.mean(vols_real)/np.mean(vols_pred):.3f} "
|
||||
f"(previsore di vol) ×{div:.3f} (diversificazione) = "
|
||||
f"{0.20*conv*(np.mean(vols_real)/np.mean(vols_pred))*div:.2%}")
|
||||
print(f" misurato: {tp.std()*365**.5:.2%}")
|
||||
print(f"\n 📌 IL FATTO CHE LO SCETTICATO NON DICE: il pezzo grosso NON e' lo stare flat, e'")
|
||||
print(f" che `tsmom_blend` e' la MEDIA DEI SEGNI di 3 orizzonti, quindi quando TP01 e'")
|
||||
print(f" 'a mercato' e' quasi sempre a 1/3 o 2/3 di convinzione. Contando lo zero, il")
|
||||
print(f" fattore di convinzione da solo vale ×{conv:.2f}. Lo stare flat e' UNA PARTE di")
|
||||
print(f" quel fattore, non il fattore.")
|
||||
print(f" Conseguenza operativa opposta a quella suggerita: se il divario fosse solo")
|
||||
print(f" 'flat', alzare target_vol non aumenterebbe il rischio nei giorni flat (dove non")
|
||||
print(f" c'e' posizione). Essendo CONVINZIONE, alzare target_vol alza la posizione")
|
||||
print(f" proprio nei giorni in cui il segnale e' PIU' DEBOLE (1 orizzonte su 3).")
|
||||
print(f"\n SCOMPOSIZIONE ESATTA (i due fattori si moltiplicano e chiudono il conto):")
|
||||
print(f" 20,00% ×{per_leg:.3f} (dentro la GAMBA) ×{div:.3f} (diversificazione BTC/ETH)"
|
||||
f" = {0.20*per_leg*div:.2%}")
|
||||
print(f" misurato sullo sleeve di produzione: {tp.std()*365**.5:.2%}")
|
||||
print(f"\n E il ×{per_leg:.3f} della gamba si spacca a sua volta in DUE meta' quasi uguali:")
|
||||
print(f" · sta FLAT nel {p_flat:.1%} delle barre (direzione 0) -> ×{f_flat:.3f}")
|
||||
print(f" · quando NON e' flat sta a convinzione PARZIALE (rms {conv/f_flat:.2f}) -> ×{f_conv:.3f}")
|
||||
print(f" prodotto sqrt(E[d²]) = ×{conv:.3f}; il residuo fino a ×{per_leg:.3f} e'")
|
||||
print(f" la correlazione fra convinzione e vol realizzata/prevista"
|
||||
f" (previsore di vol da solo: ×{np.mean(vols_real)/np.mean(vols_pred):.3f}).")
|
||||
print(f"\n 📌 QUELLO CHE LO SCETTICATO NON DICE, e sono DUE cose.")
|
||||
print(f" (i) «TP01 e' long-flat» spiega **circa META'** del divario (×{f_flat:.2f}), non")
|
||||
print(f" il divario. L'altra meta' e' che `tsmom_blend` e' la MEDIA DEI SEGNI di 3")
|
||||
print(f" orizzonti: quando TP01 e' a mercato sta quasi sempre a 1/3 o 2/3 di")
|
||||
print(f" convinzione (×{f_conv:.2f}). E la differenza NON e' accademica: se il divario")
|
||||
print(f" fosse solo 'flat', alzare target_vol non toccherebbe i giorni senza")
|
||||
print(f" posizione; essendo CONVINZIONE, alzarlo aumenta la size proprio nei giorni")
|
||||
print(f" in cui il segnale e' PIU' DEBOLE (1 orizzonte su 3 d'accordo).")
|
||||
print(f" (ii) un terzo fattore non sta nemmeno nello sleeve: ×{div:.2f} viene dal")
|
||||
print(f" COMBINARE BTC ed ETH a 50/50 (corr {J['BTC'].corr(J['ETH']):.2f}). Non e' un")
|
||||
print(f" difetto d'etichetta: e' diversificazione, ed e' esattamente cio' che si")
|
||||
print(f" vuole. Chiamare 'sovrastima del rischio' anche questo pezzo confonde una")
|
||||
print(f" proprieta' desiderata con un errore di misura.")
|
||||
|
||||
print("\n E TP01 E' UN CASO PARTICOLARE, non la regola — gli altri sleeve vol-targeted:")
|
||||
print(f"\n {'sleeve':>9}{'target dichiarato':>19}{'vol REALIZZATA':>16}{'su barre ATTIVE':>17}{'attive':>9}")
|
||||
@@ -590,6 +671,59 @@ def attacco2() -> None:
|
||||
print(" esattamente quella protetta dal test di guardia.")
|
||||
|
||||
|
||||
def attacco2b() -> None:
|
||||
"""Quali affermazioni pubblicate cambiano, e quali no — misurato, non argomentato."""
|
||||
from src.portfolio import sleeves as SL
|
||||
sezione("(6-bis) ATTACCO 2c — QUALI AFFERMAZIONI CAMBIANO (e la maggior parte NON cambia)")
|
||||
tp, sk = SL._tp01_returns(), SL._skyhook_returns()
|
||||
if tp.index.tz is None:
|
||||
tp = tp.tz_localize("UTC")
|
||||
if sk.index.tz is None:
|
||||
sk = sk.tz_localize("UTC")
|
||||
B = pd.concat({"TP01": tp, "SKH01": sk}, axis=1, join="inner").fillna(0.0)
|
||||
w = np.array([W_TP, W_SKH])
|
||||
X = B[["TP01", "SKH01"]].values
|
||||
bk = X @ w
|
||||
cov = np.cov(X.T)
|
||||
var_b = float(bk.var())
|
||||
rc = w * (cov @ w) / (cov @ w @ w)
|
||||
print("\n NON cambia NIENTE di misurato. Ogni Sharpe, maxDD, muro, traiettoria e rendita")
|
||||
print(" pubblicati sono calcolati sulla SERIE DI RITORNI REALIZZATA, mai sull'etichetta:")
|
||||
print(" `target_vol` entra nel codice come parametro di sizing e non compare in nessuna")
|
||||
print(" formula di rischio a valle. Un'etichetta che sovrastima non rende ottimista un")
|
||||
print(" numero che non la usa.")
|
||||
print("\n CAMBIA una classe di affermazioni sola: quelle che CONFRONTANO un parametro con")
|
||||
print(" una misura. Nel progetto ce n'e' una, ed e' nel libro LIVE:")
|
||||
print(f"\n {'sleeve':>8}{'peso NOZIONALE':>16}{'vol realizzata':>16}"
|
||||
f"{'contributo al RISCHIO':>23}")
|
||||
for i, nm in enumerate(("TP01", "SKH01")):
|
||||
print(f" {nm:>8}{w[i]:>16.0%}{X[:, i].std()*365**.5:>16.2%}{rc[i]:>23.1%}")
|
||||
print(f" {'libro':>8}{1.0:>16.0%}{bk.std()*365**.5:>16.2%}{sum(rc):>23.1%}")
|
||||
print(f" corr TP01/SKH01 {np.corrcoef(X.T)[0,1]:+.3f} vol libro {bk.std()*365**.5:.2%}")
|
||||
sig = X.std(axis=0) * 365 ** 0.5
|
||||
cf = (W_TP * 0.20) ** 2 / ((W_TP * 0.20) ** 2 + (W_SKH * sig[1]) ** 2)
|
||||
print("\n ❌ IPOTESI MIA, REFUTATA DAL MIO STESSO NUMERO. Mi aspettavo che l'etichetta")
|
||||
print(" nascondesse una ripartizione del rischio molto diversa dai pesi di nozionale.")
|
||||
print(f" Non e' cosi': {rc[0]*100:.0f}/{rc[1]*100:.0f} contro {W_TP*100:.0f}/{W_SKH*100:.0f}"
|
||||
f" — coincidono entro un paio di punti, e per una")
|
||||
print(" ragione che si legge in una riga: contributi di rischio ~ (w·σ)² con corr")
|
||||
print(f" {np.corrcoef(X.T)[0,1]:+.2f} ~ 0, e {W_TP:.2f}×{sig[0]:.1%} = {W_TP*sig[0]:.2%} contro"
|
||||
f" {W_SKH:.2f}×{sig[1]:.1%} = {W_SKH*sig[1]:.2%}")
|
||||
print(" stanno gia' in rapporto ~75/25. **Coincidenza numerica, non struttura.**")
|
||||
print("\n 📌 IL FATTO CHE RESTA, ed e' piu' utile dell'ipotesi che ho perso: l'errore non")
|
||||
print(" lo commette chi legge i PESI, lo commette chi legge l'ETICHETTA. Un lettore che")
|
||||
print(f" prendesse `target_vol=20%` per buono calcolerebbe TP01 al {cf*100:.0f}% del rischio")
|
||||
print(f" di libro; il vero e' {rc[0]*100:.0f}%. L'etichetta non rende ottimista nessun numero")
|
||||
print(" pubblicato (nessuno la usa) — sposta di ~16 punti l'ATTRIBUZIONE del rischio,")
|
||||
print(" cioe' proprio la domanda 'quale gamba mi fa male' su cui il progetto ha gia'")
|
||||
print(" tre misure indipendenti che indicano SKH01 come la fragile (LOO d'ancora,")
|
||||
print(" sensibilita' alla fee 4x, vol realizzata piu' alta).")
|
||||
print("\n E si precisa una frase: «SKH01 e' l'unica delle 5 a NON essere vol-targeted» e'")
|
||||
print(" esatta sul CODICE e va letta insieme al fatto che e' anche quella che di vol ne")
|
||||
print(" realizza di PIU' (21,0% contro 12,1%). Non e' una contraddizione: e' il motivo per")
|
||||
print(" cui il peso 25% e' stato confermato tre volte all'estremo prudente.")
|
||||
|
||||
|
||||
def guardia() -> None:
|
||||
sezione("(7) IL VINCOLO DURO — cosa protegge davvero il test di guardia")
|
||||
cfg = json.loads((ROOT / "config" / "live.json").read_text())
|
||||
@@ -614,5 +748,6 @@ def guardia() -> None:
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
attacco2()
|
||||
attacco2b()
|
||||
guardia()
|
||||
print(f"\n▸ totale: consegna nel messaggio dell'agente.")
|
||||
|
||||
@@ -540,8 +540,11 @@ def main() -> None:
|
||||
f"-> {'INDISTINGUIBILI' if abs(dA) < 2*seA else 'differenza reale'}")
|
||||
print(f" argmax J - libro LIVE : {dB:+.4f} +- {seB:.4f} "
|
||||
f"-> {'INDISTINGUIBILI' if abs(dB) < 2*seB else 'differenza reale'}")
|
||||
print(" La prima riga E' il risultato del filone: progettare per la barriera invece che")
|
||||
print(" per lo Sharpe NON sposta i pesi. La seconda dice dove sta il guadagno vero.")
|
||||
quota = abs(dA) / abs(dB) if dB != 0 else float("nan")
|
||||
print(f" La prima riga E' il risultato del filone: riallocare per la BARRIERA invece che")
|
||||
print(f" per lo SHARPE vale {dA:+.3f} di J, cioe' il {quota:.0%} del {dB:+.3f} che vale")
|
||||
print(f" passare dal libro LIVE a un libro diversificato QUALSIASI. Il grosso del guadagno")
|
||||
print(f" NON e' nella funzione obiettivo: e' nell'aggiungere lo sleeve che a $600 non entra.")
|
||||
|
||||
print("\n Le prime 8 celle per J:")
|
||||
print(f" {'#':>3} {'pesi':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8} "
|
||||
@@ -688,13 +691,16 @@ def main() -> None:
|
||||
for lv in LEVS:
|
||||
rows.append((w, lv, objective(w, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt)))
|
||||
w_, lv_, o_ = max(rows, key=lambda x: x[2]["J"])
|
||||
oL = max((objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx,
|
||||
drift_mult=dmt) for lv in LEVS), key=lambda x: x["J"])
|
||||
lvL, oL = max(((lv, objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx,
|
||||
drift_mult=dmt)) for lv in LEVS), key=lambda x: x[1]["J"])
|
||||
dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO", ev_idx, fu_idx)
|
||||
print(f" {mult:>12.2f} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {o_['p_pass']:>8.1%} "
|
||||
f"{o_['p_alive']:>8.1%} {o_['e_payout']:>9,.0f}$ (LIVE 75/25: J {oL['J']:.3f})")
|
||||
print("\n Se anche a drift XS01 = 0 (cioe' XS01 ridotto a pura diversificazione di VOLATILITA',")
|
||||
print(" senza alcun rendimento atteso) la configurazione diversificata batte il libro LIVE,")
|
||||
print(" allora il risultato NON dipende dall'edge di XS01 ma dalla sua DECORRELAZIONE.")
|
||||
f"{o_['p_alive']:>8.1%} {o_['e_payout']:>9,.0f}$ LIVE {oL['J']:.3f} "
|
||||
f"delta {dd_:+.3f} +-{sd_:.3f}")
|
||||
print("\n La colonna che decide e' l'ULTIMA. Se il vantaggio svanisce col drift di XS01, il")
|
||||
print(" risultato e' un'ipotesi sul RENDIMENTO ATTESO di XS01 misurato sulla sua finestra di")
|
||||
print(" scoperta; se resta a drift 0, il vantaggio e' pura DECORRELAZIONE e non dipende da")
|
||||
print(" quell'ipotesi. Il numero, non la narrazione, dice quale delle due.")
|
||||
|
||||
# ---------------------------------------------------------------- F. VRP01 / GTAA01
|
||||
tick("E. banda ancora")
|
||||
@@ -715,6 +721,10 @@ def main() -> None:
|
||||
lab = "nessuna (base)" if extra is None else f"{extra} @ {wx:.0%}"
|
||||
print(f" {lab:>34} {lv:>6.3f} {o['J']:>7.3f} {o['p_pass']:>8.1%} {o['p_alive']:>8.1%} "
|
||||
f"{o['sharpe']:>7.2f} {o['e_payout']:>9,.0f}$")
|
||||
print("\n Le righe VRP01 e GTAA01 escono quasi IDENTICHE: non e' un errore, e' che sotto questa")
|
||||
print(" funzione obiettivo i due sleeve sono lo stesso oggetto (vol ~6.2%, drift ~7.7%, corr ~0")
|
||||
print(" col crypto). La barriera vede vol, drift e correlazione; la differenza fra i due — skew")
|
||||
print(" -4.6 contro -0.4 — a peso 10-20% e' diluita sotto la soglia di daily-loss.")
|
||||
print("\n ATTENZIONE: la riga VRP01 e' calcolata con **gap = 0** (il mark infra-settimana non")
|
||||
print(" esiste nel dataset dello sleeve): e' un TETTO, non una stima. Con il mark misurato il")
|
||||
print(" 30/07 (peggiore infra-settimana mediano -6.9%, minimo -81.7%) una barriera STATICA al")
|
||||
@@ -815,10 +825,22 @@ def main() -> None:
|
||||
seed=20260822, lens="coupled", start=START_WIN)
|
||||
print(f" {fct:>9.2f} {lab:>26} {r['med']:>10.2f} {r['p_target']:>10.1%} "
|
||||
f"{r['p_zero']:>8.1%}")
|
||||
print("\n ⚠ Il headline ~6% del 25/07 NON e' la riga 'LIVE su tutti i conti' (quella li' era")
|
||||
print(" CONC-2SL, dichiarata 'la PEGGIORE'): era la politica MISTA, sleeve diversi su conti")
|
||||
print(" diversi. Replica ESATTA di quella politica, stessa macchina, de-luck 0.60, leva 0.75:")
|
||||
pc.DELUCK = 0.60
|
||||
cfgs25 = [(0.75, 0.25, 0.0), (0.55, 0.20, 0.25), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
|
||||
r25 = pc.simulate(cfgs25, [1, 4, 1, 2, 1, 4], n_paths=1500, lev=0.75, seed=20260822,
|
||||
lens="coupled", start=START_WIN)
|
||||
print(f" MISTO (25/07) @0.75x, de-luck 0.60 : EUR/g med {r25['med']:.2f} | "
|
||||
f"P(>=50/g) {r25['p_target']:.1%} | P(zero) {r25['p_zero']:.1%}")
|
||||
print(f" -> il 25/07 pubblico' P(>=50/g) 5.6-6.5% e P(zero) 52% a 0.75x. "
|
||||
f"{'REPLICA OK' if 0.03 <= r25['p_target'] <= 0.10 else 'NON REPLICA — leggere con cautela'}")
|
||||
pc.DELUCK = DELUCK
|
||||
print("\n La cella in alto a sinistra (0.60 + LIVE) e' la configurazione del 25/07 e va letta")
|
||||
print(" come replica: se riproduce ~6% di P(>=50/g) e ~52-65% di P(zero), la macchina e' la")
|
||||
print(" stessa e le altre tre celle misurano cio' che dichiarano.")
|
||||
print("\n Lettura della 2x2: il de-luck e l'allocazione contribuiscono ENTRAMBI, e vanno citati")
|
||||
print(" separati. Il fattore 0.60 era una stima a occhio poi RI-MISURATA troppo severa (26/07):")
|
||||
print(" usarlo ancora sarebbe sbagliato, ma il salto da 1.7% a 41.7% e' il prodotto di due")
|
||||
print(" correzioni, non di una scoperta.")
|
||||
|
||||
print("\n" + "=" * 104)
|
||||
print(f" fatto in {time.time()-t0:.0f}s")
|
||||
|
||||
@@ -40,7 +40,6 @@ Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822_skew.py
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
@@ -57,8 +56,6 @@ import altlib as A # noqa: E402
|
||||
import cblib as CB # noqa: E402
|
||||
|
||||
CHAIN = ROOT / "data" / "raw" / "cb_chain"
|
||||
SCRATCH = Path(os.environ.get("SKEW_SCRATCH", "/tmp/claude-1001/-opt-docker-PythagorasGoal/"
|
||||
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad"))
|
||||
ASSETS = ("BTC", "ETH")
|
||||
COLS = ["ts", "asset", "instrument_name", "option_type", "strike", "exp",
|
||||
"bid", "ask", "mid", "iv", "delta", "open_interest", "quote_status"]
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
"""r0822 SLIP-AUDIT — l'attrito VERO del libro live: quanto costa davvero un fill.
|
||||
|
||||
DOMANDA
|
||||
-------
|
||||
Il progetto modella le fee con cura chirurgica (3,50 bps/lato misurati sui fill reali il 21/08)
|
||||
ma ASSUME il fill al prezzo di chiusura della barra. Lo **slippage realizzato** del libro live
|
||||
non e' mai stato misurato: e' dichiarato "rischio #1" per XSR01 e semplicemente ignorato per
|
||||
TP01/SKH01. Ci sono 18 esecuzioni reali dal 2026-07-14. Questo script le misura.
|
||||
|
||||
La domanda decidibile e' UNA: i backtest girano a `fee_rt=0.001` (10 bps RT) mentre la fee vera
|
||||
e' 3,50 bps/lato = 7 bps RT. C'e' **3 bps RT = 1,50 bps/LATO** di margine gia' pagato dal modello.
|
||||
**Lo slippage realizzato ci sta dentro?**
|
||||
|
||||
TRE DIFETTI DEL DATO TROVATI PRIMA DI MISURARE (e riparati/dichiarati qui)
|
||||
-------------------------------------------------------------------------
|
||||
(1) **`ts_utc` nel log NON e' l'ora del fill.** `book_execute.py:213` scrive
|
||||
`ts_utc=str(pd.Timestamp(r['last_data']))` = l'etichetta dell'ULTIMA BARRA DEL FEED, non
|
||||
l'orologio. Tutti e 18 i record dicono `00:00:00`. L'ora vera si recupera solo dalle
|
||||
intestazioni di `logs/cron_book.log` (`===== <ISO> cron_book =====`). Senza questo passo
|
||||
ogni confronto "al prezzo dell'ora del fill" misurerebbe la mezzanotte.
|
||||
Prova che serviva: il fill #1 e' etichettato `2026-07-08` ed e' stato eseguito il
|
||||
**2026-07-14T14:00Z** — sei giorni dopo (feed-freeze del 14/07, gia' noto al progetto).
|
||||
(2) **Il feed certificato NON e' lo strumento che si trada.** `rebuild_history.DERIBIT_INSTR`
|
||||
prende `BTC/USD:BTC` / `ETH/USD:ETH` = i perpetual **INVERSE**; `deribit.INSTRUMENT` esegue su
|
||||
`BTC_USDC-PERPETUAL` / `ETH_USDC-PERPETUAL` = i **LINEARI USDC**. Il commento nel sorgente dice
|
||||
"inverse ~ lineare entro 3 bps": **3 bps e' esattamente la taglia della domanda**. Misurare lo
|
||||
slippage contro il feed certificato significherebbe misurare la BASE, non lo slippage.
|
||||
Qui la base viene MISURATA (non assunta) e il riferimento vero e' lo strumento tradato.
|
||||
Stessa forma del difetto di `fee_watch` del 21/08.
|
||||
(3) **Convenzione LINEARE**: `amount` = quantita' BASE, `fee` gia' in USDC ->
|
||||
`fee_bps = fee / (filled * price) * 1e4`. La convenzione inverse darebbe ~8 ordini di
|
||||
grandezza di errore SENZA sollevare nulla. Il controllo e' stampato in chiaro.
|
||||
|
||||
TRE RIFERIMENTI, TENUTI SEPARATI (il punto 1c della missione)
|
||||
------------------------------------------------------------
|
||||
Il modello decide sulla barra GIORNALIERA, il cron esegue all'ORA: sono grandezze diverse.
|
||||
REF-A "modello" = close della barra 1d etichettata `last_data` (= il prezzo a cui il
|
||||
BACKTEST assume di riempire). Misura modello-vs-realta', NON slippage.
|
||||
REF-B "5m/1h" = close dell'ultima barra CHIUSA prima del fill sullo strumento TRADATO.
|
||||
Questo, e solo questo, e' slippage d'esecuzione.
|
||||
REF-C "VWAP 1h" = VWAP delle 12 barre 5m dell'ora che contiene il fill (benchmark
|
||||
NON causale, dichiarato tale).
|
||||
Segno orientato al lato: `costo_bps = lato * (fill - ref)/ref * 1e4`, lato +1 buy / -1 sell.
|
||||
Positivo = costo, negativo = guadagno.
|
||||
|
||||
LA STATISTICA CHE HA POTENZA (e perche' quella in bps non ne ha)
|
||||
----------------------------------------------------------------
|
||||
Con n=18 la via in bps e' persa in partenza: il rumore e' la deriva del prezzo fra la chiusura
|
||||
della barra 5m e il fill, che vale decine di bps. La statistica giusta e' NORMALIZZATA dal range
|
||||
della barra: il nostro fill e' esso stesso un print della barra 5m che lo contiene, quindi
|
||||
q = (fill - low)/(high - low) per un BUY, q = 1 - quello per un SELL.
|
||||
Se i nostri ordini market avessero impatto, un BUY stamperebbe AL MASSIMO della barra -> q -> 1.
|
||||
Senza impatto q e' un'estrazione ~uniforme -> E[q] = 0.5. sigma_q = 1/sqrt(12) = 0.289 e' ~20x
|
||||
piu' piccolo del rumore in bps.
|
||||
|
||||
Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822_slip_audit.py
|
||||
(--no-net salta il fetch dello strumento lineare e lo dichiara non misurato)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
SCRATCH = Path(os.environ.get("SLIP_SCRATCH", "/tmp/claude-1001/-opt-docker-PythagorasGoal/"
|
||||
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad"))
|
||||
|
||||
EXEC_LOG = ROOT / "data" / "live" / "book_executions.jsonl"
|
||||
CRON_LOG = ROOT / "logs" / "cron_book.log"
|
||||
|
||||
# Sensibilita' PUBBLICATA del libro (r0726_fee_sensitivity.py, CLAUDE.md 26/07).
|
||||
# Unita': per bps PER LATO (verificato sulla tabella: 0->15 bps/lato = Sharpe 1.849->1.597).
|
||||
DSHARPE_PER_BPS_SIDE = -0.017
|
||||
DCAGR_PER_BPS_SIDE = -0.23 # punti percentuali di CAGR
|
||||
# Margine gia' pagato dal modello: backtest 10 bps RT vs fee vera 7 bps RT = 3 bps RT.
|
||||
MARGIN_BPS_SIDE = 1.5
|
||||
FEE_TAKER_BPS_SIDE = 3.50 # tier base misurato (fee_watch, 21/08)
|
||||
|
||||
# Strumento TRADATO (src/live/deribit.INSTRUMENT) vs strumento del FEED certificato
|
||||
# (scripts/analysis/rebuild_history.DERIBIT_INSTR). NON sono lo stesso: vedi difetto (2).
|
||||
CCXT_LINEAR = {"BTC": "BTC/USDC:USDC", "ETH": "ETH/USDC:USDC"}
|
||||
CCXT_INVERSE = {"BTC": "BTC/USD:BTC", "ETH": "ETH/USD:ETH"}
|
||||
|
||||
RNG = np.random.default_rng(822)
|
||||
N_BOOT = 20_000
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------- fills
|
||||
def _parse_cron_fills() -> list[dict]:
|
||||
"""Ora VERA di ogni fill, dalle intestazioni di cron_book.log. Vedi difetto (1)."""
|
||||
if not CRON_LOG.exists():
|
||||
raise SystemExit(f"manca {CRON_LOG}: l'ora vera del fill non e' recuperabile altrove")
|
||||
head = re.compile(r"^===== (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) cron_book =====")
|
||||
fill = re.compile(r"^\s+-> (BUY|SELL) ([\d.]+) @ \$([\d,.]+) fee ([\d.]+) \((\w+)\)")
|
||||
out, cur = [], None
|
||||
with open(CRON_LOG, errors="replace") as f:
|
||||
for line in f:
|
||||
m = head.match(line)
|
||||
if m:
|
||||
cur = pd.Timestamp(m.group(1))
|
||||
continue
|
||||
m = fill.match(line)
|
||||
if m:
|
||||
out.append(dict(fill_ts=cur, side=m.group(1).lower(),
|
||||
amt=float(m.group(2)), px=float(m.group(3).replace(",", "")),
|
||||
fee=float(m.group(4)), status=m.group(5)))
|
||||
return out
|
||||
|
||||
|
||||
def load_fills() -> pd.DataFrame:
|
||||
"""JSONL (verita' sui numeri) + cron log (verita' sull'ora), appaiati e VERIFICATI."""
|
||||
recs = [json.loads(l) for l in EXEC_LOG.read_text().splitlines() if l.strip()]
|
||||
cron = _parse_cron_fills()
|
||||
if len(recs) != len(cron):
|
||||
raise SystemExit(f"appaiamento impossibile: {len(recs)} righe jsonl vs {len(cron)} fill "
|
||||
f"nel cron log. Non invento un'ora: mi fermo.")
|
||||
rows = []
|
||||
for i, (r, c) in enumerate(zip(recs, cron)):
|
||||
# entrambi i file sono append-only dallo stesso processo -> stesso ordine. Lo PROVO.
|
||||
ok = (r["side"] == c["side"]
|
||||
and abs(round(r["filled"], 4) - round(c["amt"], 4)) < 1e-9
|
||||
and abs(round(r["price"], 1) - round(c["px"], 1)) < 0.051
|
||||
and abs(round(r["fee"], 5) - round(c["fee"], 5)) < 1e-9)
|
||||
if not ok:
|
||||
raise SystemExit(f"riga {i}: jsonl e cron log NON combaciano ({r} vs {c}) -> mi fermo")
|
||||
if c["status"] != "OK":
|
||||
continue
|
||||
rows.append(dict(
|
||||
i=i, fill_ts=c["fill_ts"], model_bar=pd.Timestamp(r["ts_utc"], tz="UTC"),
|
||||
asset=r["asset"], side=r["side"], lato=1.0 if r["side"] == "buy" else -1.0,
|
||||
filled=float(r["filled"]), price=float(r["price"]), fee=float(r["fee"]),
|
||||
notional=float(r["filled"]) * float(r["price"]),
|
||||
skh=r.get("skh_sign"), tp_frac=r.get("tp_frac"),
|
||||
))
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------- prezzi
|
||||
def _certified(asset: str, tf: str, t0, t1) -> pd.DataFrame:
|
||||
"""Feed certificato su disco = strumento INVERSE. Letto a colonne e tagliato subito (RAM)."""
|
||||
f = ROOT / "data" / "raw" / f"{asset.lower()}_{tf}.parquet"
|
||||
d = pd.read_parquet(f, columns=["timestamp", "open", "high", "low", "close", "volume"])
|
||||
d["ts"] = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
|
||||
d = d[(d["ts"] >= t0) & (d["ts"] <= t1)].drop(columns=["timestamp"]).reset_index(drop=True)
|
||||
return d
|
||||
|
||||
|
||||
def _fetch_linear(asset: str, t0, t1) -> pd.DataFrame | None:
|
||||
"""Strumento TRADATO (lineare USDC) via ccxt pubblico. Cache su scratchpad, pacing gentile.
|
||||
|
||||
Il progetto ha una storia di rate-limit per-IP (29-30/07): qui sono ~13 pagine per asset,
|
||||
distanziate. Nessuna scrittura sotto data/."""
|
||||
SCRATCH.mkdir(parents=True, exist_ok=True)
|
||||
cache = SCRATCH / f"lin_{asset.lower()}_5m.parquet"
|
||||
if cache.exists():
|
||||
d = pd.read_parquet(cache)
|
||||
if d["ts"].min() <= t0 and d["ts"].max() >= t1 - pd.Timedelta(minutes=10):
|
||||
return d
|
||||
try:
|
||||
import ccxt, time
|
||||
ex = ccxt.deribit({"enableRateLimit": True})
|
||||
sym, since, rows = CCXT_LINEAR[asset], int(t0.timestamp() * 1000), []
|
||||
end = int(t1.timestamp() * 1000)
|
||||
while since < end:
|
||||
o = ex.fetch_ohlcv(sym, "5m", since=since, limit=1000)
|
||||
if not o:
|
||||
break
|
||||
rows += o
|
||||
since = o[-1][0] + 300_000
|
||||
time.sleep(0.35)
|
||||
if not rows:
|
||||
return None
|
||||
d = pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"])
|
||||
d = d.drop_duplicates("timestamp").sort_values("timestamp")
|
||||
d["ts"] = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
|
||||
d = d.drop(columns=["timestamp"]).reset_index(drop=True)
|
||||
d.to_parquet(cache)
|
||||
return d
|
||||
except Exception as e: # rete assente = si dichiara
|
||||
print(f" ! fetch lineare {asset} fallito: {type(e).__name__}: {str(e)[:90]}")
|
||||
return None
|
||||
|
||||
|
||||
def _last_closed(bars: pd.DataFrame, t) -> pd.Series | None:
|
||||
"""Ultima barra CHIUSA a `t`: una barra etichettata b chiude a b+durata."""
|
||||
if bars is None or bars.empty:
|
||||
return None
|
||||
step = bars["ts"].diff().median()
|
||||
ok = bars[bars["ts"] + step <= t]
|
||||
return None if ok.empty else ok.iloc[-1]
|
||||
|
||||
|
||||
def _containing(bars: pd.DataFrame, t) -> pd.Series | None:
|
||||
if bars is None or bars.empty:
|
||||
return None
|
||||
step = bars["ts"].diff().median()
|
||||
ok = bars[(bars["ts"] <= t) & (t < bars["ts"] + step)]
|
||||
return None if ok.empty else ok.iloc[-1]
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------- stats
|
||||
def ci_mean(x, boot=True):
|
||||
x = np.asarray(x, float)
|
||||
x = x[np.isfinite(x)]
|
||||
n = len(x)
|
||||
if n < 2:
|
||||
return dict(n=n, mean=np.nan, lo=np.nan, hi=np.nan, sd=np.nan, med=np.nan)
|
||||
m, sd = x.mean(), x.std(ddof=1)
|
||||
if boot:
|
||||
bs = RNG.choice(x, size=(N_BOOT, n), replace=True).mean(axis=1)
|
||||
lo, hi = np.percentile(bs, [2.5, 97.5])
|
||||
else:
|
||||
from scipy import stats
|
||||
h = stats.t.ppf(0.975, n - 1) * sd / np.sqrt(n)
|
||||
lo, hi = m - h, m + h
|
||||
return dict(n=n, mean=m, lo=lo, hi=hi, sd=sd, med=float(np.median(x)))
|
||||
|
||||
|
||||
def fmt(d, u="bps"):
|
||||
if not np.isfinite(d["mean"]):
|
||||
return "n/d"
|
||||
z = "" if (d["lo"] <= 0 <= d["hi"]) else " <- ZERO ESCLUSO"
|
||||
return f"{d['mean']:+7.2f} {u} IC95 [{d['lo']:+7.2f}, {d['hi']:+7.2f}] (med {d['med']:+7.2f}, n={d['n']}){z}"
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 96)
|
||||
print(" r0822 SLIP-AUDIT — l'attrito VERO del libro live (18 esecuzioni, dal 2026-07-14)")
|
||||
print("=" * 96)
|
||||
|
||||
fills = load_fills()
|
||||
t0 = fills["fill_ts"].min().floor("D") - pd.Timedelta(days=3)
|
||||
t1 = fills["fill_ts"].max().ceil("h") + pd.Timedelta(hours=2)
|
||||
|
||||
# ---------------------------------------------------------------- 0. difetti del dato
|
||||
print("\n[0] DIFETTI DEL DATO — trovati PRIMA di misurare\n" + "-" * 96)
|
||||
lag = (fills["fill_ts"] - fills["model_bar"]).dt.total_seconds() / 3600.0
|
||||
print(f" (1) `ts_utc` nel jsonl = etichetta dell'ultima barra del feed, NON l'ora del fill.")
|
||||
print(f" 18/18 record dicono 00:00:00. Ora vera recuperata da logs/cron_book.log.")
|
||||
print(f" Scarto fill - etichetta: mediana {lag.median():.1f}h, max {lag.max():.1f}h "
|
||||
f"({(lag > 26).sum()} fill oltre le 26h).")
|
||||
if (lag > 26).any():
|
||||
w = fills.loc[lag.idxmax()]
|
||||
print(f" Il caso estremo e' il fill #{w['i']}: etichetta {w['model_bar'].date()}, "
|
||||
f"eseguito {w['fill_ts']} = feed-freeze del 14/07 (gia' noto al progetto).")
|
||||
print(f" (2) Feed certificato = INVERSE ({CCXT_INVERSE['BTC']}), libro tradato = LINEARE "
|
||||
f"({CCXT_LINEAR['BTC']}).")
|
||||
print(f" La base fra i due e' misurata sotto, non assunta.")
|
||||
|
||||
# ---------------------------------------------------------------- 1. convenzione fee
|
||||
print("\n[1] CONTROLLO DI CONVENZIONE (lineare USDC) — la trappola da ~8 ordini di grandezza\n"
|
||||
+ "-" * 96)
|
||||
fills["fee_bps"] = fills["fee"] / fills["notional"] * 1e4
|
||||
fb = ci_mean(fills["fee_bps"])
|
||||
print(f" fee_bps = fee_usdc / (filled * price) * 1e4 [LINEARE: amount=base, fee gia' in USDC]")
|
||||
print(f" media {fb['mean']:.3f} bps/lato · mediana {fb['med']:.3f} · "
|
||||
f"min {fills['fee_bps'].min():.3f} · max {fills['fee_bps'].max():.3f}")
|
||||
print(f" tier base dichiarato da fee_watch: {FEE_TAKER_BPS_SIDE:.2f} bps/lato -> "
|
||||
f"scarto {fb['med'] - FEE_TAKER_BPS_SIDE:+.3f} bps")
|
||||
bad = (fills["fee"] / fills["filled"] * 1e4).median()
|
||||
print(f" [se avessi usato la convenzione INVERSE: {bad:,.0f} bps -> "
|
||||
f"{bad / FEE_TAKER_BPS_SIDE:,.0f}x il vero, e senza sollevare nulla]")
|
||||
odd = fills[(fills["fee_bps"] - FEE_TAKER_BPS_SIDE).abs() > 0.05]
|
||||
if len(odd):
|
||||
print(f" ⚠️ {len(odd)}/{len(fills)} fill NON al tier {FEE_TAKER_BPS_SIDE:.2f}:")
|
||||
for _, o in odd.iterrows():
|
||||
print(f" #{o['i']} {o['fill_ts']} {o['asset']} {o['side']} "
|
||||
f"{o['filled']} @ {o['price']:.2f} = ${o['notional']:.2f} · "
|
||||
f"fee {o['fee']:.8f} = {o['fee_bps']:.2f} bps")
|
||||
print(f" -> nozionale ${odd['notional'].min():.2f}: sotto i ~$7 la fee sembra")
|
||||
print(f" arrotondata AL RIALZO. E' un costo REALE che il modello non ha, e")
|
||||
print(f" morde solo gli ordini minuscoli (dove il libro vive a $600).")
|
||||
|
||||
# ---------------------------------------------------------------- 2. potenza, PRIMA
|
||||
print("\n[2] POTENZA DICHIARATA — calcolata PRIMA di guardare lo slippage\n" + "-" * 96)
|
||||
sig5 = {}
|
||||
for a in sorted(fills["asset"].unique()):
|
||||
d = _certified(a, "5m", t0, t1)
|
||||
r = np.log(d["close"]).diff().dropna() * 1e4
|
||||
sig5[a] = float(r.std())
|
||||
sig = float(np.mean(list(sig5.values())))
|
||||
n = len(fills)
|
||||
mde_bps = 2.80 * sig / np.sqrt(n) # 80% potenza, bilaterale alpha=0.05
|
||||
print(f" Rumore strutturale: sigma dei ritorni 5m nella finestra = "
|
||||
+ " · ".join(f"{a} {v:.1f} bps" for a, v in sig5.items()) + f" (media {sig:.1f})")
|
||||
print(f" Il riferimento 5m dista dal fill 0-5 min -> il rumore e' di QUELL'ordine.")
|
||||
print(f" MDE in bps a n={n}, potenza 80%, alpha 5%: 2.80 * {sig:.1f} / sqrt({n}) = "
|
||||
f"**{mde_bps:.1f} bps/lato**")
|
||||
print(f" Il margine da difendere e' {MARGIN_BPS_SIDE:.1f} bps/lato (3 bps RT).")
|
||||
print(f" => VERDETTO DI POTENZA, dichiarato prima del risultato: la via in bps NON puo'")
|
||||
print(f" risolvere {MARGIN_BPS_SIDE:.1f} bps ({mde_bps / MARGIN_BPS_SIDE:.0f}x troppo grossolana). "
|
||||
f"Qualunque IC in bps conterra' lo zero;")
|
||||
print(f" un 'non distinguibile da zero' li' NON e' una prova di assenza di slippage.")
|
||||
mde_q = 2.80 * (1 / np.sqrt(12)) / np.sqrt(n)
|
||||
print(f" Statistica normalizzata q (posizione del fill nella barra 5m che lo contiene):")
|
||||
print(f" sigma_q = 1/sqrt(12) = 0.289 -> MDE_q = {mde_q:.3f} di range di barra.")
|
||||
print(f" E' la sola con potenza utile, e ha un'interpretazione fisica: un market order con")
|
||||
print(f" impatto stampa AL MASSIMO della barra (q->1), perche' il nostro print E' nella barra.")
|
||||
|
||||
# ---------------------------------------------------------------- 3. base inverse/lineare
|
||||
print("\n[3] BASE inverse-vs-lineare — MISURATA, non assunta (difetto 2)\n" + "-" * 96)
|
||||
lin, inv = {}, {}
|
||||
use_net = "--no-net" not in sys.argv
|
||||
for a in sorted(fills["asset"].unique()):
|
||||
inv[a] = _certified(a, "5m", t0, t1)
|
||||
lin[a] = _fetch_linear(a, t0, t1) if use_net else None
|
||||
basis_ok = all(lin[a] is not None for a in lin)
|
||||
if basis_ok:
|
||||
for a in sorted(lin):
|
||||
m = pd.merge(inv[a][["ts", "close"]], lin[a][["ts", "close"]], on="ts",
|
||||
suffixes=("_inv", "_lin"))
|
||||
b = (m["close_lin"] - m["close_inv"]) / m["close_inv"] * 1e4
|
||||
print(f" {a}: n={len(m):,} barre 5m · base mediana {b.median():+.2f} bps · "
|
||||
f"media {b.mean():+.2f} · IQR [{b.quantile(.25):+.2f}, {b.quantile(.75):+.2f}] · "
|
||||
f"|base| p95 {b.abs().quantile(.95):.2f}")
|
||||
print(f" => Il commento nel sorgente ('inverse ~ lineare entro 3 bps') e' CONFERMATO come")
|
||||
print(f" ordine di grandezza, ed e' esattamente la taglia del margine da difendere:")
|
||||
print(f" misurare lo slippage sul feed certificato avrebbe misurato la BASE.")
|
||||
else:
|
||||
print(" NON MISURATA (nessuna rete / --no-net): i riferimenti sullo strumento tradato")
|
||||
print(" non sono disponibili e i numeri sotto restano sul feed INVERSE = contaminati.")
|
||||
|
||||
# ---------------------------------------------------------------- 4. i tre riferimenti
|
||||
print("\n[4] I TRE RIFERIMENTI — segno orientato al lato (positivo = COSTO)\n" + "-" * 96)
|
||||
inv1h = {a: _certified(a, "1h", t0 - pd.Timedelta(days=10), t1)
|
||||
for a in sorted(fills["asset"].unique())}
|
||||
daily = {}
|
||||
for a, d in inv1h.items():
|
||||
g = d.set_index("ts").resample("1D")
|
||||
daily[a] = pd.DataFrame(dict(close=g["close"].last())).dropna()
|
||||
|
||||
rows = []
|
||||
for _, f in fills.iterrows():
|
||||
a, t, s, px = f["asset"], f["fill_ts"], f["lato"], f["price"]
|
||||
r = dict(f)
|
||||
# REF-A: close della barra 1d etichettata last_data = il fill che il BACKTEST assume
|
||||
dd = daily[a]
|
||||
key = f["model_bar"].tz_convert("UTC").normalize()
|
||||
r["ref_A"] = float(dd.loc[key, "close"]) if key in dd.index else np.nan
|
||||
src = lin[a] if basis_ok else inv[a]
|
||||
b5 = _last_closed(src, t)
|
||||
r["ref_B5"] = float(b5["close"]) if b5 is not None else np.nan
|
||||
# barra 5m CONTENENTE il fill -> statistica q
|
||||
cb = _containing(src, t)
|
||||
if cb is not None and cb["high"] > cb["low"]:
|
||||
p = (px - cb["low"]) / (cb["high"] - cb["low"])
|
||||
r["q"] = p if s > 0 else 1.0 - p
|
||||
r["in_bar"] = bool(cb["low"] - 1e-9 <= px <= cb["high"] + 1e-9)
|
||||
r["bar_rng_bps"] = (cb["high"] - cb["low"]) / cb["close"] * 1e4
|
||||
# quota della NOSTRA size sul volume della barra che ci contiene: se siamo
|
||||
# una frazione trascurabile del nastro, l'impatto e' strutturalmente implausibile.
|
||||
r["partecip"] = f["filled"] / cb["volume"] if cb["volume"] > 0 else np.nan
|
||||
else:
|
||||
r["q"], r["in_bar"], r["bar_rng_bps"], r["partecip"] = np.nan, None, np.nan, np.nan
|
||||
# REF-B1h e REF-C VWAP dell'ora contenente
|
||||
h0 = t.floor("h")
|
||||
hb = src[(src["ts"] >= h0) & (src["ts"] < h0 + pd.Timedelta(hours=1))]
|
||||
r["ref_C"] = float((hb["close"] * hb["volume"]).sum() / hb["volume"].sum()) \
|
||||
if len(hb) and hb["volume"].sum() > 0 else np.nan
|
||||
prev = src[src["ts"] + pd.Timedelta(minutes=5) <= h0]
|
||||
r["ref_B1"] = float(prev.iloc[-1]["close"]) if len(prev) else np.nan
|
||||
for k in ("A", "B5", "B1", "C"):
|
||||
v = r.get(f"ref_{k}")
|
||||
r[f"slip_{k}"] = s * (px - v) / v * 1e4 if v and np.isfinite(v) else np.nan
|
||||
rows.append(r)
|
||||
R = pd.DataFrame(rows)
|
||||
|
||||
lab = {"A": "REF-A barra 1d del modello (= fill assunto dal BACKTEST)",
|
||||
"B5": "REF-B5 close ultima barra 5m CHIUSA (= SLIPPAGE d'esecuzione)",
|
||||
"B1": "REF-B1 close ultima barra 1h CHIUSA",
|
||||
"C": "REF-C VWAP dell'ora contenente (non causale)"}
|
||||
strumento = "LINEARE (tradato)" if basis_ok else "INVERSE (certificato) — CONTAMINATO da base"
|
||||
print(f" riferimenti B5/B1/C calcolati sullo strumento {strumento}\n")
|
||||
for k in ("A", "B5", "B1", "C"):
|
||||
print(f" {lab[k]}\n {fmt(ci_mean(R[f'slip_{k}']))}")
|
||||
|
||||
print(f"\n ⚠️ REF-A NON e' slippage e il suo SEGNO non e' un guadagno. La barra 1d etichettata")
|
||||
print(f" `last_data` chiude fino a ~23h DOPO il fill: su questa finestra (BTC 66k->78k in")
|
||||
print(f" 4 giorni) un trend-follower che compra alle 01:07 batte per costruzione la")
|
||||
print(f" chiusura serale, ma il backtest tiene la posizione DA quella chiusura — e' una")
|
||||
print(f" differenza di FASE, non un attrito. La domanda 'quanto costa la barra parziale'")
|
||||
print(f" e' gia' stata misurata bene il 26/07 (r0726_tp01_partial_day, 24 ancore appaiate:")
|
||||
print(f" ±0.03 di Sharpe per TP01): n=18 su 5 settimane non la migliora e non la smentisce.")
|
||||
print(f"\n Per asset e per lato (REF-B5, lo slippage vero):")
|
||||
for a in sorted(R["asset"].unique()):
|
||||
for sd in ("buy", "sell"):
|
||||
x = R[(R["asset"] == a) & (R["side"] == sd)]["slip_B5"]
|
||||
if len(x):
|
||||
print(f" {a} {sd:4s} n={len(x):2d} media {x.mean():+7.2f} med {x.median():+7.2f}")
|
||||
|
||||
# ---------------------------------------------------------------- 5. la statistica con potenza
|
||||
print("\n[5] LA STATISTICA CON POTENZA — q, posizione del fill nella barra 5m\n" + "-" * 96)
|
||||
q = R["q"].dropna()
|
||||
dq = ci_mean(q)
|
||||
print(f" q = (fill-low)/(high-low) per i BUY, 1-quello per i SELL. E[q]=0.5 senza impatto.")
|
||||
print(f" media q {dq['mean']:.3f} IC95 [{dq['lo']:.3f}, {dq['hi']:.3f}] "
|
||||
f"(med {dq['med']:.3f}, n={dq['n']})")
|
||||
print(f" scostamento da 0.5: {dq['mean'] - 0.5:+.3f} (MDE dichiarata {mde_q:.3f})")
|
||||
outside = (R["in_bar"] == False).sum()
|
||||
print(f" fill FUORI dal range della propria barra: {outside}/{dq['n']} "
|
||||
f"(un fill fuori sarebbe strumento o ora sbagliati)")
|
||||
print(f" q=1.00 esatti (fill AL massimo = firma di impatto): {(q >= 0.999).sum()}/{dq['n']}"
|
||||
f" · q=0.00 esatti: {(q <= 0.001).sum()}/{dq['n']}")
|
||||
rng_med = float(R["bar_rng_bps"].median())
|
||||
print(f" range mediano di barra 5m: {rng_med:.1f} bps -> lo scostamento misurato vale")
|
||||
print(f" ~{(dq['mean'] - 0.5) * rng_med:+.2f} bps/lato "
|
||||
f"[IC95 {(dq['lo'] - 0.5) * rng_med:+.2f}, {(dq['hi'] - 0.5) * rng_med:+.2f}]")
|
||||
|
||||
# -- null EMPIRICO: "0.5" e' un'assunzione (uniforme). Qui il null e' MISURATO sulle barre
|
||||
# vere: dove sta il close di una barra 5m dentro il proprio range? Segno randomizzato
|
||||
# per imitare l'orientamento al lato.
|
||||
qc = []
|
||||
for a in sorted(fills["asset"].unique()):
|
||||
src = lin[a] if basis_ok else inv[a]
|
||||
m = src[src["high"] > src["low"]]
|
||||
qc.append(((m["close"] - m["low"]) / (m["high"] - m["low"])).to_numpy())
|
||||
qc = np.concatenate(qc)
|
||||
flip = RNG.random((N_BOOT, dq["n"])) < 0.5
|
||||
draw = RNG.choice(qc, size=(N_BOOT, dq["n"]), replace=True)
|
||||
null = np.where(flip, draw, 1.0 - draw).mean(axis=1)
|
||||
pval = float((null >= dq["mean"]).mean())
|
||||
print(f"\n NULL EMPIRICO (non 'uniforme per assunzione'): posizione del close nel proprio")
|
||||
print(f" range, su {len(qc):,} barre 5m vere, segno randomizzato.")
|
||||
print(f" null: media {null.mean():.3f} · sd {null.std():.3f} "
|
||||
f"(l'uniforme darebbe sd {(1 / np.sqrt(12)) / np.sqrt(dq['n']):.3f})")
|
||||
print(f" p(one-sided, impatto = q piu' alto del null) = {pval:.3f}"
|
||||
f" -> {'IMPATTO RILEVATO' if pval < 0.05 else 'nessun impatto rilevabile'}")
|
||||
|
||||
# -- argomento FISICO, indipendente dal campione: quanto siamo grandi e quanto vale un tick
|
||||
from src.live.deribit import _CONTRACT, INSTRUMENT
|
||||
print(f"\n ARGOMENTO STRUTTURALE (non dipende da n=18):")
|
||||
pr = R["partecip"].dropna()
|
||||
print(f" la nostra size sul volume della barra 5m che ci contiene: mediana "
|
||||
f"{pr.median() * 100:.2f}% · max {pr.max() * 100:.2f}%")
|
||||
for a in sorted(R["asset"].unique()):
|
||||
tick = _CONTRACT[INSTRUMENT[a]]["tick"]
|
||||
px = float(R[R["asset"] == a]["price"].median())
|
||||
print(f" {a}: tick {tick} su ~${px:,.0f} = {tick / px * 1e4:.4f} bps -> anche uno")
|
||||
print(f" spread di 5 tick costerebbe {2.5 * tick / px * 1e4:.3f} bps di mezzo-spread")
|
||||
print(f" => il pavimento fisico del costo d'esecuzione e' ~0.1 bps, DUE ordini di")
|
||||
print(f" grandezza sotto il margine di {MARGIN_BPS_SIDE:.1f} bps. Perche' lo slippage")
|
||||
print(f" sfondi il margine il libro dovrebbe CAMMINARE il book — e a <1% del nastro")
|
||||
print(f" con 0/18 fill al massimo di barra non c'e' traccia che lo faccia.")
|
||||
|
||||
# ---------------------------------------------------------------- 6. conversione
|
||||
print("\n[6] CONVERSIONE IN SHARPE/CAGR — curva PUBBLICATA, non ricalcolata\n" + "-" * 96)
|
||||
print(f" r0726_fee_sensitivity: {DSHARPE_PER_BPS_SIDE:+.3f} Sharpe/bps-lato · "
|
||||
f"{DCAGR_PER_BPS_SIDE:+.2f} pp CAGR/bps-lato (SKH01 ~4x piu' sensibile di TP01)")
|
||||
b5 = ci_mean(R["slip_B5"])
|
||||
qs_mean = (dq["mean"] - 0.5) * rng_med
|
||||
qs_lo, qs_hi = (dq["lo"] - 0.5) * rng_med, (dq["hi"] - 0.5) * rng_med
|
||||
for nm, m, lo, hi in (("REF-B5 (bps, senza potenza)", b5["mean"], b5["lo"], b5["hi"]),
|
||||
("q normalizzata (con potenza)", qs_mean, qs_lo, qs_hi)):
|
||||
print(f" {nm:<30} slip {m:+6.2f} bps -> dSharpe {DSHARPE_PER_BPS_SIDE * m:+.4f} "
|
||||
f"· dCAGR {DCAGR_PER_BPS_SIDE * m:+.3f} pp "
|
||||
f"[IC95 dSharpe {DSHARPE_PER_BPS_SIDE * hi:+.4f}..{DSHARPE_PER_BPS_SIDE * lo:+.4f}]")
|
||||
print(f"\n Margine gia' pagato dal modello: {MARGIN_BPS_SIDE:.1f} bps/lato "
|
||||
f"(backtest 10 bps RT - fee vera 7 bps RT).")
|
||||
print(f" Slippage con potenza (q) dentro il margine? "
|
||||
f"{'SI' if qs_hi <= MARGIN_BPS_SIDE else 'NON DIMOSTRATO'} "
|
||||
f"(estremo alto IC95 {qs_hi:+.2f} vs {MARGIN_BPS_SIDE:.1f})")
|
||||
|
||||
# ---------------------------------------------------------------- 7. taglia e ora
|
||||
print("\n[7] DIPENDENZA DA TAGLIA E ORA — il campione lo permette?\n" + "-" * 96)
|
||||
from scipy import stats
|
||||
sub = R.dropna(subset=["q"])
|
||||
rho, p = stats.spearmanr(sub["notional"], sub["q"])
|
||||
print(f" taglia ESEGUITA ${R['notional'].min():.0f}-${R['notional'].max():.0f} "
|
||||
f"(mediana ${R['notional'].median():.0f}). NB: e' l'INCREMENTO mandato al venue, non il")
|
||||
print(f" target di posizione (che arriva a $189): il libro riconcilia, non ri-apre.")
|
||||
print(f" Spearman(notional, q) = {rho:+.3f} (p={p:.2f})")
|
||||
big = sub[sub["notional"] >= sub["notional"].median()]["q"]
|
||||
small = sub[sub["notional"] < sub["notional"].median()]["q"]
|
||||
print(f" meta' grande n={len(big)} q={big.mean():.3f} · meta' piccola n={len(small)} "
|
||||
f"q={small.mean():.3f} · differenza {big.mean() - small.mean():+.3f} "
|
||||
f"(MDE su un confronto a meta' campione ~{2.80 * (1 / np.sqrt(12)) * np.sqrt(2 / (n / 2)):.3f})")
|
||||
hrs = sorted(int(h) for h in R["fill_ts"].dt.hour.unique())
|
||||
print(f" ore del giorno coperte: {hrs} ({len(hrs)} ore distinte su 24, "
|
||||
f"{R['fill_ts'].dt.hour.value_counts().max()} fill nell'ora piu' popolata)")
|
||||
print(f" => con n={n} e {len(hrs)} ore distinte, l'effetto ora NON e' stimabile: "
|
||||
f"lo dichiaro non misurato, non 'assente'.")
|
||||
|
||||
# ---------------------------------------------------------------- 8. netting
|
||||
print("\n[8] NETTING TP01/SKH01 — cosa NON e' misurabile da questi dati\n" + "-" * 96)
|
||||
print(f" Il libro netta TP01 e SKH01 su UNO strumento: ogni fill e' la RISULTANTE, mai")
|
||||
print(f" il trade di uno sleeve. Quindi lo slippage misurato e' quello del LIBRO, e non si")
|
||||
print(f" puo' attribuire a TP01 o a SKH01 — proprio mentre SKH01 e' ~4x piu' sensibile.")
|
||||
print(f" Il netting RIDUCE il turnover eseguito: {n} ordini in "
|
||||
f"{(fills['fill_ts'].max() - fills['fill_ts'].min()).days} giorni, "
|
||||
f"nozionale totale ${R['notional'].sum():,.0f}.")
|
||||
|
||||
print("\n" + "=" * 96)
|
||||
out = SCRATCH / "r0822_slip_fills.csv"
|
||||
SCRATCH.mkdir(parents=True, exist_ok=True)
|
||||
R.to_csv(out, index=False)
|
||||
print(f" dettaglio per fill: {out}")
|
||||
print("=" * 96)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -58,7 +58,8 @@ from r0725_statarb_basket_gate import basket_from_positions, pair_frames
|
||||
from r0725_statarb_demean_skeptic import ret_from_pos # noqa: E402
|
||||
|
||||
ANN = np.sqrt(365.0)
|
||||
DISCOVERY = pd.Timestamp("2026-07-25", tz="UTC") # ultima barra vista dalla scoperta
|
||||
DISCOVERY = pd.Timestamp("2026-07-25", tz="UTC") # ultima barra vista dalla scoperta (PROVVISORIA)
|
||||
CLOSED_AT_DISCOVERY = pd.Timestamp("2026-07-24", tz="UTC") # ultima barra CHIUSA quel giorno
|
||||
CRONLOG = ROOT / "logs" / "cron_daily.log"
|
||||
FWD = ROOT / "data" / "paper_xsr" / "returns.jsonl"
|
||||
FWD_STATE = ROOT / "data" / "paper_xsr" / "state.json"
|
||||
@@ -138,6 +139,8 @@ def t1_lenti():
|
||||
for tag, (Pi, Si) in [
|
||||
("OGGI pieno", (P, S)),
|
||||
(f"tronc. {DISCOVERY.date()} (barra completa)", (_cut(P, DISCOVERY), _cut(S, DISCOVERY))),
|
||||
("tronc. 2026-07-24 = ultima barra CHIUSA il 25/07",
|
||||
(_cut(P, CLOSED_AT_DISCOVERY), _cut(S, CLOSED_AT_DISCOVERY))),
|
||||
]:
|
||||
rows.append((f"L1 gate {tag}", _stats(lens_L1(Pi, Si))))
|
||||
rows.append((f"L2 scett. {tag}", _stats(lens_L2(Pi, Si))))
|
||||
@@ -337,8 +340,7 @@ def t3_monitor():
|
||||
print(" non entrano in nessun rendimento registrato.")
|
||||
sh_a = float(a.mean() / a.std() * ANN)
|
||||
sh_b = float(b.mean() / b.std() * ANN)
|
||||
se = float(ANN / np.sqrt(len(a)) / np.sqrt(365) * np.sqrt(365))
|
||||
se = float(np.sqrt(365.0 / len(a)))
|
||||
se = float(np.sqrt(365.0 / len(a))) # SE di uno Sharpe annualizzato su n barre giornaliere
|
||||
print(f"\n Sharpe della finestra forward: REGISTRATO {sh_a:+.2f} RICALCOLATO {sh_b:+.2f}")
|
||||
print(f" ⚠ NESSUNO DEI DUE E' UNA LETTURA DEL GATE. Con {len(a)} barre l'errore standard di")
|
||||
print(f" uno Sharpe annualizzato e' ~{se:.2f}: entrambi i numeri sono indistinguibili da 0 e")
|
||||
|
||||
Reference in New Issue
Block a user