From ea22a0e20c920d2371cb4a1f21255fa778383f6a Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:29:19 +0000 Subject: [PATCH] =?UTF-8?q?research(wave-0822):=20XSR-REPRO=20trova=20un?= =?UTF-8?q?=20difetto=20di=20produzione=20(i=20monitor=20forward=20registr?= =?UTF-8?q?ano=20~41=20min/giorno);=20SKEW=20scompone=20il=20f=20di=20VRP0?= =?UTF-8?q?1=20=E2=80=94=20il=2042%=20e'=20struttura=20a=20termine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 80 ++++ scripts/research/r0822_alt_options.py | 530 +++++++++++++++++++++ scripts/research/r0822_basis_calendar.py | 106 +++-- scripts/research/r0822_hl_exec.py | 83 +++- scripts/research/r0822_leverage_skeptic.py | 175 ++++++- scripts/research/r0822_prop_alloc.py | 44 +- scripts/research/r0822_skew.py | 3 - scripts/research/r0822_slip_audit.py | 509 ++++++++++++++++++++ scripts/research/r0822_xsr_repro.py | 8 +- 9 files changed, 1445 insertions(+), 93 deletions(-) create mode 100644 scripts/research/r0822_alt_options.py create mode 100644 scripts/research/r0822_slip_audit.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 1ecf1b1..e13f37b 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -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). diff --git a/scripts/research/r0822_alt_options.py b/scripts/research/r0822_alt_options.py new file mode 100644 index 0000000..2cad5af --- /dev/null +++ b/scripts/research/r0822_alt_options.py @@ -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()) diff --git a/scripts/research/r0822_basis_calendar.py b/scripts/research/r0822_basis_calendar.py index a3199b1..3997af5 100644 --- a/scripts/research/r0822_basis_calendar.py +++ b/scripts/research/r0822_basis_calendar.py @@ -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}") diff --git a/scripts/research/r0822_hl_exec.py b/scripts/research/r0822_hl_exec.py index 28a2587..8eede86 100644 --- a/scripts/research/r0822_hl_exec.py +++ b/scripts/research/r0822_hl_exec.py @@ -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) diff --git a/scripts/research/r0822_leverage_skeptic.py b/scripts/research/r0822_leverage_skeptic.py index 65ce9b3..963730a 100644 --- a/scripts/research/r0822_leverage_skeptic.py +++ b/scripts/research/r0822_leverage_skeptic.py @@ -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.") diff --git a/scripts/research/r0822_prop_alloc.py b/scripts/research/r0822_prop_alloc.py index cb2813c..603b451 100644 --- a/scripts/research/r0822_prop_alloc.py +++ b/scripts/research/r0822_prop_alloc.py @@ -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") diff --git a/scripts/research/r0822_skew.py b/scripts/research/r0822_skew.py index 37b2dec..206c258 100644 --- a/scripts/research/r0822_skew.py +++ b/scripts/research/r0822_skew.py @@ -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"] diff --git a/scripts/research/r0822_slip_audit.py b/scripts/research/r0822_slip_audit.py new file mode 100644 index 0000000..c51939c --- /dev/null +++ b/scripts/research/r0822_slip_audit.py @@ -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` (`===== 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() diff --git a/scripts/research/r0822_xsr_repro.py b/scripts/research/r0822_xsr_repro.py index c387ae0..134318f 100644 --- a/scripts/research/r0822_xsr_repro.py +++ b/scripts/research/r0822_xsr_repro.py @@ -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")