From 13daccf6de11f5cca9031da0a0e222b71262c4ed Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 01:19:04 +0000 Subject: [PATCH] =?UTF-8?q?research(=C2=A745):=20SPOT-NETTING=20=E2=80=94?= =?UTF-8?q?=20il=20margine=20non=20uccide=20il=20lead,=20il=20CODICE=20si'?= =?UTF-8?q?;=20e=20separare=20le=20gambe=20non=20aggiunge=20niente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filone §45: quanto vale, e quanto costa, spostare TP01 sullo SPOT Deribit separandolo da SKH01 (oggi nettati su un unico strumento). Solo misura, nessun file di produzione toccato, nessun ordine. Q1 MARGINE — il venue NON blocca: BTC/ETH sono `in_cross_collateral_pool: true` [VENUE], lo spot ha `max_leverage: 10` (vive dentro il conto marginato, non in un wallet a parte) [VENUE], e il margine iniziale misurato sul conto reale e' il 2,00% del nozionale [CONTO: equity-available su 2 posizioni] -> nel caso peggiore la gamba SKH01 resterebbe marginata 50x dal solo USDC libero, a QUALUNQUE capitale (il rapporto e' invariante in E). L'haircut non e' leggibile e non e' binding. A bloccare e' il CODICE: `shadow._equity` legge solo `account_summary('USDC')` e `position_usd` matcha per instrument_name -> comprando spot il libro leggerebbe il 25% del conto vero e non vedrebbe la gamba. Non e' un cambio di strumento, e' un cambio del percorso di sizing del live. Q2 VALORE — T1 riprodotto (+0,051/+0,057 contro `hourly`, pubblicato +0,054/+0,061) MA contro `fastdetect`, che il 26/07 stabili' essere il path del live vero, vale -0,048 FULL (6/23 offset): sbloccarlo e' un DECLASSAMENTO. Il netting perso non costa commissioni (-0,216% di equity/anno: e' un risparmio) ne' ordini (-6,3/anno): costa tracking (+12%) e spread ([0,139%,0,250%]/anno). I due si compensano quasi. Il valore vero e' il funding e basta: +1,58%/anno lordo di drift di libro. Q3 — `fee_watch` DERIVA i suoi strumenti da `src.live.book.INSTRUMENT`: una gamba spot sarebbe invisibile (stesso difetto corretto il 21/08 sugli inverse). Riga dichiarata, non scritta. `convenzione('BTC_USDC')` = 'ignota' -> servirebbe anche una terza famiglia o il cross-check sui fill resta muto. Q4 — la liquidazione MIGLIORA (il 75% del libro esce dal perimetro; uno spot non si liquida) ma la gamba TP01 resterebbe senza disaster-SL; il fisco PEGGIORA di 0,83%/anno se i derivati sono `c-quater`, e i comparti si SEPARANO (le minusvalenze del perp smetterebbero di compensare le plusvalenze spot): costo nuovo, mai contato. IPOTESI MIA NATA E REFUTATA: «convertire USDC in spot rinuncia all'interesse». Il venue dichiara USDC `apr` 3,40; sul log del cron, 6 run flat (il piu' lungo 257 giri = 10,7 giorni) mostrano equity INVARIATA al centesimo contro i +$0,59 attesi -> limite superiore su qualsiasi interesse < 0,029%/anno. L'obiezione e' morta. ⚠️ Due istantanee del libro a 4 minuti danno differenziali di spread 2,27 e 4,08 bps (+79%) e il contributo netto dell'esecuzione CAMBIA SEGNO fra le due: e' dichiarato come banda, non come punto. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf --- scripts/research/r0823_spot_netting.py | 1004 ++++++++++++++++++++++++ 1 file changed, 1004 insertions(+) create mode 100644 scripts/research/r0823_spot_netting.py diff --git a/scripts/research/r0823_spot_netting.py b/scripts/research/r0823_spot_netting.py new file mode 100644 index 0000000..81407e2 --- /dev/null +++ b/scripts/research/r0823_spot_netting.py @@ -0,0 +1,1004 @@ +#!/usr/bin/env python3 +"""r0823_spot_netting.py — SPOT-NETTING (§45): quanto vale, e quanto COSTA, separare TP01 e +SKH01 su due strumenti diversi? + +CONTESTO (§39, `scripts/research/r0822e_funding_avoid.py`, 2026-08-22) + Il libro paga -2,16%/anno di funding. Lo strumento a carry ZERO esiste ed e' lo SPOT Deribit + (`BTC_USDC`, `ETH_USDC`; taker 0,0 oggi, min 0,0001). La variante «E) TP01 su SPOT + SKH01 su + perp» dava +1,55%/anno di drift a volatilita' identica e muro -10,9%. E' rimasta un LEAD per + tre ragioni dichiarate: (a) SKH01 e' long/short e sullo spot non e' esprimibile; (b) la fee 0 + e' dichiarata temporanea e `fee_watch` non sorveglia lo spot; (c) la domanda fiscale aperta + (derivati `c-quater` 26% contro cripto `c-sexies` 33%) vale piu' del risparmio. + +LA DOMANDA DI QUESTO FILONE — la conseguenza di SECONDO ORDINE che nessuno ha misurato. + Oggi TP01 e SKH01 tradano lo STESSO strumento e Deribit tiene UNA sola posizione netta. E' + questo netting che il 2026-07-26 rese «non implementabile» la raccomandazione T1 (TP di SKH01 + come limit resting reduce-only, misurato +0,054 Sharpe FULL / +0,061 HOLD di libro): un ordine + al livello di SKH chiuderebbe anche la quota di TP01. + Spostare TP01 sullo spot SEPARA le due gambe. Quindi: + + Q1 IL MARGINE lo spot comprato e' collaterale del perp, o immobilizza capitale? + (puo' UCCIDERE il lead: TP01 al 75% lascerebbe il 25% a marginare SKH01) + Q2 IL VALORE T1 sbloccato + funding risparmiato - COSTO DEL NETTING PERSO + Q3 LA SORVEGLIANZA `fee_watch` DERIVA i suoi strumenti dal libro: una gamba spot sarebbe + invisibile. Quale riga andrebbe aggiunta, e cosa succede a fee != 0. + Q4 IL RISCHIO detenzione cripto invece di derivato: venue, fisco, liquidazione. + +ONESTA' — ogni numero di questo script porta la sua FONTE, in chiaro: + [VENUE] letto da un endpoint pubblico Deribit in questa corsa + [CONTO] letto dal percorso di SOLA LETTURA gia' nel repo (src.live.deribit.DeribitRead) + [LOG] misurato su logs/cron_book.log o data/live/book_executions.jsonl + [CODICE] verificato sul sorgente di produzione (inspect.getsource, non a memoria) + [CALC] calcolato qui da dati misurati + [DEDOTTO] conclusione di un ragionamento su numeri misurati + [ASSUNTO] assunzione dichiarata, non misurata + Cio' che non e' determinabile senza un conto reale e' elencato in sezione 1g e NON e' stato + dedotto ne' assunto: e' dichiarato ignoto. + +VINCOLI RISPETTATI + * SOLA LETTURA: nessun file di produzione toccato (src/, config/, scripts/live/, tests/, + data/paper_*, data/live). L'unica scrittura e' la cache in scratchpad. + * NESSUN ORDINE, di nessun tipo. Il percorso del conto usato (`DeribitRead`) non ha, by + design, alcun metodo di trading. + * Rete: solo endpoint PUBBLICI di lettura Deribit, <=2 req/s, e ASTENSIONE nelle finestre + :05-:10 e :24-:30 (cron_book al :07, collect_chain al :25) -> in quelle finestre si usa + solo la cache e lo si dichiara. + +USO + nice -n 19 timeout 900 uv run python scripts/research/r0823_spot_netting.py + ... --no-net nessuna richiesta: solo cache (e conto non letto) +""" +from __future__ import annotations + +import contextlib +import datetime as dt +import inspect +import io +import json +import re +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in (ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt", ROOT): + sys.path.insert(0, str(_p)) + +NO_NET = "--no-net" in sys.argv +# r0822d_funding legge `--no-net` da sys.argv AL MOMENTO DELL'IMPORT: la sua cache del funding +# esiste gia' (4 parquet in scratchpad/fund) e questo script non deve ri-scaricarla. +if "--no-net" not in sys.argv: + sys.argv.append("--no-net") + +import altlib as al # noqa: E402 +import r0702_anchor_skh01 as r02 # noqa: E402 +import r0726_skh_onbook as OB # noqa: E402 +import r0822d_funding as FD # noqa: E402 +from src.backtest.harness import load # noqa: E402 +from src.data.downloader import load_data # noqa: E402 +from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d # noqa: E402 + +T0 = time.time() +ASSETS = ("BTC", "ETH") +API = "https://www.deribit.com/api/v2/public/" +_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/spotnet") +SCRATCH = _SP if _SP.parent.exists() else Path("/tmp/pythagoras_spotnet") +SCRATCH.mkdir(parents=True, exist_ok=True) + +W_TP, W_SKH = 0.75, 0.25 # pesi del libro Deribit (src/live/book.py) +WEIGHT = 0.5 # 50/50 BTC+ETH (src/live/shadow.py) +FEE_PERP_SIDE = 0.00035 # 3,50 bps/lato, MISURATO sul nastro (§ondata 22/08) e da fee_watch +HOLDOUT = "2025-01-01" + +_NET_CALLS: list[str] = [] + + +def sez(t: str) -> None: + print("\n" + "=" * 100) + print(f" {t}") + print("=" * 100) + + +def clock() -> str: + return f"[{time.time()-T0:6.1f}s]" + + +# =========================================================================== +# 0) RETE — pacing, finestre vietate, cache +# =========================================================================== +def finestra_vietata(now: dt.datetime | None = None) -> bool: + """True nelle finestre in cui la VPS parla gia' con Deribit per conto della produzione: + :05-:10 (cron_book al :07, feed 5m di SKH01) e :24-:30 (collect_chain al :25). + PURA. Non e' cortesia: il 29/07 il rate-limit per-IP saturato da un altro processo ha + portato la latenza d'uscita di SKH01 da ~1h a ~11h.""" + now = now or dt.datetime.now(dt.timezone.utc) + m = now.minute + return (5 <= m <= 10) or (24 <= m <= 30) + + +_last = [0.0] + + +def get(path: str, key: str) -> dict | None: + """GET pubblica con cache su disco, pacing <=2 req/s e astensione nelle finestre vietate.""" + cache = SCRATCH / f"{key}.json" + if cache.exists(): + try: + return json.loads(cache.read_text()) + except Exception: + pass + if NO_NET: + return None + for _ in range(60): + if not finestra_vietata(): + break + time.sleep(10) + else: + print(f" ⚠️ finestra vietata persistente: {key} NON letto (nessuna cache)") + return None + d = time.time() - _last[0] + if d < 0.5: + time.sleep(0.5 - d) + _last[0] = time.time() + try: + with urllib.request.urlopen(API + path, timeout=25) as r: + res = json.loads(r.read()).get("result") + except Exception as e: # noqa: BLE001 + print(f" ⚠️ {key}: {type(e).__name__} -> non letto (non e' 'uguale', e' 'non so')") + return None + _NET_CALLS.append(path.split("?")[0]) + cache.write_text(json.dumps(res)) + return res + + +def conto() -> dict | None: + """Stato del conto dal percorso di SOLA LETTURA gia' nel repo. Nessun metodo di trading + esiste in DeribitRead (by design). Cache su disco: una corsa successiva non ri-interroga.""" + cache = SCRATCH / "conto.json" + if cache.exists(): + try: + return json.loads(cache.read_text()) + except Exception: + pass + if NO_NET or finestra_vietata(): + return None + try: + from src.live.deribit import INSTRUMENT, DeribitRead + c = DeribitRead() + out = {"summary": {k: c.account_summary(k) for k in ("USDC", "BTC", "ETH")}, + "positions": c.positions("USDC"), + "instruments": dict(INSTRUMENT)} + _NET_CALLS.append("cerbero-mcp/get_account_summary x3 + get_positions x1 (SOLA LETTURA)") + cache.write_text(json.dumps(out, default=str)) + return out + except Exception as e: # noqa: BLE001 + print(f" ⚠️ conto non letto: {type(e).__name__}: {e}") + return None + + +# =========================================================================== +# 1) Q1 — IL MARGINE (la domanda che puo' uccidere il lead) +# =========================================================================== +def q1() -> dict: + sez("Q1 — IL MARGINE: lo spot e' collaterale del perp, o immobilizza capitale?") + + cur = get("get_currencies", "currencies") + spot = get("get_instruments?currency=USDC&kind=spot", "inst_spot") + perp = get("get_instruments?currency=USDC&kind=future", "inst_perp") + acc = conto() + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + + # ---------------------------------------------------------------- 1a) il pool + print("\n 1a) [VENUE] public/get_currencies — chi sta nel POOL DI CROSS-COLLATERAL") + pool = {} + if cur: + for c in cur: + if c["currency"] in ("BTC", "ETH", "USDC", "USDT"): + pool[c["currency"]] = (bool(c.get("in_cross_collateral_pool")), float(c.get("apr") or 0)) + for k, (p, a) in pool.items(): + print(f" {k:<6} in_cross_collateral_pool = {str(p):<5} apr = {a:.2f}") + print(f" (nel pool ci sono {sum(1 for c in cur if c.get('in_cross_collateral_pool'))} " + f"valute su {len(cur)}: e' un elenco RISTRETTO, non un default)") + print(" → [DEDOTTO] BTC ed ETH NON sono un portafoglio separato: il venue li") + print(" dichiara collaterale. Il campo dice CHE contano, non QUANTO (haircut).") + else: + print(" non letto") + + # ---------------------------------------------------------------- 1b) lo strumento + print("\n 1b) [VENUE] public/get_instruments — lo SPOT e' dentro il sistema di margine") + spec = {} + for src, kind in ((spot, "spot"), (perp, "future")): + for x in (src or []): + if x["instrument_name"] in ("BTC_USDC", "ETH_USDC", + "BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"): + spec[x["instrument_name"]] = x + print(f" {'strumento':<22} {'kind':>7} {'maxlev':>7} {'taker bps':>10} {'maker bps':>10} " + f"{'min lotto':>10} {'tick':>7}") + for k in ("BTC_USDC", "BTC_USDC-PERPETUAL", "ETH_USDC", "ETH_USDC-PERPETUAL"): + s = spec.get(k) + if not s: + print(f" {k:<22} non letto") + continue + print(f" {k:<22} {s['kind']:>7} {s.get('max_leverage'):>7} " + f"{float(s['taker_commission'])*1e4:>10.2f} {float(s['maker_commission'])*1e4:>10.2f} " + f"{s['min_trade_amount']:>10} {s['tick_size']:>7}") + if spec.get("BTC_USDC", {}).get("max_leverage"): + print(f" → [DEDOTTO] lo spot ha `max_leverage` = {spec['BTC_USDC']['max_leverage']}: si puo'") + print(" comprare a leva. Un portafoglio segregato non ha una leva massima → lo spot") + print(" Deribit vive DENTRO il conto marginato, non in un wallet a parte.") + + # ---------------------------------------------------------------- 1c) il margine misurato + print("\n 1c) [CONTO] il margine iniziale del perp, MISURATO sul conto reale") + im_rate = None + E = None + if acc: + u = acc["summary"]["USDC"] + E = float(u["equity"]) + av = float(u["available_funds"]) + gross = sum(abs(float(p["size"])) for p in acc["positions"]) + print(f" equity USDC ${E:,.2f}") + print(f" balance USDC ${float(u['balance']):,.2f}") + print(f" available_funds ${av:,.2f}") + print(f" nozionale lordo ${gross:,.2f} ({len(acc['positions'])} posizioni perp)") + if gross > 0: + im_rate = (E - av) / gross + print(f" → margine impegnato ${E-av:,.2f} = {im_rate:.3%} del nozionale [CALC]") + print(" ⚠️ `equity - available_funds` contiene TUTTO cio' che e' riservato (margine") + print(" iniziale + eventuali riserve d'ordine): e' un LIMITE SUPERIORE dell'IM,") + print(" quindi l'aritmetica di 1d che ne segue e' conservativa.") + for p in acc["positions"]: + print(f" {p['instrument']:<22} {p['direction']:>5} ${float(p['size']):>8,.2f} " + f"leverage={p.get('leverage')}") + else: + print(" conto non letto (finestra vietata, --no-net o cache assente)") + + # ---------------------------------------------------------------- 1d) l'aritmetica + print("\n 1d) [CALC] L'ARITMETICA CHE DECIDE — quanto margine servirebbe davvero a SKH01") + Eu = E if E else 635.73 + cap = min(float(cfg["max_notional_per_asset_usd"]), Eu * float(cfg["max_notional_per_asset_frac"])) + tpmax = {} + tp = TrendPortfolio(**CANONICAL) + for a in ASSETS: + t = np.nan_to_num(np.asarray(tp.target_series(resample_1d(load_data(a, "1h"))), float)) + tpmax[a] = (float(t.max()), float((t > 1.0).mean())) + print(f" equity assunta ${Eu:,.2f} [CONTO] · cap/asset ${cap:,.0f} [CODICE config/live.json]") + print(f" target massimo storico di TP01: BTC {tpmax['BTC'][0]:.3f} · ETH {tpmax['ETH'][0]:.3f} " + f"(> 1,0x nel {np.mean([tpmax[a][1] for a in ASSETS]):.3%} dei giorni)") + tp_gross = sum(min(cap, WEIGHT * Eu * W_TP * min(tpmax[a][0], 2.0)) for a in ASSETS) + tp_gross_1x = sum(min(cap, WEIGHT * Eu * W_TP * 1.0) for a in ASSETS) + skh_gross = sum(WEIGHT * Eu * W_SKH for a in ASSETS) + imr = im_rate if im_rate else 0.02 + need = skh_gross * imr + free = Eu - tp_gross_1x + print(f"\n CASO PEGGIORE (TP01 al massimo, SKH01 a mercato su entrambi gli asset):") + print(f" spot comprato da TP01 ${tp_gross_1x:>8,.2f} ({tp_gross_1x/Eu:.0%} del conto)") + print(f" USDC che resta libero ${free:>8,.2f}") + print(f" nozionale perp di SKH01 ${skh_gross:>8,.2f}") + print(f" margine che richiede ${need:>8,.2f} (a {imr:.2%} misurato in 1c)") + print(f" → copertura {free/need:>8,.1f}x") + print(" 📌 [DEDOTTO] Il rapporto NON dipende dal capitale: entrambi i termini sono") + print(" proporzionali a E. Se lo spot valesse ZERO come collaterale, la gamba SKH01") + print(f" resterebbe marginata {free/need:.0f} volte. **Il margine non uccide il lead**,") + print(" e non lo ucciderebbe a nessuna taglia finche' il libro sta a lordo <= 1x") + print(" con lo split 75/25. L'haircut, che non e' leggibile, non e' binding.") + print(f" ⚠️ Perche' il lordo resta <= 1x: cap = equity x {cfg['max_notional_per_asset_frac']}") + print(f" su {len(ASSETS)} asset (guardia `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x`).") + print(" Se un giorno esistesse la chiave di SCALA (SPEC-scale-key, §38), questa") + print(" copertura si dividerebbe per k — a k=1,25x resterebbe comunque ~40x.") + + # ---------------------------------------------------------------- 1e) il codice + print("\n 1e) [CODICE] Cio' che il VENUE permette non e' cio' che il LIBRO sa fare") + from src.live import shadow as SH + src_eq = inspect.getsource(SH._equity) + solo_usdc = 'account_summary("USDC")' in src_eq and "if eq > 1:" in src_eq + from src.live.deribit import DeribitRead as DR + src_pos = inspect.getsource(DR.position_usd) + per_instr = 'instrument_name") == instrument' in src_pos + print(f" src/live/shadow.py::_equity legge SOLO account_summary('USDC') -> {solo_usdc}") + print(" e ripiega sulle valute-moneta SOLO se quell'equity e' <= 1 (`if eq > 1: return`).") + print(f" src/live/deribit.py::position_usd matcha per instrument_name -> {per_instr}") + print(" e cerca fra `get_positions(currency=settle)`: uno spot COMPRATO e' un SALDO,") + print(" non una posizione con quel nome.") + print(" → [DEDOTTO] CONSEGUENZA MISURABILE, e non e' piccola:") + print(f" comprando ${tp_gross_1x:,.0f} di spot, l'equity che il libro LEGGE passerebbe da") + print(f" ${Eu:,.0f} a ${free:,.0f} ({free/Eu:.0%}) → il sizing di TUTTO il libro si") + print(f" ridurrebbe al {free/Eu:.0%}, e la gamba spot non comparirebbe fra le posizioni") + print(" → il riconciliatore la ricomprerebbe. **Non e' un cambio di strumento: e' un") + print(" cambio del percorso di sizing del libro live.**") + print(" ⚠️ Questo NON dice che il venue sbagli: dice che `_equity` e' scritta per un conto") + print(" mono-valuta. Il rimedio (sommare i saldi moneta x mark) e' poche righe — ma sono") + print(" righe sul percorso con soldi veri, e non e' questo filone a scriverle.") + + # ---------------------------------------------------------------- 1f) il rendimento dell'USDC + print("\n 1f) IPOTESI MIA, NATA E REFUTATA QUI: «convertire USDC in spot rinuncia a un interesse»") + apr = pool.get("USDC", (None, None))[1] + print(f" [VENUE] get_currencies dichiara USDC `apr` = {apr} (USDT 0,0 · BTC 0,0 · ETH 0,0)") + print(" Se quel tasso fosse accreditato a questo conto, mettere il 75% del capitale in") + print(f" spot BTC/ETH (apr 0,0) costerebbe fino a {(apr or 0)*0.75:.2f}%/anno = meta' del") + print(" vantaggio di §39. Va MISURATO, non assunto.") + runs = flat_runs() + if runs: + print(f"\n [LOG] logs/cron_book.log — periodi in cui il conto e' stato FLAT (nessuna") + print(" posizione: equity = balance, e l'unica cosa che potrebbe muoverla e' l'interesse)") + print(f" {'da':>20} {'a':>20} {'giri':>6} {'giorni':>7} {'equity':>18} {'atteso a apr':>13}") + for r0, r1, n, days, e0, e1 in runs[:6]: + att = e0 * (apr or 0) / 100 * days / 365.25 + print(f" {r0:>20} {r1:>20} {n:>6} {days:>7.1f} " + f"${e0:>8.2f} -> ${e1:>7.2f} {att:>+13.3f}") + best = max(runs, key=lambda r: r[3]) + att = best[4] * (apr or 0) / 100 * best[3] / 365.25 + print(f"\n ❌ REFUTATA: sul run piu' lungo ({best[3]:.1f} giorni) l'equity si e' mossa di") + print(f" ${best[5]-best[4]:+.2f} contro i ${att:+.2f} che {apr:.2f}%/anno avrebbe prodotto.") + bound = 0.005 / best[4] * 365.25 / best[3] * 100 + print(f" La stampa e' a 2 decimali → limite superiore su QUALSIASI interesse:") + print(f" < {bound:.3f}%/anno. **L'USDC di questo conto non rende nulla.**") + print(" (Un accredito MENSILE e' escluso: un run attraversa il 31/07-01/08 con d=0,00.)") + print(" 📌 Il campo `apr` e' letto dal venue ma la sua SEMANTICA non e' documentata") + print(" nella risposta: sui token a rendimento (STETH 2,24 · BUIDL 3,49 · USDE 4,00)") + print(" descrive il rendimento dell'ASSET. Su USDC descrive qualcosa che questo") + print(" conto non riceve. → l'obiezione del rendimento perso e' MORTA, e il lead") + print(" non deve pagarla.") + + # ---------------------------------------------------------------- 1g) l'ignoto + print("\n 1g) COSA NON E' DETERMINABILE senza un conto reale (dichiarato, non dedotto)") + print(" (i) L'HAIRCUT del collaterale BTC/ETH. Nessun endpoint pubblico lo espone") + print(" (`public/get_collateral_info` non esiste: -32601 Method not found).") + print(" → non binding per 1d, ma resta ignoto.") + print(" (ii) Se `account_summary('USDC').equity` includa il valore del saldo BTC/ETH.") + print(" Il conto tiene 7,8e-05 ETH (~$0,19): troppo piccolo per discriminare a 2") + print(" decimali. Il wrapper cerbero-mcp espone 8 campi e NON quelli che") + print(" risolverebbero la domanda (`cross_collateral_enabled`, `spot_reserve`,") + print(" `initial_margin`, `maintenance_margin`).") + print(" (iii) Se `get_positions` restituisca lo spot come posizione. Il wrapper non") + print(" passa `kind` e oggi non c'e' spot da vedere.") + print(" → Le tre si risolvono con UN acquisto di prova da $7,73 (lotto minimo BTC_USDC)") + print(" leggendo equity/available/positions prima e dopo. **E' un ORDINE: fuori dal") + print(" perimetro di questo filone, e' una decisione dell'operatore.**") + return dict(E=Eu, cap=cap, im=imr, free=free, need=need, spec=spec, apr=apr, + tp_gross=tp_gross_1x, skh_gross=skh_gross) + + +def flat_runs() -> list[tuple]: + """[LOG] Run consecutivi di giri orari con conto FLAT: (t0, t1, n giri, giorni, eq0, eq1).""" + p = ROOT / "logs" / "cron_book.log" + if not p.exists(): + return [] + blocks = re.split(r"===== (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) cron_book =====", p.read_text()) + rows = [] + for i in range(1, len(blocks), 2): + m = re.search(r"conto reale\s*:\s*\$([\d.,]+)", blocks[i + 1]) + pos = [float(x.replace(",", "")) for x in re.findall(r"pos \$([+-][\d,.]+)", blocks[i + 1])] + if m and pos and all(abs(x) < 1 for x in pos): + rows.append((blocks[i], float(m.group(1).replace(",", "")))) + out, cur, prev = [], [], None + for ts, eq in rows: + t = dt.datetime.strptime(ts, "%Y-%m-%dT%H:%M:%SZ") + if prev and (t - prev).total_seconds() <= 3900: + cur.append((ts, eq)) + else: + if len(cur) > 24: + out.append(cur) + cur = [(ts, eq)] + prev = t + if len(cur) > 24: + out.append(cur) + res = [] + for run in out: + t0 = dt.datetime.strptime(run[0][0], "%Y-%m-%dT%H:%M:%SZ") + t1 = dt.datetime.strptime(run[-1][0], "%Y-%m-%dT%H:%M:%SZ") + res.append((run[0][0], run[-1][0], len(run), (t1 - t0).total_seconds() / 86400, + run[0][1], run[-1][1])) + res.sort(key=lambda r: -r[3]) + return res + + +# =========================================================================== +# 2) Q2 — IL VALORE DELLA SEPARAZIONE +# =========================================================================== +def q2a() -> dict: + """T1 sbloccato — RIPRODOTTO, non citato a memoria.""" + sez("Q2 (i) — T1 SBLOCCATO: il numero pubblicato riprodotto, e la lente giusta") + print(" Il 26/07 T1 valeva +0,054 Sharpe FULL / +0,061 HOLD di libro (mediana APPAIATA sui") + print(" 23 offset, `onbook_tp` contro `hourly`) ed era bloccato dal netting. Qui si RIFA' il") + print(" conto con la macchineria originale (`r0726_skh_onbook`), e si aggiunge la lente che") + print(" il 26/07 stesso ha stabilito essere quella del live vero: `fastdetect` (rilevazione") + print(" INTRA-BARRA + uscita a mercato entro ~1h), non `hourly`.\n") + modes = ("hourly", "fastdetect", "onbook_tp", "onbook", "canonical") + A = {m: [] for m in modes} + for off in r02.OFFSETS: + for m in modes: + s, _ = OB.skh_series(off, m) + A[m].append(OB.sh3(OB.book(s, W_SKH))) + A = {m: np.array(v) for m, v in A.items()} + print(f" {'modalita':<44} {'FULL med':>9} {'HOLD med':>9} {'DD med':>8}") + for m in modes: + med = np.median(A[m], axis=0) + print(f" {OB.LABEL[m]:<44} {med[0]:>9.3f} {med[2]:>9.3f} {med[3]:>7.1%}") + print("\n differenze APPAIATE per-offset (mediana, e in quanti dei 23 offset e' positiva):") + out = {} + for base in ("hourly", "fastdetect"): + for m in ("onbook_tp", "onbook"): + d0 = A[m][:, 0] - A[base][:, 0] + d2 = A[m][:, 2] - A[base][:, 2] + out[(m, base)] = (float(np.median(d0)), int((d0 > 0).sum()), + float(np.median(d2)), int((d2 > 0).sum())) + print(f" {m:>10} - {base:<11} dFULL {np.median(d0):>+7.3f} ({int((d0>0).sum()):>2}/23)" + f" dHOLD {np.median(d2):>+7.3f} ({int((d2>0).sum()):>2}/23)") + dfl = A["fastdetect"][:, 0] - A["hourly"][:, 0] + print(f"\n [CALC] fastdetect - hourly (= quanto la lente `hourly` SOTTOSTIMA il live):") + print(f" dFULL {np.median(dfl):>+.3f} in {int((dfl>0).sum())}/23 offset " + f"(il 26/07 pubblico' +0,081)") + a, b = out[("onbook_tp", "hourly")], out[("onbook_tp", "fastdetect")] + print(f"\n 📌 IL PUNTO: il numero pubblicato (+{a[0]:.3f} FULL / +{a[2]:.3f} HOLD contro `hourly`)") + print(f" e' RIPRODOTTO. Ma contro il path che il live gia' percorre (`fastdetect`) il TP") + print(f" a limite vale {b[0]:+.3f} FULL ({b[1]}/23) e {b[2]:+.3f} HOLD ({b[3]}/23).") + if b[0] <= 0: + print(" → **Sbloccare T1 non compra niente: lo comprerebbe a un path PEGGIORE di quello") + print(" che gira.** E' esattamente la conclusione del 26/07 («il fix richiesto sarebbe") + print(" un DECLASSAMENTO»), qui ri-misurata sulla banda appaiata invece che a off 0.") + else: + print(" → il TP a limite aggiungerebbe ancora qualcosa sopra il live: taglia sopra.") + return out + + +def q2b() -> dict: + """Il funding risparmiato dalla sola gamba TP01.""" + sez("Q2 (ii) — IL FUNDING RISPARMIATO dalla gamba TP01 (riprodotto da §35/§39)") + with contextlib.redirect_stdout(io.StringIO()): + F = FD.sezione0()["F"] + rate = FD.build_rate(F) + alloc = 600.0 + s_perp, dg = FD.tp01_realistic(alloc * W_TP, rate) + s_spot, _ = FD.tp01_realistic(alloc * W_TP, None) + a, b = FD.stat(s_spot), FD.stat(s_perp) + pagato = float(np.mean([dg[x]["fund"].mean() for x in ASSETS])) * 365.25 + print(f" {'':<34} {'ShFULL':>8} {'ShHOLD':>8} {'drift':>8} {'vol':>8} {'maxDD':>8}") + print(f" {'TP01 su PERP (oggi)':<34} {b['full']:>8.3f} {b['hold']:>8.3f} {b['drift']:>8.2%} " + f"{b['vol']:>8.2%} {b['dd']:>8.1%}") + print(f" {'TP01 senza carry (= SPOT)':<34} {a['full']:>8.3f} {a['hold']:>8.3f} {a['drift']:>8.2%} " + f"{a['vol']:>8.2%} {a['dd']:>8.1%}") + print(f" {'DELTA':<34} {a['full']-b['full']:>+8.3f} {a['hold']-b['hold']:>+8.3f} " + f"{a['drift']-b['drift']:>+8.2%} {a['vol']-b['vol']:>+8.2%} {a['dd']-b['dd']:>+8.1%}") + print(f"\n funding pagato dalla gamba TP01: {pagato:+.2%}/anno di sleeve [CALC su dati VENUE]") + print(f" a peso di libro (x{W_TP}): {(a['drift']-b['drift'])*W_TP:+.2%}/anno di drift di LIBRO") + print(f" → §39 pubblico' +1,55%/anno sulla variante E: riprodotto " + f"({(a['drift']-b['drift'])*W_TP:+.2%} + il residuo di base del perpetual, +0,07%).") + print(" 📌 La vol NON cambia (terza cifra): e' l'unica variante che alza il drift senza") + print(" alzare il rischio. E' questo, e non lo Sharpe, il motivo per cui i MURI si muovono.") + return dict(d_drift_book=(a["drift"] - b["drift"]) * W_TP, pagato=pagato, + tp_drift_spot=a["drift"]) + + +# --------------------------------------------------------------------------- il netting +def skh_pos_path(a: str, off: int = 0) -> tuple[pd.DatetimeIndex, np.ndarray, dict]: + """Segno di SKH01 su griglia 230m, replicando ESATTAMENTE `backtest_signals` (stessa + regola d'uscita, stessa priorita' SL, stesso non-overlap). Validato contro il suo + `n_trades` e la sua `time_in_market`: se diverge, il controllo lo dice.""" + daily, m, ltf, ent = r02.run_asset(a, off) + c = ltf["close"].values.astype(float) + h = ltf["high"].values.astype(float) + lo = ltf["low"].values.astype(float) + n = len(c) + pos = np.zeros(n) + busy, ntr, bars = -1, 0, 0 + for i in range(n): + e = ent[i] if i < len(ent) else None + if e is None or e.get("dir", 0) == 0 or i <= busy: + continue + d = int(e["dir"]); tp = e.get("tp"); sl = e.get("sl"); mb = int(e.get("max_bars") or 24) + ex = min(i + mb, n - 1) + for j in range(i + 1, min(i + mb + 1, n)): + if sl is not None and ((d == 1 and lo[j] <= sl) or (d == -1 and h[j] >= sl)): + ex = j; break + if tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and lo[j] <= tp)): + ex = j; break + ex = j + pos[i + 1:ex + 1] = d + busy, ntr, bars = ex, ntr + 1, bars + (ex - i) + idx = pd.to_datetime(ltf["timestamp"].values + r02.MS_LTF, unit="ms", utc=True) + chk = dict(ntr=ntr, ntr_ref=int(m.n_trades), bars=bars, + bars_ref=int(round(m.time_in_market * n))) + return idx, pos, chk + + +def walk_bps(levels: list, usd: float, mid: float | None) -> float | None: + """Costo in bps di attraversare `usd` dollari sul lato dato del libro, rispetto al mid. + None se il libro visibile non basta. PURA.""" + if not levels: + return None + mid = mid or levels[0][0] + resto, speso, preso = usd, 0.0, 0.0 + for px, am in levels: + cap = px * am + q = min(resto, cap) + speso += q + preso += q / px + resto -= q + if resto <= 1e-9: + break + if resto > 1e-9 or preso <= 0: + return None + return (speso / preso / mid - 1) * 1e4 + + +def q2c(Q1: dict) -> dict: + """Il COSTO del netting perso — simulato sul percorso di produzione, cadenza oraria.""" + sez("Q2 (iii) — IL COSTO DEL NETTING PERSO (il lato che §39 stimava «1-2%»)") + E = Q1["E"]; cap = Q1["cap"]; imr = Q1["im"] + minu = 5.0 + print(f" Simulazione della RICONCILIAZIONE ORARIA (quella che gira), 2 percorsi a parita' di") + print(f" segnale: NETTA (oggi, un ordine per asset) contro SEPARATA (TP01 spot + SKH01 perp).") + print(f" E = ${E:,.2f} costante [ASSUNTO] · cap/asset ${cap:,.0f} · min ordine ${minu:.0f} " + f"[CODICE config]") + print(" Il nozionale in USD si muove col prezzo anche a segnale fermo: e' la prima causa di") + print(" ordini nel live, ed e' modellata (pos *= px[t]/px[t-1]).\n") + + tp = TrendPortfolio(**CANONICAL) + res = {} + for a in ASSETS: + h1 = load(a, "1h") + idx1 = pd.DatetimeIndex(pd.to_datetime(h1["datetime"], utc=True)) + px = h1["close"].values.astype(float) + d1 = resample_1d(load_data(a, "1h")) + tgt = np.nan_to_num(np.asarray(tp.target_series(d1), float)) + di = pd.DatetimeIndex(pd.to_datetime(d1["datetime"], utc=True)) + # il target deciso sulla barra i e' noto alla sua CHIUSURA -> vale da i+1 (causale) + tps = pd.Series(tgt, index=di).shift(1).reindex(idx1, method="ffill").fillna(0.0).values + si, sp, chk = skh_pos_path(a, 0) + sks = pd.Series(sp, index=si).reindex(idx1, method="ffill").fillna(0.0).values + ok = (chk["ntr"] == chk["ntr_ref"]) and abs(chk["bars"] - chk["bars_ref"]) <= 1 + print(f" [CONTROLLO] {a}: percorso SKH01 ricostruito = harness? trade " + f"{chk['ntr']} vs {chk['ntr_ref']}, barre in mercato {chk['bars']} vs " + f"{chk['bars_ref']} -> {'OK' if ok else 'DIVERGE'}") + assert ok, f"ricostruzione SKH01 divergente su {a}" + res[a] = dict(px=px, tp=tps, sk=sks, idx=idx1) + + def simula(a: str, sep: bool) -> dict: + d = res[a]; px, tps, sks = d["px"], d["tp"], d["sk"] + n = len(px) + p_net = p_tp = p_sk = 0.0 + ordini = ord_live = ord_spot = 0 + turn_perp = turn_spot = gross_sum = track = 0.0 + # finestra del libro LIVE (arming del cron_book): serve come CONTROLLO POSITIVO del + # conteggio ordini contro le 18 esecuzioni realmente avvenute. + t_live = pd.Timestamp("2026-06-23", tz="UTC") + anni_live = (d["idx"][-1] - t_live).days / 365.25 + for t in range(1, n): + g = px[t] / px[t - 1] + p_net *= g; p_tp *= g; p_sk *= g + raw_tp = WEIGHT * E * W_TP * max(tps[t], 0.0) + raw_sk = WEIGHT * E * W_SKH * sks[t] + if sep: + # cap: si conserva il tetto per-asset sul LORDO delle due gambe + lordo = abs(raw_tp) + abs(raw_sk) + k = min(1.0, cap / lordo) if lordo > 0 else 1.0 + t_tp, t_sk = raw_tp * k, raw_sk * k + if abs(t_tp - p_tp) >= minu: + turn_spot += abs(t_tp - p_tp); p_tp = t_tp; ordini += 1; ord_spot += 1 + ord_live += int(d["idx"][t] >= t_live) + if abs(t_sk - p_sk) >= minu: + turn_perp += abs(t_sk - p_sk); p_sk = t_sk; ordini += 1 + ord_live += int(d["idx"][t] >= t_live) + gross_sum += abs(p_tp) + abs(p_sk) + track += abs((p_tp + p_sk) - (t_tp + t_sk)) + else: + t_net = max(-cap, min(cap, raw_tp + raw_sk)) + if abs(t_net - p_net) >= minu: + turn_perp += abs(t_net - p_net); p_net = t_net; ordini += 1 + ord_live += int(d["idx"][t] >= t_live) + gross_sum += abs(p_net) + track += abs(p_net - t_net) + anni = (d["idx"][-1] - d["idx"][0]).days / 365.25 + return dict(ordini=ordini / anni, spot=turn_spot / anni, perp=turn_perp / anni, + gross=gross_sum / (n - 1), anni=anni, track=track / (n - 1), + ord_live=ord_live, anni_live=anni_live, ord_spot=ord_spot / anni) + + print() + tot = {} + for sep in (False, True): + agg = dict(ordini=0.0, spot=0.0, perp=0.0, gross=0.0, track=0.0, ord_live=0.0, + ord_spot=0.0) + for a in ASSETS: + r = simula(a, sep) + for k in agg: + agg[k] += r[k] + r0 = simula("BTC", sep) + agg["anni"] = r0["anni"] + agg["ord_live"] = agg["ord_live"] / r0["anni_live"] + tot["SEPARATA" if sep else "NETTA"] = agg + print(f" {'percorso':<12} {'ordini/anno':>12} {'turnover perp':>14} {'turnover spot':>14} " + f"{'lordo medio':>12}") + for k, v in tot.items(): + print(f" {k:<12} {v['ordini']:>12.1f} {v['perp']:>13,.0f}$ {v['spot']:>13,.0f}$ " + f"{v['gross']:>11,.2f}$") + dn = tot["SEPARATA"]; nn = tot["NETTA"] + print(f" {'DELTA':<12} {dn['ordini']-nn['ordini']:>+12.1f} " + f"{dn['perp']-nn['perp']:>+13,.0f}$ {dn['spot']-nn['spot']:>+13,.0f}$ " + f"{dn['gross']-nn['gross']:>+11,.2f}$") + print(f"\n su {nn['anni']:.1f} anni di storia (2019-03 -> oggi), a capitale COSTANTE ${E:,.0f}") + print(f"\n ⚠️ MENO ORDINI NON E' MEGLIO — il controllo giusto non e' il conteggio ma il") + print(f" TRACKING (5ª occorrenza della lezione «banda in valuta assoluta», 27/07):") + print(f" scarto medio |posizione tenuta - target| : NETTA ${nn['track']:.2f} · " + f"SEPARATA ${dn['track']:.2f}") + print(f" La separata fa {nn['ordini']-dn['ordini']:.1f} ordini/anno in MENO perche' la banda da $5 e'") + print(f" applicata a due target piu' PICCOLI invece che alla loro somma: filtra di piu'.") + print(f" Lo scarto {'peggiora' if dn['track'] > nn['track'] else 'migliora'} di " + f"${dn['track']-nn['track']:+.2f} ({(dn['track']-nn['track'])/max(nn['track'],1e-9):+.0%}).") + print(f"\n [CONTROLLO POSITIVO] il conteggio ordini regge sul campione dove esiste la verita'?") + print(f" simulato sulla finestra del libro live (dal 2026-06-23): " + f"NETTA {nn['ord_live']:.0f} ordini/anno") + nex = len([x for x in (ROOT / "data" / "live" / "book_executions.jsonl") + .read_text().splitlines() if x.strip()]) + gg = (pd.Timestamp.now(tz="UTC") - pd.Timestamp("2026-06-23", tz="UTC")).days + print(f" osservato: {nex} esecuzioni in {gg} giorni = {nex*365.25/gg:.0f}/anno [LOG]") + print(f" → la simulazione e' nello stesso ordine di grandezza dell'osservato; la media su") + print(f" 8 anni ({nn['ordini']:.0f}/anno) e' piu' alta perche' il 2026 e' un anno in cui il libro") + print(f" e' stato flat l'85% del tempo. **Il DELTA fra i due percorsi, non il livello,") + print(f" e' la grandezza su cui si decide.**") + + fee_net = nn["perp"] * FEE_PERP_SIDE + fee_sep_oggi = dn["perp"] * FEE_PERP_SIDE + dn["spot"] * 0.0 + fee_sep_dopo = (dn["perp"] + dn["spot"]) * FEE_PERP_SIDE + print(f"\n [CALC] COMMISSIONI (perp {FEE_PERP_SIDE*1e4:.2f} bps/lato [VENUE/nastro], " + f"spot {Q1['spec'].get('BTC_USDC',{}).get('taker_commission',0)*1e4 if Q1['spec'] else 0:.2f} bps [VENUE]):") + print(f" NETTA (oggi) ${fee_net:>7.2f}/anno = {fee_net/E:>6.3%} di equity") + print(f" SEPARATA, spot a fee ZERO ${fee_sep_oggi:>7.2f}/anno = {fee_sep_oggi/E:>6.3%}" + f" ({(fee_sep_oggi-fee_net)/E:+.3%})") + print(f" SEPARATA, spot al taker del perp ${fee_sep_dopo:>7.2f}/anno = {fee_sep_dopo/E:>6.3%}" + f" ({(fee_sep_dopo-fee_net)/E:+.3%})") + print(" 📌 [DEDOTTO] Il netting NON e' un risparmio di commissioni: e' il contrario. Oggi la") + print(" separazione fa PAGARE MENO, perche' i tre quarti del turnover passano su uno") + print(" strumento a fee zero. Il netting risparmia ORDINI, non dollari.") + + im_net = nn["gross"] * imr + im_sep = dn["gross"] * imr + print(f"\n [CALC] MARGINE IMMOBILIZZATO (a {imr:.2%} misurato in 1c):") + print(f" NETTA lordo medio ${nn['gross']:.2f} -> IM ${im_net:.2f} = {im_net/E:.3%} di equity") + print(f" SEPARATA lordo medio ${dn['gross']:.2f} -> il perp e' solo la gamba SKH01") + print(f" ⚠️ Il «1-2%» stimato da §39 come risparmio del netting su margine e commissioni:") + print(f" misurato, il margine in gioco vale {im_net/E:.3%} di equity e le commissioni") + print(f" {fee_net/E:.3%}/anno. **La stima era alta di un ordine di grandezza.**") + + STRUM = ("BTC_USDC", "BTC_USDC-PERPETUAL", "ETH_USDC", "ETH_USDC-PERPETUAL") + books = {k: get(f"get_order_book?instrument_name={k}&depth=20", f"ob20_{k}") for k in STRUM} + hs = {} + for k, b in books.items(): + if b and b.get("best_bid_price") and b.get("best_ask_price"): + mid = (b["best_bid_price"] + b["best_ask_price"]) / 2 + hs[k] = (b["best_ask_price"] - b["best_bid_price"]) / 2 / mid * 1e4 + # TUTTE le istantanee del libro presenti in cache (ogni corsa ne lascia una): il + # differenziale di spread e' l'input piu' rumoroso di questa sezione e va mostrato + # come BANDA, non come punto. + snaps = [] + for pref in sorted({f.name.rsplit("_", 3)[0] for f in SCRATCH.glob("ob*_*_USDC*.json")}): + bb = {} + for k in STRUM: + f = SCRATCH / f"{pref}_{k}.json" + if f.exists(): + try: + bb[k] = json.loads(f.read_text()) + except Exception: + pass + if len(bb) == 4 and all(bb[k].get("best_bid_price") for k in STRUM): + h = {k: (bb[k]["best_ask_price"] - bb[k]["best_bid_price"]) / + (bb[k]["best_ask_price"] + bb[k]["best_bid_price"]) * 1e4 for k in STRUM} + d = np.mean([h["BTC_USDC"] - h["BTC_USDC-PERPETUAL"], + h["ETH_USDC"] - h["ETH_USDC-PERPETUAL"]]) + ts = pd.Timestamp(bb["BTC_USDC"].get("timestamp", 0), unit="ms", tz="UTC") + snaps.append((pref, ts, float(d))) + if hs: + print("\n [VENUE] ⚠️ LA FEE NON E' TUTTO IL COSTO: mezzo spread al top del book, ORA") + for k in ("BTC_USDC", "BTC_USDC-PERPETUAL", "ETH_USDC", "ETH_USDC-PERPETUAL"): + if k in hs: + b = books[k] + print(f" {k:<22} {hs[k]:>6.2f} bps bid {b['best_bid_price']:>10,.2f} " + f"x{b.get('best_bid_amount')} ask {b['best_ask_price']:>10,.2f} " + f"x{b.get('best_ask_amount')}") + d_hs = np.mean([hs.get("BTC_USDC", np.nan) - hs.get("BTC_USDC-PERPETUAL", np.nan), + hs.get("ETH_USDC", np.nan) - hs.get("ETH_USDC-PERPETUAL", np.nan)]) + extra = dn["spot"] * d_hs * 1e-4 + print(f" → lo spot e' {d_hs:+.2f} bps piu' largo del perp (istantanea, 1 momento solo)") + print(f" su ${dn['spot']:,.0f}/anno di turnover deviato = ${extra:+.2f}/anno = " + f"{extra/E:+.3%} di equity") + print(f" Sommato al risparmio di fee ({(fee_sep_oggi-fee_net)/E:+.3%}): " + f"{(fee_sep_oggi-fee_net+extra)/E:+.3%}/anno.") + print("\n ⚠️ E QUANTO VALGA QUELLA ISTANTANEA LO DICONO LE ISTANTANEE STESSE. Questo") + print(" script ne lascia una in cache a ogni corsa; quelle presenti:") + for pref, ts, d in snaps: + print(f" {str(ts)[:19]} UTC differenziale spot-perp {d:>+6.2f} bps " + f"-> sovracosto {d*1e-4*dn['spot']/E:>+7.3%}/anno") + if len(snaps) >= 2: + lo, hi = min(x[2] for x in snaps), max(x[2] for x in snaps) + print(f" → due letture a pochi minuti di distanza danno {lo:.2f} e {hi:.2f} bps " + f"({(hi-lo)/max(lo,1e-9):+.0%}).") + print(f" BANDA del sovracosto d'esecuzione: " + f"[{lo*1e-4*dn['spot']/E:+.3%}, {hi*1e-4*dn['spot']/E:+.3%}]/anno, contro un") + print(f" risparmio di fee di {(fee_sep_oggi-fee_net)/E:+.3%} -> il netto d'esecuzione") + n_lo = (fee_sep_oggi - fee_net) / E + lo * 1e-4 * dn["spot"] / E + n_hi = (fee_sep_oggi - fee_net) / E + hi * 1e-4 * dn["spot"] / E + print(f" CAMBIA SEGNO fra le due letture: [{n_lo:+.3%}, {n_hi:+.3%}]/anno.") + print(f" **Non e' risolvibile con un'istantanea**, e non serve che lo sia:") + print(f" l'ampiezza della banda ({n_hi-n_lo:.3%}) e' il {(n_hi-n_lo)/0.0158:.0%} del lead.") + print(f" Il numero onesto oggi e' «l'esecuzione non decide»; se un giorno") + print(f" dovesse decidere, si raccoglie il libro su piu' ore.") + else: + print(" (una sola istantanea in cache: rilanciare lo script a ore diverse per") + print(" avere la banda — un punto solo non e' una misura di spread)") + print(" ⚠️ Il book si allarga di notte e nei crash. §39 fece la stessa cosa e la") + print(" dichiaro'. Va misurata su piu' ore prima di metterla in un muro.") + print("\n [VENUE] ⚠️ E IL MEZZO SPREAD NON BASTA: il MIGLIOR LIVELLO dello spot e'") + print(" SOTTILE. Profondita' cumulata (20 livelli) e costo di attraversare un") + print(" ticket, misurati camminando il libro:") + tick = dn["spot"] / max(dn["ord_spot"], 1e-9) + print(f" ticket medio della gamba spot a ${E:,.0f} di equity: ~${tick:.0f} " + f"({dn['ord_spot']:.0f} ordini spot/anno) -> scala col capitale") + print(f" {'strumento':<22} {'top ask $':>10} {'20 liv. $':>11} " + + "".join(f"{'$'+f'{tick*m:,.0f}':>10}" for m in (1, 20 / 0.636, 80 / 0.636))) + prof = {} + for k in ("BTC_USDC", "BTC_USDC-PERPETUAL", "ETH_USDC", "ETH_USDC-PERPETUAL"): + b = books.get(k) + if not b or not b.get("asks"): + continue + top = b["asks"][0][0] * b["asks"][0][1] + cum = sum(px * am for px, am in b["asks"]) + prof[k] = cum + costi = [] + for m in (1, 20 / 0.636, 80 / 0.636): + costi.append(walk_bps(b["asks"], tick * m, + (b["best_bid_price"] + b["best_ask_price"]) / 2)) + print(f" {k:<22} {top:>10,.0f} {cum:>11,.0f} " + + "".join((f"{c:>9.1f}b" if c is not None else f"{'FUORI':>10}") for c in costi)) + print(" (le tre colonne sono il costo in bps di attraversare il ticket medio a") + print(f" ${E:,.0f} / $20.000 / $80.000 di equity; 'FUORI' = il libro visibile non basta)") + cb = walk_bps(books["BTC_USDC"]["asks"], tick, None) if books.get("BTC_USDC") else None + print(" 📌 [DEDOTTO] A questa taglia il ticket e' cosi' piccolo che cammina un") + print(" livello solo: il costo vero e' il MEZZO SPREAD, ed e' quello contato") + print(" sopra. La profondita' diventa il vincolo molto dopo i $20k — ma va") + print(" RI-MISURATA a ogni salto di taglia, come lo slippage del libro (§ondata).") + sp_cost = extra + + opp = 0.0 + tots = 0 + for a in ASSETS: + d = res[a] + m = (d["tp"] > 0) & (d["sk"] < 0) + opp += int(m.sum()); tots += len(m) + print(f"\n [CALC] Quando il netting serve DAVVERO (segni opposti: TP01 long e SKH01 short):") + print(f" {opp/tots:.2%} delle ore di storia. Li' la netta tiene {'' }un nozionale piu' piccolo") + print(f" della somma delle gambe; nel resto del tempo netta e separata tengono lo STESSO") + print(f" lordo e il netting risparmia solo l'ordine.") + return dict(net=nn, sep=dn, fee_net=fee_net, fee_sep_oggi=fee_sep_oggi, + fee_sep_dopo=fee_sep_dopo, opp=opp / tots, im_net=im_net, + spread=locals().get("sp_cost", 0.0), E=E, + spread_banda=[x[2] * 1e-4 * dn["spot"] for x in snaps]) + + +def q2d() -> None: + """Il percorso LIVE reale, per contro-prova.""" + sez("Q2 (iii-bis) — CONTRO-PROVA SUL PATH LIVE REALE (2 mesi, 1443 giri orari)") + p = ROOT / "logs" / "cron_book.log" + blocks = re.split(r"===== (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) cron_book =====", p.read_text()) + pat = re.compile(r"(BTC|ETH) TP ([+-][\d.]+) · SKH ([+-]\d)\((flat|LONG|SHORT)") + stato = {"flat": 0, "LONG": 0, "SHORT": 0} + giri = 0 + both = 0 + for i in range(1, len(blocks), 2): + rows = pat.findall(blocks[i + 1]) + if rows: + giri += 1 + for a, t, s, st in rows: + stato[st if st in stato else "flat"] += 1 + if float(t) > 0 and int(s) < 0: + both += 1 + ex = [json.loads(x) for x in (ROOT / "data" / "live" / "book_executions.jsonl") + .read_text().splitlines() if x.strip()] + print(f" [LOG] {giri} giri con segnale leggibile · stati di SKH01 (conteggio ASSET-ORA, " + f"2 asset per giro):") + print(f" flat {stato['flat']} · LONG {stato['LONG']} · SHORT {stato['SHORT']} " + f"(totale {sum(stato.values())} = 2 x {giri})") + print(f" [LOG] asset-ora con TP01 long E SKH01 short (il caso in cui il netting conta): {both}") + print(f" [LOG] {len(ex)} esecuzioni reali; fee totale pagata ${sum(e['fee'] for e in ex):.4f}") + print(f" su nozionale ${sum(abs(e['filled']*e['price']) for e in ex):,.2f} = " + f"{sum(e['fee'] for e in ex)/sum(abs(e['filled']*e['price']) for e in ex)*1e4:.2f} bps/lato") + print(" 📌 In 2 mesi di vita del libro SKH01 non e' MAI stato short: il campione live NON") + print(" puo' misurare il costo del netting, e va detto invece di estrapolarlo. La misura") + print(" che decide e' quella su 7,4 anni di sezione precedente.") + print(" ⚠️ `ts_utc` in book_executions.jsonl NON e' l'ora del fill (18/18 a 00:00:00): l'ora") + print(" vera sta solo in logs/cron_book.log. Qui non serve (si contano fee e nozionale).") + + +# =========================================================================== +# 3) Q3 — LA SORVEGLIANZA MANCANTE +# =========================================================================== +def q3(Q1: dict, Q2b: dict, Q2c: dict) -> None: + sez("Q3 — LA SORVEGLIANZA: `fee_watch` non vedrebbe una gamba spot") + import importlib.util + _spec = importlib.util.spec_from_file_location( + "fee_watch_ro", ROOT / "scripts" / "live" / "fee_watch.py") + FW = importlib.util.module_from_spec(_spec) + _spec.loader.exec_module(FW) # import puro: il main e' sotto __main__, non gira + src = inspect.getsource(FW) + derivato = "INSTRUMENTS = tuple(BOOK_INSTRUMENT[a] for a in sorted(BOOK_INSTRUMENT))" in src + print(f" [CODICE] scripts/live/fee_watch.py DERIVA i suoi strumenti dal libro -> {derivato}") + print(f" INSTRUMENTS = {FW.INSTRUMENTS}") + print(" → una gamba su BTC_USDC/ETH_USDC non comparirebbe: il sorvegliante continuerebbe a") + print(" dire OK guardando due strumenti su cui passerebbe solo un quarto del turnover.") + print(" (E' ESATTAMENTE il difetto corretto il 21/08, in cui sorvegliava gli inverse.)") + print("\n LA RIGA — non scritta, dichiarata (questo filone non tocca scripts/live/):") + print(" in `src/live/book.py`, accanto a INSTRUMENT:") + print(' SPOT_INSTRUMENT = {"BTC": "BTC_USDC", "ETH": "ETH_USDC"}') + print(" in `scripts/live/fee_watch.py`:") + print(" from src.live.book import SPOT_INSTRUMENT as BOOK_SPOT") + print(" INSTRUMENTS = (tuple(BOOK_INSTRUMENT[a] for a in sorted(BOOK_INSTRUMENT))") + print(" + tuple(BOOK_SPOT[a] for a in sorted(BOOK_SPOT)))") + print(f" ⚠️ NON basta: `convenzione('BTC_USDC')` = '{FW.convenzione('BTC_USDC')}' -> il") + print(" cross-check sui fill reali tornerebbe None (= «non misurata», che e' onesto ma") + print(" cieco). Servirebbe una terza famiglia 'spot' (amount in BASE, fee in USDC).") + print(" Senza, si sorveglia il TIER ma non cio' che si paga davvero.") + + sp = Q1["spec"].get("BTC_USDC") + if sp: + print(f"\n [VENUE] listino spot OGGI: taker {float(sp['taker_commission'])*1e4:.2f} bps · " + f"maker {float(sp['maker_commission'])*1e4:.2f} bps · " + f"min lotto {sp['min_trade_amount']} BTC") + print(" ⚠️ La fee 0 e' dichiarata TEMPORANEA nell'annuncio del 1° agosto («spot a zero fino") + print(" al collegamento con Coinbase»). Fonte: articolo Insights, SECONDARIA.") + costo = (Q2c["fee_sep_dopo"] - Q2c["fee_sep_oggi"]) / Q1["E"] + print(f" Se lo spot passasse al taker dei lineari (3,50 bps) il lead perderebbe " + f"{costo:.3%}/anno") + print(f" ({costo/Q2b['d_drift_book']:.0%} del guadagno di {Q2b['d_drift_book']:.2%}): " + f"**sopravviverebbe**. La ragione per cui") + print(" sorvegliarlo non e' che lo ucciderebbe, e' che oggi nessuno se ne accorgerebbe —") + print(" e un costo che nessuno vede non entra in nessun muro.") + + +# =========================================================================== +# 4) Q4 — IL RISCHIO CHE SI AGGIUNGE +# =========================================================================== +def q4(Q1: dict, Q2b: dict, Q2c: dict) -> None: + sez("Q4 — IL RISCHIO: detenere cripto non e' tenere un derivato") + E = Q1["E"] + print(" (i) VENUE — invariato, e non e' una buona notizia: il rischio era GIA' totale") + print(" (decisione dell'operatore 26/07: 100% Deribit fino a $20k). Lo spot non lo") + print(" aumenta, ma cambia COSA c'e' sul conto: oggi margine USDC, domani cripto.") + print(" In un fallimento la differenza e' di procedura concorsuale, non di quantita'.") + print(" [ASSUNTO] nessun numero: `p` non e' stimato, e' un parametro dell'operatore.") + + print("\n (ii) FISCO — e' qui che sta il numero grosso, ed e' lo stesso del 30/07 e di §39.") + tp_share = Q2b["tp_drift_spot"] * W_TP + print(f" La gamba TP01 porta ~{tp_share:.1%}/anno di drift al libro [CALC da Q2ii].") + print(f" Se i derivati fossero `c-quater` (26%) e lo spot `c-sexies` (33%), spostare") + print(f" TP01 sullo spot sposterebbe quel P&L nel comparto piu' caro: 7 punti su") + print(f" {tp_share:.1%} = {0.07*tp_share:.2%}/anno.") + print(f" Confronto diretto col guadagno misurato in Q2(ii): {Q2b['d_drift_book']:+.2%}/anno.") + if 0.07 * tp_share > Q2b["d_drift_book"] * 0.5: + print(" → **la domanda fiscale vale una frazione maggioritaria del guadagno**: si") + print(" risponde prima di provare, e la risposta non e' un backtest.") + print(" ⚠️ Comparti SEPARATI: se i derivati sono `c-quater`, le minusvalenze del perp") + print(" (SKH01) NON compenserebbero le plusvalenze spot (TP01). Oggi si compensano") + print(" perche' stanno nello stesso comparto. Questo e' un costo NUOVO della") + print(" separazione, mai considerato prima. [DEDOTTO, da verificare col commercialista]") + + print("\n (iii) LIQUIDAZIONE — e qui la separazione MIGLIORA, ed e' misurabile.") + print(f" Oggi il 100% del lordo del libro (medio ${Q2c['net']['gross']:.2f}) e' su un perp") + print(" marginato: liquidabile. Separando, la gamba TP01 diventa un ACQUISTO: uno spot") + print(" non ha prezzo di liquidazione, qualunque cosa faccia il prezzo.") + print(f" → esce dal perimetro di liquidazione il {W_TP:.0%} del libro per peso " + f"(gamba TP01),") + print(f" e col lordo misurato in Q2(iii) restano ~${Q2c['sep']['gross']*W_SKH/(W_TP+W_SKH):.0f}" + f" di nozionale liquidabile invece di ${Q2c['net']['gross']:.0f}.") + print(" ⚠️ CONTROPARTITA: il disaster-SL -30% on-book oggi protegge la posizione NETTA.") + print(" Separando, potrebbe stare solo sul perp (SKH01). La gamba spot resterebbe") + print(" senza stop — il che e' coerente (non puo' essere liquidata) ma significa che") + print(" in un blackout prolungato TP01 resta long al 75% del conto senza protezione.") + print(" Oggi il disaster-SL, sulla netta, copre anche quella quota.") + print(" 📌 [DEDOTTO] Il rischio non sparisce: cambia forma. Da «liquidazione forzata") + print(" a -30%» a «esposizione lunga non protetta». Sono due code diverse, e la") + print(" seconda e' quella che TP01 accetta per costruzione (long-flat, non a leva).") + + print("\n (iv) HAIRCUT — leggibile? NO (1g). Binding? NO (1d): la copertura del margine di") + print(f" SKH01 col solo USDC libero e' {Q1['free']/Q1['need']:.0f}x e non dipende dal capitale.") + print(" → e' l'unico caso di questo filone in cui «non lo so» e «non conta» coincidono,") + print(" e va detto in quest'ordine: prima non lo so, poi perche' non conta.") + + +# =========================================================================== +# 5) VERDETTO +# =========================================================================== +def verdetto(Q1: dict, Q2a: dict, Q2b: dict, Q2c: dict) -> None: + sez("VERDETTO") + t1_live = Q2a[("onbook_tp", "fastdetect")] + print(f" Q1 MARGINE il venue NON uccide il lead: BTC/ETH sono nel pool di") + print(f" cross-collateral [VENUE], lo spot ha max_leverage 10 [VENUE], e la") + print(f" gamba SKH01 resterebbe marginata {Q1['free']/Q1['need']:.0f}x dal solo USDC") + print(f" libero [CALC], a QUALUNQUE capitale. Uccide il lead il CODICE:") + print(f" `_equity` legge solo USDC e leggerebbe {Q1['free']/Q1['E']:.0%} del conto vero.") + bnd = Q2c["spread_banda"] or [Q2c["spread"]] + print(f" Q2 VALORE il funding risparmiato e' reale ({Q2b['d_drift_book']:+.2%}/anno di drift") + print(f" di libro, riprodotto). T1 sbloccato vale {t1_live[0]:+.3f} Sharpe FULL") + print(f" contro il path che il live gia' percorre: **zero o negativo**.") + print(f" Il netting perso NON costa commissioni ({(Q2c['fee_sep_oggi']-Q2c['fee_net'])/Q1['E']:+.3%}: e' un") + print(f" RISPARMIO) ne' ordini ({Q2c['sep']['ordini']-Q2c['net']['ordini']:+.1f}/anno): costa TRACKING (+12%) e") + print(f" SPREAD ([{min(bnd)/Q1['E']:.3%},{max(bnd)/Q1['E']:.3%}]/anno su 2 istantanee: lo spot e'") + print(f" 2-4 bps piu' largo del perp, che e' un mercato a UN TICK). I due") + print(f" si compensano quasi: l'esecuzione NON decide questo lead.") + print(f" Q3 SORVEGLIANZA una gamba spot sarebbe INVISIBILE a `fee_watch`, che deriva i suoi") + print(f" strumenti dal libro. Riga dichiarata; serve anche una convenzione") + print(f" 'spot' o il cross-check sui fill resta muto.") + print(f" Q4 RISCHIO la liquidazione MIGLIORA (il 75% del libro esce dal perimetro), il") + print(f" fisco PEGGIORA e vale {0.07*Q2b['tp_drift_spot']*W_TP:.2%}/anno se i derivati sono") + print(f" `c-quater` — piu' della meta' del guadagno.") + E = Q1["E"] + lordo = Q2b["d_drift_book"] + print("\n 📌 IL NUMERO DA CITARE — il lead non vale +1,55%/anno, vale meno, e si scrive") + print(" con la sua colonna d'esecuzione (che §39 aveva stimato e qui e' misurata):") + print(f" funding risparmiato dalla gamba TP01 {lordo:>+8.2%}/anno di drift di libro") + e_lo = (Q2c["fee_sep_oggi"] - Q2c["fee_net"]) / E + min(bnd) / E + e_hi = (Q2c["fee_sep_oggi"] - Q2c["fee_net"]) / E + max(bnd) / E + print(f" esecuzione (fee zero MENO spread), contributo netto " + f"[{-e_hi:+.3%},{-e_lo:+.3%}]/anno") + print(f" (fee {-(Q2c['fee_sep_oggi']-Q2c['fee_net'])/E:+.3%} certo · spread " + f"{-max(bnd)/E:+.3%}..{-min(bnd)/E:+.3%} da {len(bnd)} istantanee del libro)") + print(f" ------------------------------------------------------") + print(f" NETTO OGGI [{lordo-e_hi:+.2%},{lordo-e_lo:+.2%}]/anno") + print(f" NETTO se lo spot torna a 3,50 bps " + f"[{lordo-e_hi-(Q2c['fee_sep_dopo']-Q2c['fee_sep_oggi'])/E:+.2%}," + f"{lordo-e_lo-(Q2c['fee_sep_dopo']-Q2c['fee_sep_oggi'])/E:+.2%}]/anno") + print(f" imposta in piu' se i derivati sono c-quater " + f"{-0.07*Q2b['tp_drift_spot']*W_TP:>+7.2%}/anno ← non misurabile qui") + print(f" NETTO nello scenario fiscale sfavorevole " + f"{lordo-e_hi-(Q2c['fee_sep_dopo']-Q2c['fee_sep_oggi'])/E-0.07*Q2b['tp_drift_spot']*W_TP:>+8.2%}/anno" + f" (estremo conservativo)") + print(" ⚠️ Che il netto di oggi cada su +1,55% — lo stesso numero di §39 — e' una") + print(" COINCIDENZA di due decomposizioni diverse, non una conferma: §39 pubblico'") + print(" +1,55% come delta di drift della variante E (lordo dello spread, che stimava") + print(" a parte); qui il LORDO e' +1,58% (riprodotto entro 0,03 punti) e il netto ci") + print(" ricasca sopra dopo aver sottratto uno spread MISURATO. Due strade, un numero.") + print("\n 📌 IL LEAD REGGE, MA IL SUO VALORE NON E' QUELLO CHE SEMBRAVA.") + print(f" Il valore vero e' il FUNDING, e basta: {Q2b['d_drift_book']:+.2%}/anno lordo.") + print(" La separazione delle gambe — la ragione per cui questo filone e' stato aperto —") + print(" NON aggiunge: T1 e' un declassamento del path che gira, il netting non risparmia") + print(" commissioni, il margine non era il vincolo. E introduce due costi nuovi che") + print(" nessuno aveva contato: la SEPARAZIONE DEI COMPARTI FISCALI (le minusvalenze del") + print(" perp smetterebbero di compensare le plusvalenze spot) e la gamba TP01 senza") + print(" disaster-SL.") + print(" → Il prossimo passo NON e' un backtest: e' (1) la domanda al commercialista e") + print(" (2) un acquisto di prova da $7,73 che risolve le tre incognite di 1g. Entrambi") + print(" stanno fuori dal perimetro di questo filone.") + + +def main() -> None: + print("=" * 100) + print(" r0823_spot_netting — §45 SPOT-NETTING: quanto vale, e quanto costa, separare le gambe") + print("=" * 100) + print(f" {clock()} avvio · finestra vietata ora: {finestra_vietata()} · --no-net: {NO_NET}") + Q1 = q1() + print(f"\n{clock()} Q1 fatta") + A = q2a() + print(f"\n{clock()} Q2(i) fatta") + B = q2b() + print(f"\n{clock()} Q2(ii) fatta") + C = q2c(Q1) + q2d() + print(f"\n{clock()} Q2(iii) fatta") + q3(Q1, B, C) + q4(Q1, B, C) + verdetto(Q1, A, B, C) + sez("PROVA DI SOLA LETTURA") + print(f" chiamate di rete di questa corsa ({len(_NET_CALLS)}):") + for c in _NET_CALLS: + print(f" - {c}") + if not _NET_CALLS: + print(" - nessuna (tutto da cache)") + print(f" scritture: SOLO {SCRATCH} (cache). Nessun file del repo aperto in scrittura.") + print(f" ordini inviati: 0 — `DeribitRead` non ha, by design, alcun metodo di trading.") + print(f"\n{clock()} fine") + + +if __name__ == "__main__": + main()