Files
PythagorasGoal/scripts/research/r0823_spot_netting.py
T
Adriano Dal Pastro 279c70222b research(§45): il percorso --no-net non deve crollare — apr assente si DICHIARA, non si formatta
Trovato dallo smoke test a cache fredda: senza get_currencies la sezione 1f cadeva su
TypeError formattando None. Un'analisi che non puo' girare offline non e' riproducibile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf
2026-08-23 01:20:19 +00:00

1010 lines
59 KiB
Python

#!/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]
if apr is None:
print(" [VENUE] get_currencies NON letto in questa corsa -> sezione non eseguibile.")
print(" «non l'ho guardato» non e' «e' zero»: si dichiara, non si assume.")
return dict(E=Eu, cap=cap, im=imr, free=free, need=need, spec=spec, apr=None,
tp_gross=tp_gross_1x, skh_gross=skh_gross)
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()