Files
PythagorasGoal/scripts/research/r0823_newdata_scout.py
T
Adriano Dal Pastro e2aee5730b research(newdata): raccogliere 2 fonti su 10 — il criterio di ricostruibilita' ne uccide 4 da solo
§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
2026-08-23 01:46:16 +00:00

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())