e2aee5730b
§51 NEWDATA-SCOUT. La scorta di dati e' esaurita (§43), quindi la domanda cambia: non "cosa abbiamo e non guardiamo" ma "cosa NON abbiamo che vada iniziato OGGI". Ogni riga della colonna ricostruibilita' e' PROVATA con una GET pubblica + controllo positivo, non dedotta. VERDETTO: raccogliere la catena opzioni USDC-lineare (+117 chiamate/giro = +18%, non +90%); il book depth L2 come CAMPAGNA A TERMINE, non come collettore. Tutto il resto: no. - tape/liquidazioni Deribit: muro di ritenzione misurato fra -24h e -26h (non ricostruibile), ma il MDE lo manda al 2032 come segnale -> non raccogliere. E il campo `liquidation` non si e' fatto vedere in 1000 trade: non si accende una raccolta su un campo che non si sa se scatta. - §10 proponeva un collettore di OI perpetual "perche' oggi ripartirebbe da zero": MISURATO, non riparte da zero — Bybit serve >=800 giorni di OI perpetual gratis (timestamp verificati dentro la finestra chiesta), e la famiglia funding e' chiusa su 4 lati -> testare prima. - Binance OI/taker/long-short: muro vero a 30 giorni (l'API RIFIUTA, non risponde vuoto), ma MDE 6 anni + venue USDT -> no. - funding, DVOL, macro: ricostruibili E famiglie chiuse -> doppia eliminazione. Correzione a un numero pubblicato: la catena USDC con gli STESSI filtri del collettore vivo (<=95g, OI>=100) sono 113 strumenti, non ~587; l'origine del 587 resta ignota e lo dico. Il +117 e' un numero di OGGI e cresce con la liquidita' USDC: va ri-misurato prima di accendere. Bug catturato in sessione: urlopen solleva su HTTP 400, quindi il RIFIUTO di Binance veniva ingoiato come guasto di rete e il muro si ribaltava in silenzio in "RICOSTRUIBILE" — stessa conflazione error/no_quote gia' codificata in collect_chain. Guardia sulle finestre di cron verificata in entrambi i versi; MDE replica la convenzione §43 (1,40 a -> 1,66). Sola lettura sul disco, nessun ordine, nessuna chiave. Libro, pesi, cron, config INVARIATI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf
600 lines
28 KiB
Python
600 lines
28 KiB
Python
"""NEWDATA-SCOUT (filone §51) — cosa iniziare a raccogliere OGGI, se la scorta e' esaurita.
|
|
|
|
CONTESTO. Il filone §43 (`r0823_data_unused.py`) ha censito 647 file e concluso che la scorta di
|
|
dati del progetto e' ESAURITA: ogni dataset con storia vera e' gia' stato analizzato almeno una
|
|
volta, e cio' che resta non letto e' telemetria a 30-113 giorni. Quindi la domanda utile cambia:
|
|
non piu' "cosa abbiamo e non guardiamo" ma "cosa NON abbiamo, che varrebbe la pena iniziare a
|
|
raccogliere OGGI perche' fra 12-24 mesi apra una famiglia che oggi non e' apribile?".
|
|
|
|
Il progetto ha gia' pagato due volte la lezione: la catena opzioni non e' ricostruibile a
|
|
posteriori (Deribit non serve book storici; l'ora non raccolta e' persa per sempre) -> esiste
|
|
`scripts/live/collect_chain.py`; e le serie `paper_*` sono forward-only irrecuperabili.
|
|
|
|
IL CRITERIO CHE FA IL LAVORO E' IL PRIMO, E VA **PROVATO**, NON DEDOTTO.
|
|
1. RICOSTRUIBILITA' — se un dato si potra' scaricare storico quando servira', raccoglierlo oggi
|
|
e' PURO COSTO. Qui ogni riga della colonna e' il risultato di una GET pubblica di sondaggio,
|
|
con **controllo positivo** (una finestra in cui il dato DEVE esserci) prima di dichiarare un
|
|
muro: uno zero puo' voler dire "non c'e'" oppure "sto chiamando male", e le due cose si
|
|
distinguono solo provando che la stessa chiamata funziona altrove.
|
|
2. FAMIGLIA — una fonte che alimenta solo una famiglia gia' chiusa con misure non vale niente.
|
|
3. COSTO in chiamate, contro il budget IP esistente (il rate limit Deribit e' PER-IP e questo
|
|
progetto l'ha gia' saturato una volta, guasto di 8 ore il 29/07).
|
|
4. RISCHIO OPERATIVO — ogni raccolta e' un modo nuovo di rompersi in silenzio.
|
|
5. TEMPO PERCHE' SIA UTILE, dal MDE: SE(Sharpe) ~ 1/sqrt(anni), MDE ~ 1,96/sqrt(anni)
|
|
(stessa convenzione di §43: 1,40 anni -> MDE 1,66). Se la risposta e' "6 anni", non e' una
|
|
decisione di oggi.
|
|
|
|
NON PROPONE DI IMPLEMENTARE NULLA: produce materiale per una decisione dell'operatore.
|
|
|
|
uv run python scripts/research/r0823_newdata_scout.py
|
|
uv run python scripts/research/r0823_newdata_scout.py --no-net # solo tabella + MDE
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
from dataclasses import dataclass, field
|
|
from datetime import UTC, datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
# --- disciplina di rete -------------------------------------------------------------------
|
|
# Il rate limit Deribit e' PER-IP e questa VPS lo condivide. I minuti proibiti non sono cortesia:
|
|
# :05-:10 cron_book.sh (feed 5m di SKH01 -> esecuzione del libro con soldi veri)
|
|
# :24-:30 cron_chain.sh (collettore catena, 653 chiamate a 4/s per ~163 s)
|
|
FORBIDDEN_MINUTES = ((5, 10), (24, 30))
|
|
MAX_RPS = 2.0
|
|
TIMEOUT = 20
|
|
|
|
|
|
class Net:
|
|
"""GET pubbliche e tokenless, <=2/s, fuori dalle finestre dei cron di produzione."""
|
|
|
|
def __init__(self, enabled: bool = True) -> None:
|
|
self.enabled = enabled
|
|
self._next = 0.0
|
|
self.calls = 0
|
|
self.blocked = 0
|
|
|
|
@staticmethod
|
|
def in_forbidden_window() -> bool:
|
|
m = datetime.now(UTC).minute
|
|
return any(a <= m < b for a, b in FORBIDDEN_MINUTES)
|
|
|
|
def get(self, url: str) -> dict | list | None:
|
|
if not self.enabled:
|
|
return None
|
|
while self.in_forbidden_window():
|
|
self.blocked += 1
|
|
time.sleep(20)
|
|
now = time.monotonic()
|
|
if now < self._next:
|
|
time.sleep(self._next - now)
|
|
self._next = max(now, self._next) + 1.0 / MAX_RPS
|
|
self.calls += 1
|
|
req = urllib.request.Request(url, headers={"User-Agent": "PythagorasGoal-scout/1.0"})
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
|
|
return json.loads(r.read())
|
|
except urllib.error.HTTPError as e:
|
|
# UN RIFIUTO NON E' UN GUASTO. Binance risponde 400 + corpo JSON quando startTime
|
|
# e' oltre la ritenzione: se lo si ingoia come errore di rete, un MURO diventa
|
|
# indistinguibile da "nessun muro trovato" e la colonna ricostruibilita' si
|
|
# ribalta in silenzio. Stessa lezione di `quote_status` (error vs no_quote).
|
|
try:
|
|
body = json.loads(e.read())
|
|
if isinstance(body, dict):
|
|
body["_http_status"] = e.code
|
|
return body
|
|
except (ValueError, OSError):
|
|
return {"_http_status": e.code, "_unparsed": True}
|
|
except (urllib.error.URLError, TimeoutError, ValueError, OSError):
|
|
return None
|
|
|
|
|
|
DERIBIT = "https://www.deribit.com/api/v2/public"
|
|
MS = 1000
|
|
|
|
|
|
def ms_ago(**kw) -> int:
|
|
return int((datetime.now(UTC) - timedelta(**kw)).timestamp() * MS)
|
|
|
|
|
|
# --- esito di una prova di ricostruibilita' -----------------------------------------------
|
|
@dataclass
|
|
class Prova:
|
|
"""Una prova di ricostruibilita'. `positivo` e' il controllo che rende leggibile lo zero."""
|
|
fonte: str
|
|
verdetto: str = "NON PROVATO"
|
|
dettaglio: str = ""
|
|
positivo: str = ""
|
|
misurato: bool = False
|
|
extra_tot: int = 0
|
|
righe: list[str] = field(default_factory=list)
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------
|
|
# PROVE
|
|
# ------------------------------------------------------------------------------------------
|
|
def prova_tape(net: Net) -> Prova:
|
|
"""Trade tape Deribit (direzione aggressore + flag liquidation).
|
|
|
|
`get_last_trades_by_instrument_and_time` accetta qualunque finestra passata SENZA errore:
|
|
fuori ritenzione risponde `trades: []`. Uno zero non e' quindi una risposta -> serve il
|
|
controllo positivo (una finestra recente, dove i trade DEVONO esserci).
|
|
"""
|
|
p = Prova("tape / flusso aggressore Deribit")
|
|
inst = "BTC-PERPETUAL"
|
|
ore = (1, 6, 12, 24, 26, 30, 36, 48, 168)
|
|
trovato: dict[int, int] = {}
|
|
for h in ore:
|
|
st = ms_ago(hours=h)
|
|
d = net.get(f"{DERIBIT}/get_last_trades_by_instrument_and_time"
|
|
f"?instrument_name={inst}&start_timestamp={st}&end_timestamp={st + 300 * MS}"
|
|
f"&count=100&sorting=asc")
|
|
if d is None or "result" not in d:
|
|
return p
|
|
trovato[h] = len(d["result"]["trades"])
|
|
p.misurato = True
|
|
vivi = [h for h, n in trovato.items() if n > 0]
|
|
morti = [h for h, n in trovato.items() if n == 0]
|
|
if not vivi:
|
|
p.verdetto = "PROVA NON VALIDA"
|
|
p.dettaglio = "nessuna finestra restituisce trade: la chiamata e' rotta, non il dato assente"
|
|
return p
|
|
ultimo_vivo, primo_morto = max(vivi), (min(morti) if morti else None)
|
|
p.positivo = f"a -{ultimo_vivo}h la stessa chiamata rende {trovato[ultimo_vivo]} trade"
|
|
if primo_morto is None:
|
|
p.verdetto = "RICOSTRUIBILE"
|
|
p.dettaglio = f"nessun muro entro -{max(ore)}h"
|
|
else:
|
|
p.verdetto = "NON RICOSTRUIBILE"
|
|
p.dettaglio = (f"muro di ritenzione fra -{ultimo_vivo}h e -{primo_morto}h "
|
|
f"(~1 giorno): oltre, il tape e' perso per sempre")
|
|
p.righe = [f" -{h:>3}h -> {n:>3} trade" for h, n in trovato.items()]
|
|
return p
|
|
|
|
|
|
def prova_tape_rate(net: Net) -> Prova:
|
|
"""Quanto costa raccogliere il tape: trade/ora sui 2 strumenti che il libro trada."""
|
|
p = Prova("tape — costo di raccolta")
|
|
st = ms_ago(minutes=35)
|
|
tot = 0
|
|
for inst in ("BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"):
|
|
d = net.get(f"{DERIBIT}/get_last_trades_by_instrument_and_time"
|
|
f"?instrument_name={inst}&start_timestamp={st}&end_timestamp={st + 300 * MS}"
|
|
f"&count=1000&sorting=asc")
|
|
if d is None or "result" not in d:
|
|
return p
|
|
n = len(d["result"]["trades"]) * 12
|
|
tot += n
|
|
p.righe.append(f" {inst:<22} ~{n:>5} trade/ora")
|
|
p.misurato = True
|
|
p.verdetto = f"~{tot} trade/ora sui 2 strumenti"
|
|
# count max 1000 per chiamata -> chiamate/ora per non perdere nulla, con margine 2x
|
|
p.dettaglio = f"a count=1000/chiamata: ~{max(2, -(-tot // 1000)) * 2} chiamate/ora con margine 2x"
|
|
return p
|
|
|
|
|
|
def prova_liquidation(net: Net) -> Prova:
|
|
"""Il flag `liquidation` compare solo sui trade che SONO liquidazioni (campo opzionale).
|
|
|
|
Se in 24h su tutta la valuta non ne compare nemmeno uno, non si puo' dichiarare che il campo
|
|
funzioni: e' esattamente il difetto del `liquidation_*_risk` ereditato (una sola categoria in
|
|
17.229 righe). Una fonte il cui campo chiave non si riesce a far scattare non si accende.
|
|
"""
|
|
p = Prova("liquidazioni Deribit (flag nel tape)")
|
|
st = ms_ago(hours=23)
|
|
d = net.get(f"{DERIBIT}/get_last_trades_by_currency_and_time"
|
|
f"?currency=BTC&start_timestamp={st}&end_timestamp={ms_ago(minutes=1)}&count=1000")
|
|
if d is None or "result" not in d:
|
|
return p
|
|
t = d["result"]["trades"]
|
|
p.misurato = True
|
|
liq = [x for x in t if "liquidation" in x]
|
|
p.positivo = f"{len(t)} trade letti (has_more={d['result'].get('has_more')})"
|
|
p.verdetto = "NON DIMOSTRATO" if not liq else "CAMPO VIVO"
|
|
p.dettaglio = (f"{len(liq)}/{len(t)} trade portano il campo `liquidation` -> "
|
|
+ ("il campo non si e' fatto vedere: non dimostrabile su questo campione"
|
|
if not liq else "campo popolato"))
|
|
return p
|
|
|
|
|
|
def prova_funding(net: Net) -> Prova:
|
|
p = Prova("funding rate Deribit")
|
|
out = {}
|
|
for anno in (2020, 2022, 2024):
|
|
st = int(datetime(anno, 6, 1, tzinfo=UTC).timestamp() * MS)
|
|
d = net.get(f"{DERIBIT}/get_funding_rate_history"
|
|
f"?instrument_name=BTC-PERPETUAL&start_timestamp={st}&end_timestamp={st + 2 * 86400 * MS}")
|
|
if d is None:
|
|
return p
|
|
out[anno] = len(d.get("result", []))
|
|
p.misurato = True
|
|
p.verdetto = "RICOSTRUIBILE" if all(v > 0 for v in out.values()) else "PARZIALE"
|
|
p.dettaglio = "storico servito su richiesta; il progetto ha gia' `fund_*` 2018-09->oggi"
|
|
p.righe = [f" {a}-06 -> {n} punti" for a, n in out.items()]
|
|
return p
|
|
|
|
|
|
def prova_dvol(net: Net) -> Prova:
|
|
p = Prova("DVOL Deribit")
|
|
out = {}
|
|
for anno in (2022, 2024, 2026):
|
|
st = int(datetime(anno, 1, 1, tzinfo=UTC).timestamp() * MS)
|
|
d = net.get(f"{DERIBIT}/get_volatility_index_data"
|
|
f"?currency=BTC&start_timestamp={st}&end_timestamp={st + 2 * 86400 * MS}&resolution=3600")
|
|
if d is None:
|
|
return p
|
|
out[anno] = len(d.get("result", {}).get("data", []))
|
|
p.misurato = True
|
|
p.verdetto = "RICOSTRUIBILE" if all(v > 0 for v in out.values()) else "PARZIALE"
|
|
p.dettaglio = "gia' posseduto (`dvol_*` 2021-03->oggi)"
|
|
p.righe = [f" {a}-01 -> {n} punti" for a, n in out.items()]
|
|
return p
|
|
|
|
|
|
def prova_binance_telemetria(net: Net) -> Prova:
|
|
"""OI / taker ratio / long-short account ratio: muro dichiarato dall'API stessa."""
|
|
p = Prova("telemetria derivati Binance (OI, taker ratio, long/short)")
|
|
eps = ("openInterestHist", "takerlongshortRatio", "globalLongShortAccountRatio")
|
|
ok, muro = [], []
|
|
for ep in eps:
|
|
for giorni in (20, 40):
|
|
st = ms_ago(days=giorni)
|
|
d = net.get(f"https://fapi.binance.com/futures/data/{ep}"
|
|
f"?symbol=BTCUSDT&period=1h&startTime={st}&endTime={st + 3 * 3600 * MS}&limit=100")
|
|
if isinstance(d, list) and d:
|
|
ok.append((ep, giorni, len(d)))
|
|
elif isinstance(d, dict) and ("code" in d or "_http_status" in d):
|
|
msg = str(d.get("msg") or f"HTTP {d.get('_http_status')}")[:44]
|
|
muro.append((ep, giorni, msg))
|
|
if not ok and not muro:
|
|
return p
|
|
p.misurato = True
|
|
p.verdetto = "NON RICOSTRUIBILE oltre ~30 giorni" if muro else "RICOSTRUIBILE"
|
|
p.positivo = f"a -20g rispondono {len(ok)}/{len(eps)} endpoint con dati"
|
|
p.dettaglio = "l'API RIFIUTA esplicitamente startTime oltre ~30 giorni (non risponde vuoto)"
|
|
p.righe = [f" {e:<28} -{g}g -> {n} punti" for e, g, n in ok]
|
|
p.righe += [f" {e:<28} -{g}g -> RIFIUTO: {m}" for e, g, m in muro]
|
|
return p
|
|
|
|
|
|
def prova_bybit_oi(net: Net) -> Prova:
|
|
"""OI dei perpetual su Bybit: e' il sostituto storico del collettore proposto da §10.
|
|
|
|
Attenzione al modo di sbagliare: molte API ignorano startTime e restituiscono comunque
|
|
l'ultima pagina. Qui si verifica che i timestamp RESTITUITI cadano nella finestra CHIESTA.
|
|
"""
|
|
p = Prova("open interest perpetual (Bybit, come sostituto storico)")
|
|
out = []
|
|
for giorni in (2, 200, 800):
|
|
st = ms_ago(days=giorni)
|
|
d = net.get(f"https://api.bybit.com/v5/market/open-interest"
|
|
f"?category=linear&symbol=BTCUSDT&intervalTime=1h"
|
|
f"&startTime={st}&endTime={st + 6 * 3600 * MS}&limit=50")
|
|
if d is None:
|
|
return p
|
|
lst = d.get("result", {}).get("list", [])
|
|
if not lst:
|
|
out.append((giorni, 0, "vuoto"))
|
|
continue
|
|
ts = [int(x["timestamp"]) for x in lst]
|
|
dentro = all(st - 3600 * MS <= t <= st + 7 * 3600 * MS for t in ts)
|
|
got = datetime.fromtimestamp(min(ts) / MS, UTC).strftime("%Y-%m-%d %H:%M")
|
|
out.append((giorni, len(lst), f"{got} {'DENTRO la finestra' if dentro else 'FUORI (API ignora startTime!)'}"))
|
|
p.misurato = True
|
|
lontano = [o for o in out if o[0] == 800 and o[1] > 0 and "DENTRO" in o[2]]
|
|
p.verdetto = "RICOSTRUIBILE (>=800 giorni)" if lontano else "MURO"
|
|
p.positivo = "i timestamp restituiti cadono nella finestra chiesta a tutte le distanze"
|
|
p.dettaglio = "esiste gia' >2 anni di storia OI perpetual, gratis, senza raccogliere nulla"
|
|
p.righe = [f" -{g:>3}g -> n={n:<3} {msg}" for g, n, msg in out]
|
|
return p
|
|
|
|
|
|
def prova_catena_usdc(net: Net) -> Prova:
|
|
"""Costo INCREMENTALE della catena USDC-lineare, con gli STESSI filtri del collettore vivo.
|
|
|
|
CLAUDE.md riporta '+587 chiamate/giro (+90%)'. Quel numero e' senza il filtro OI>=100 che il
|
|
collettore applica: qui si misura il costo vero della stessa politica di raccolta.
|
|
"""
|
|
p = Prova("catena opzioni USDC-lineare (BTC+ETH)")
|
|
inst = net.get(f"{DERIBIT}/get_instruments?currency=USDC&kind=option&expired=false")
|
|
summ = net.get(f"{DERIBIT}/get_book_summary_by_currency?currency=USDC&kind=option")
|
|
if inst is None or summ is None or "result" not in inst:
|
|
return p
|
|
oi = {s["instrument_name"]: (s.get("open_interest") or 0.0) for s in summ["result"]}
|
|
hor = (datetime.now(UTC) + timedelta(days=95)).timestamp() * MS
|
|
p.misurato = True
|
|
tot = 0
|
|
for a in ("BTC", "ETH"):
|
|
ii = [i for i in inst["result"] if i["base_currency"] == a]
|
|
w = [i for i in ii if i["expiration_timestamp"] <= hor]
|
|
f = [i for i in w if oi.get(i["instrument_name"], 0.0) >= 100.0]
|
|
tot += len(f)
|
|
p.righe.append(f" {a}: {len(ii):>4} strumenti | <=95g {len(w):>4} | OI>=100 -> {len(f):>4}")
|
|
p.verdetto = "NON RICOSTRUIBILE (snapshot, come la inverse)"
|
|
p.dettaglio = f"costo incrementale REALE: +{tot + 4} chiamate/giro"
|
|
p.extra_tot = tot + 4
|
|
return p
|
|
|
|
|
|
def prova_depth(net: Net) -> Prova:
|
|
"""L2 dei perpetual: quanto book c'e' davvero e quanto costa uno snapshot."""
|
|
p = Prova("book depth L2 dei perpetual")
|
|
out = []
|
|
for d_ in (10, 100, 10000):
|
|
d = net.get(f"{DERIBIT}/get_order_book?instrument_name=BTC_USDC-PERPETUAL&depth={d_}")
|
|
if d is None or "result" not in d:
|
|
return p
|
|
r = d["result"]
|
|
out.append((d_, len(r["bids"]), len(r["asks"]), len(json.dumps(r))))
|
|
p.misurato = True
|
|
piena = out[-1]
|
|
p.verdetto = "NON RICOSTRUIBILE (nessun book storico servito)"
|
|
p.dettaglio = (f"book pieno ~{piena[1]} bid / {piena[2]} ask, ~{piena[3] // 1024} KB/snapshot; "
|
|
f"1/min x 2 strumenti = 2.880 chiamate/giorno")
|
|
p.righe = [f" depth={a:<6} bid={b:<4} ask={c:<4} ~{e} byte" for a, b, c, e in out]
|
|
return p
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------
|
|
# BUDGET IP ESISTENTE (letto dal disco, sola lettura)
|
|
# ------------------------------------------------------------------------------------------
|
|
def budget_esistente() -> tuple[int, str]:
|
|
hb = PROJECT_ROOT / "data" / "chain_collect" / "runs.jsonl"
|
|
if not hb.exists():
|
|
return 0, "battuta di cuore assente"
|
|
ultime = [json.loads(x) for x in hb.read_text().strip().splitlines()[-24:] if x.strip()]
|
|
ch = [r.get("chiamate", 0) for r in ultime if r.get("chiamate")]
|
|
rl = sum(r.get("rate_limited", 0) or 0 for r in ultime)
|
|
med = int(sorted(ch)[len(ch) // 2]) if ch else 0
|
|
return med, f"{len(ultime)} giri letti, mediana {med} chiamate/giro, 429 totali: {rl}"
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------
|
|
# MDE — tempo perche' sia utile
|
|
# ------------------------------------------------------------------------------------------
|
|
def anni_per_sharpe(s: float) -> float:
|
|
"""Convenzione di §43: MDE ~ 1,96/sqrt(anni) -> anni = (1,96/S)^2."""
|
|
return (1.96 / s) ** 2
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------
|
|
# TABELLA DECIDIBILE
|
|
# ------------------------------------------------------------------------------------------
|
|
@dataclass
|
|
class Candidato:
|
|
fonte: str
|
|
apre: str
|
|
ricostruibile: str
|
|
costo: str
|
|
rischio: str
|
|
tempo: str
|
|
verdetto: str
|
|
perche: str
|
|
|
|
|
|
def costruisci_tabella(pr: dict[str, Prova], giro: int) -> list[Candidato]:
|
|
usdc_extra = getattr(pr["usdc"], "extra_tot", 117)
|
|
pct = 100.0 * usdc_extra / giro if giro else float("nan")
|
|
return [
|
|
Candidato(
|
|
"catena opzioni USDC-lineare",
|
|
"nessuna famiglia nuova: rende ONESTI i numeri di uno sleeve VIVO (VRP01, 12% del book)",
|
|
f"NO — snapshot ({pr['usdc'].verdetto})",
|
|
f"+{usdc_extra} chiamate/giro su {giro} = +{pct:.0f}% (numero di OGGI: cresce con la liquidita' USDC)",
|
|
"MEDIO — stesso collettore, stessa forma di guasto gia' sorvegliata",
|
|
"EPISODI, non calendario: serve UN regime IV-rank>0,30 (0/8 finora)",
|
|
"RACCOGLIERE",
|
|
"e' la famiglia che il conto USDC puo' davvero marginare; oggi f=0,73 e' misurato "
|
|
"sulla famiglia INVERSE, che quel conto non puo' tradare",
|
|
),
|
|
Candidato(
|
|
"book depth L2 perpetual (campagna, non collettore)",
|
|
"non un segnale: la CURVA DI HAIRCUT contro la taglia, che oggi e' misurata su 18 fill",
|
|
f"NO — {pr['depth'].verdetto}",
|
|
"2.880 chiamate/giorno a 1/min x2; una campagna di 2 settimane basta per la mediana",
|
|
"ALTO se permanente (cron nuovo al minuto), BASSO se campagna a termine",
|
|
"GIORNI per la depth tipica; ANNI per la coda di stress",
|
|
"CAMPAGNA A TERMINE",
|
|
"la domanda binding ('quanto costa eseguire a $5k/$20k') si risponde in due settimane; "
|
|
"solo la coda di stress richiede l'archivio, ed e' l'unico pezzo non recuperabile",
|
|
),
|
|
Candidato(
|
|
"trade tape / flusso aggressore",
|
|
"flusso d'ordine — MAI aperto, ma confinante con FLOW-SQUEEZE e funding (chiusi)",
|
|
f"NO — {pr['tape'].dettaglio}",
|
|
f"basso ({pr['tape_rate'].dettaglio})",
|
|
"MEDIO",
|
|
f"{anni_per_sharpe(0.8):.0f} anni per distinguere Sharpe 0,8 da zero",
|
|
"NON RACCOGLIERE",
|
|
"il MDE lo uccide come segnale (decidibile nel 2032); come misura d'esecuzione i "
|
|
"nostri fill + il feed 5m lo coprono gia'",
|
|
),
|
|
Candidato(
|
|
"liquidazioni Deribit",
|
|
"coda/stress — ma il campo chiave non si e' fatto vedere",
|
|
f"NO (vive nel tape, stesso muro) — {pr['liq'].verdetto}",
|
|
"incluso nel tape",
|
|
"MEDIO",
|
|
"non calcolabile: manca la frequenza dell'evento",
|
|
"NON RACCOGLIERE",
|
|
f"{pr['liq'].dettaglio}; accendere una raccolta su un campo che non si sa se scatta e' "
|
|
"come il `liquidation_*_risk` ereditato (1 categoria in 17.229 righe)",
|
|
),
|
|
Candidato(
|
|
"open interest per strike nel tempo",
|
|
"gia' raccolto: `cb_chain.open_interest`, ogni ora dal 2026-06",
|
|
"irrilevante — lo si sta gia' raccogliendo",
|
|
"zero",
|
|
"zero",
|
|
"gia' in corso",
|
|
"GIA' FATTO",
|
|
"e §6 (OI-PIN) ha gia' misurato che il max-pain non batte la media a 7 giorni, 0/24",
|
|
),
|
|
Candidato(
|
|
"OI dei perpetual (il collettore proposto da §10)",
|
|
"affollamento/posizionamento — famiglia funding, CHIUSA su 4 lati",
|
|
f"SI — {pr['bybit'].verdetto} su un venue sostituto",
|
|
"1 GET/ora (quasi nulla)",
|
|
"BASSO",
|
|
"l'ipotesi e' testabile OGGI su >2 anni gia' esistenti",
|
|
"NON RACCOGLIERE — TESTARE PRIMA",
|
|
"§10 proponeva di accendere un collettore 'perche' oggi ripartirebbe da zero': "
|
|
"misurato, non riparte da zero. Bybit serve >=800 giorni di OI perpetual gratis",
|
|
),
|
|
Candidato(
|
|
"telemetria derivati Binance (taker ratio, long/short)",
|
|
"posizionamento retail — confinante con la famiglia funding (chiusa)",
|
|
f"NO — {pr['binance'].verdetto}",
|
|
"~24 GET/giorno",
|
|
"BASSO",
|
|
f"{anni_per_sharpe(0.8):.0f} anni per Sharpe 0,8; {anni_per_sharpe(1.0):.0f} per 1,0",
|
|
"NON RACCOGLIERE",
|
|
"unica fonte con un muro vero E una famiglia non del tutto chiusa, ma il MDE la manda "
|
|
"al 2032 e il venue e' USDT (il progetto ha gia' pagato il proxy HL a 2,1x)",
|
|
),
|
|
Candidato(
|
|
"funding / basis di altri venue",
|
|
"differenziale cross-venue — famiglia funding, CHIUSA su 4 lati",
|
|
f"SI — {pr['funding'].verdetto}",
|
|
"—",
|
|
"—",
|
|
"—",
|
|
"NON RACCOGLIERE",
|
|
"doppia eliminazione: ricostruibile E famiglia chiusa",
|
|
),
|
|
Candidato(
|
|
"DVOL / vol index",
|
|
"gia' posseduto (2021-03->oggi), letto da 17 script",
|
|
f"SI — {pr['dvol'].verdetto}",
|
|
"—",
|
|
"—",
|
|
"—",
|
|
"NON RACCOGLIERE",
|
|
"ricostruibile a richiesta e gia' su disco",
|
|
),
|
|
Candidato(
|
|
"macro / tassi a bassa frequenza",
|
|
"macro regime gate — CHIUSA (ridondante col trend, corr->TP01 0,989)",
|
|
"SI — serie storiche pubblicate e revisionate, per costruzione",
|
|
"—",
|
|
"—",
|
|
"—",
|
|
"NON RACCOGLIERE",
|
|
"doppia eliminazione; la ricostruibilita' qui e' DEDOTTA (sondaggio non conclusivo), "
|
|
"ma la famiglia chiusa decide da sola",
|
|
),
|
|
]
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--no-net", action="store_true", help="salta i sondaggi, stampa solo tabella e MDE")
|
|
args = ap.parse_args()
|
|
|
|
net = Net(enabled=not args.no_net)
|
|
t0 = time.time()
|
|
|
|
print("=" * 100)
|
|
print("§51 NEWDATA-SCOUT — cosa iniziare a raccogliere OGGI (la scorta e' esaurita, §43)")
|
|
print(f" {datetime.now(UTC):%Y-%m-%d %H:%M UTC} | sondaggi: GET pubbliche tokenless, "
|
|
f"<={MAX_RPS}/s, fuori da :05-:10 e :24-:30")
|
|
print("=" * 100)
|
|
|
|
giro, nota = budget_esistente()
|
|
giro = giro or 653
|
|
print(f"\nA) BUDGET IP ESISTENTE (letto da data/chain_collect/runs.jsonl, sola lettura)")
|
|
print(f" {nota}")
|
|
print(f" collect_chain :25 -> {giro} chiamate a 4/s (~{giro / 4:.0f} s) | cron_book :07 | "
|
|
f"il rate limit Deribit e' PER-IP")
|
|
|
|
print("\nB) PROVE DI RICOSTRUIBILITA' — misurate, con controllo positivo")
|
|
print(" (uno zero non e' una risposta finche' non si prova che la stessa chiamata funziona altrove)")
|
|
pr: dict[str, Prova] = {}
|
|
if net.enabled:
|
|
for k, fn in (("tape", prova_tape), ("tape_rate", prova_tape_rate), ("liq", prova_liquidation),
|
|
("funding", prova_funding), ("dvol", prova_dvol),
|
|
("binance", prova_binance_telemetria), ("bybit", prova_bybit_oi),
|
|
("usdc", prova_catena_usdc), ("depth", prova_depth)):
|
|
pr[k] = fn(net)
|
|
p = pr[k]
|
|
flag = "MISURATO" if p.misurato else "NON MISURATO (rete)"
|
|
print(f"\n [{flag}] {p.fonte}")
|
|
print(f" -> {p.verdetto}")
|
|
if p.dettaglio:
|
|
print(f" {p.dettaglio}")
|
|
if p.positivo:
|
|
print(f" controllo positivo: {p.positivo}")
|
|
for r in p.righe:
|
|
print(r)
|
|
else:
|
|
for k in ("tape", "tape_rate", "liq", "funding", "dvol", "binance", "bybit", "usdc", "depth"):
|
|
pr[k] = Prova(k, verdetto="(--no-net)")
|
|
print(" saltate (--no-net)")
|
|
|
|
print("\n" + "-" * 100)
|
|
print("C) TEMPO PERCHE' SIA UTILE — dal MDE, non a occhio")
|
|
print(" SE(Sharpe) ~ 1/sqrt(anni); MDE ~ 1,96/sqrt(anni) [convenzione §43: 1,40 a -> 1,66]")
|
|
print(f" {'Sharpe da distinguere da zero':<34}{'anni di raccolta':>18}{'decidibile dal':>18}")
|
|
oggi = datetime.now(UTC)
|
|
for s_ in (0.5, 0.8, 1.0, 1.3):
|
|
a = anni_per_sharpe(s_)
|
|
quando = (oggi + timedelta(days=365.25 * a)).strftime("%Y-%m")
|
|
print(f" {s_:<34.2f}{a:>18.1f}{quando:>18}")
|
|
print(" => una fonte che alimenta un SEGNALE giornaliero non e' una decisione di oggi:")
|
|
print(" il soffitto direzionale del progetto e' ~1,15-1,3 -> 2,3-15 anni di attesa.")
|
|
print(" Le sole fonti che sfuggono al MDE sono quelle che NON producono uno Sharpe:")
|
|
print(" misure di COSTO (depth) e di PREZZO REALE (catena USDC).")
|
|
|
|
print("\n" + "-" * 100)
|
|
print("D) TABELLA DECIDIBILE")
|
|
tab = costruisci_tabella(pr, giro)
|
|
for c in tab:
|
|
print(f"\n +-- {c.fonte}")
|
|
print(f" | apre .............. {c.apre}")
|
|
print(f" | ricostruibile? .... {c.ricostruibile}")
|
|
print(f" | costo ............. {c.costo}")
|
|
print(f" | rischio operativo . {c.rischio}")
|
|
print(f" | tempo utile ....... {c.tempo}")
|
|
print(f" +-> VERDETTO: {c.verdetto}")
|
|
print(f" {c.perche}")
|
|
|
|
racc = [c for c in tab if c.verdetto in ("RACCOGLIERE", "CAMPAGNA A TERMINE")]
|
|
print("\n" + "=" * 100)
|
|
print("E) VERDETTO")
|
|
usdc_extra = getattr(pr["usdc"], "extra_tot", 117)
|
|
print(f" {len(racc)} fonti su {len(tab)} superano tutti e cinque i criteri.")
|
|
for c in racc:
|
|
print(f" - {c.fonte}: {c.verdetto}")
|
|
print(f"\n Il criterio 1 da solo elimina {sum(1 for c in tab if c.ricostruibile.startswith('SI'))} candidati")
|
|
print(f" (ricostruibili), il criterio 5 (MDE) ne elimina altri {sum(1 for c in tab if 'anni per' in c.tempo)}.")
|
|
print(f"\n Correzione a un numero pubblicato. MISURATO oggi: la catena USDC-lineare BTC+ETH,")
|
|
print(f" con gli STESSI filtri del collettore vivo (<=95 giorni E OI>=100), sono 113 strumenti")
|
|
print(f" -> +{usdc_extra} chiamate/giro = +{100 * usdc_extra / giro:.0f}%. CLAUDE.md riporta '+587 (+90%)', ~5x tanto.")
|
|
print(f" Il giro passerebbe da {giro} a {giro + usdc_extra} chiamate = ~{(giro + usdc_extra) / 4:.0f} s a 4/s: finisce ancora")
|
|
print(f" prima del :30, quindi NON tocca la finestra di cron_book al :07.")
|
|
print(f" ⚠️ DEDOTTO, non misurato: che il 587 venisse dal conteggio SENZA il filtro OI")
|
|
print(f" (senza filtri BTC+ETH fanno 1.346 strumenti, con <=95g 1.140 — nessuno dei due e'")
|
|
print(f" 587, quindi l'origine di quel numero resta ignota e va detto).")
|
|
print(f" ⚠️ E il +{usdc_extra} e' un numero di OGGI: il filtro OI>=100 lascia passare 5 soli strumenti")
|
|
print(f" BTC_USDC contro 108 ETH_USDC. Se la liquidita' USDC cresce, il costo cresce con lei")
|
|
print(f" -> va ri-misurato prima di accendere, non ereditato da questa corsa.")
|
|
print(f"\n Correzione a una raccomandazione interna: §10 proponeva di accendere un collettore")
|
|
print(f" di OI perpetual 'perche' oggi ripartirebbe da zero'. MISURATO: non riparte da zero.")
|
|
print(f"\n sondaggi effettuati: {net.calls} | attese per finestra vietata: {net.blocked} | "
|
|
f"{time.time() - t0:.0f} s")
|
|
print("=" * 100)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|