Files
PythagorasGoal/tests/test_collect_chain.py
Adriano Dal Pastro d55eb13533 feat(chain): assorbita la raccolta catena opzioni, cerbero-bite dismesso
cerbero-bite viene eliminato. L'unica sua parte irreversibile e' il DATO:
una catena opzioni non si ricostruisce a posteriori (Deribit non serve book
storici, non c'e' un secondo venue). Il codice si riscrive; le ore non
raccolte no.

ASSORBITO
- scripts/live/collect_chain.py + scripts/cron_chain.sh (cron 25 * * * *):
  raccolta propria, ~570 strumenti/giro, ~3 min.
- scripts/analysis/import_cb_archive.py: archivio 1.23M righe (2026-05-01+)
  + market_snapshots 17.402 righe (2026-03-26+: dealer gamma, gamma flip,
  rischio liquidazioni, funding cross — dati che non abbiamo altrove).
- snapshot sqlite integrale in /opt/docker/backups/manual/ (SHA256).

NON ASSORBITO, con motivo: motore credit-spread ETH (regola "niente
short-vol da modello in deploy", conto a $52 contro minimo $720), GUI, kill
switch/dead-man/audit (abbiamo venue_watch/edge_watch/monitor_health/
fee_watch), dvol_history (fetch_dvol.py ha storia PIU' LUNGA: 2020+ contro
2026-05), decisions/positions (0 posizioni).

TRE DIFETTI DI BITE NON REPLICATI, tutti misurati il 30/07:
1. una chiamata per strumento invece di due (get_order_book?depth=3 da' gia'
   quote+greche+IV+OI+book+underlying) + prefiltro OI in una chiamata sola:
   551 -> ~290 chiamate per asset;
2. pacing invece di raffica. Il carico non e' mai stato il problema: 570
   chiamate/ora = 0.16/s DISTRIBUITE; bite le sparava in 26s (~44/s) e si
   auto-saturava il rate limit per-IP (12.186 risposte 429 in 26h, 96% al
   minuto :00). Primo giro reale: 574 chiamate, 0 risposte 429. Il minuto :25
   e' scelto: :00 era la raffica, :07 e' cron_book (feed 5m di SKH01).
3. quote_status esplicito {ok, no_quote, error} e book_depth NULL su errore
   mai 0. "Book vuoto" e "chiamata fallita" sono cose diverse: e' per questo
   che il guasto del 29/07 (50% di quote perse, 38 ore) non produsse alcun
   segnale. Le righe ereditate restano 'unknown': bite non lo registrava e a
   posteriori non e' ricostruibile.

Battuta di cuore in data/chain_collect/runs.jsonl anche a giro fallito,
sorvegliata da monitor_health (1h, max_age 3h): un collettore fermo non
produce niente, e il niente si legge come "nessun dato quel giorno".

Difetto trovato per strada: due formati ISO nella stessa colonna (92 righe
di backfill senza microsecondi). pd.to_datetime senza `format` ne inferisce
uno solo e manda gli altri a NaT -> il dropna a valle li toglieva in
silenzio, e la serie di contesto perdeva 5 settimane slittando dal 26/03 al
01/05. Corretto con format="ISO8601" e scarto RUMOROSO.

Book, pesi, config, strategia INVARIATI. 537 test verdi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:14:34 +00:00

140 lines
5.6 KiB
Python

"""Test del collettore catena opzioni (successore di cerbero-bite).
Il collettore esiste per NON ripetere tre difetti misurati su bite il 2026-07-30. I test
sorvegliano esattamente quei tre, perche' sono difetti SILENZIOSI: nessuno di loro fa fallire un
giro, tutti e tre corrompono una serie irrecuperabile.
1. una chiamata fallita non deve diventare una riga che sembra un dato;
2. "book vuoto" e "chiamata fallita" non devono collassare sullo stesso valore;
3. il pacing deve esserci davvero (bite si auto-saturava il rate limit per-IP).
Nessun test tocca la rete.
"""
from __future__ import annotations
import sys
import time
from datetime import UTC, datetime
from pathlib import Path
import pandas as pd
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "live"))
from collect_chain import Budget, _depth_top3, snapshot_row, write # noqa: E402
TS = datetime(2026, 7, 30, 20, 0, tzinfo=UTC)
INST = {
"instrument_name": "ETH-31JUL26-1750-P", "base_currency": "ETH",
"strike": 1750.0, "option_type": "put",
"expiration_timestamp": int(pd.Timestamp("2026-07-31T08:00:00Z").timestamp() * 1000),
}
def _ob(bid=0.016, ask=0.0175, bids=((0.016, 10.0),), asks=((0.0175, 12.0),)):
return {
"best_bid_price": bid, "best_ask_price": ask, "mark_iv": 52.3,
"greeks": {"delta": -0.28, "gamma": 0.001, "theta": -1.2, "vega": 0.9},
"open_interest": 500.0, "stats": {"volume": 31.0},
"bids": [list(b) for b in bids], "asks": [list(a) for a in asks],
"underlying_price": 1923.4, "index_price": 1922.7,
}
# ------------------------------------------------- 1. una chiamata fallita non e' un dato
def test_chiamata_fallita_marcata_error_e_senza_quota():
r = snapshot_row(INST, None, TS)
assert r["quote_status"] == "error"
assert r["bid"] is None and r["ask"] is None and r["iv"] is None
assert r["instrument_name"] == INST["instrument_name"], (
"la riga si scrive lo stesso: cio' che cambia e' che dichiara di non avere il dato"
)
def test_profondita_e_NULL_su_errore_mai_zero():
"""Il difetto di bite: depth 0 su chiamata fallita = indistinguibile da 'book vuoto'.
Qui su errore la profondita' non esiste, e un book davvero vuoto vale 0."""
assert snapshot_row(INST, None, TS)["book_depth_top3"] is None
vuoto = snapshot_row(INST, _ob(bid=0.0, ask=0.0, bids=(), asks=()), TS)
assert vuoto["book_depth_top3"] == 0.0
assert vuoto["quote_status"] == "no_quote"
def test_book_vuoto_e_chiamata_fallita_sono_stati_DIVERSI():
"""Controllo positivo dell'idea stessa del collettore: se questi due collassassero, la
diagnostica del 29/07 (50% di quote perse per rate-limit) sarebbe di nuovo impossibile."""
fallita = snapshot_row(INST, None, TS)["quote_status"]
vuoto = snapshot_row(INST, _ob(bid=0.0, ask=0.0, bids=(), asks=()), TS)["quote_status"]
assert fallita == "error" and vuoto == "no_quote" and fallita != vuoto
def test_un_solo_lato_quotato_e_comunque_ok():
"""Un book con solo il bid e' un mercato reale (illiquido), non un guasto."""
r = snapshot_row(INST, _ob(ask=0.0, asks=()), TS)
assert r["quote_status"] == "ok"
assert r["bid"] is not None and r["ask"] is None
assert r["mid"] is None, "senza un lato il mid non esiste: non si inventa"
def test_campi_valorizzati_quando_la_quota_c_e():
r = snapshot_row(INST, _ob(), TS)
assert r["quote_status"] == "ok"
assert r["mid"] == pytest.approx((0.016 + 0.0175) / 2)
assert r["delta"] == pytest.approx(-0.28) and r["iv"] == pytest.approx(52.3)
assert r["book_depth_top3"] == pytest.approx(22.0)
assert r["underlying_price"] == pytest.approx(1923.4)
assert r["option_type"] == "P"
def test_depth_top3_somma_solo_i_primi_tre_livelli():
assert _depth_top3([[1, 5], [2, 5], [3, 5], [4, 100]]) == pytest.approx(15.0)
assert _depth_top3([]) == 0.0
assert _depth_top3(None) is None
# ------------------------------------------------- 3. il pacing deve esistere
def test_il_budget_impone_davvero_una_cadenza():
"""Bite non aveva pacing: ~44 chiamate/s per 26s. Qui la cadenza e' una proprieta' misurabile."""
b = Budget(rps=20.0)
t0 = time.monotonic()
for _ in range(6):
b.wait()
dur = time.monotonic() - t0
assert dur >= 5 / 20.0 * 0.8, f"6 chiamate a 20/s non possono durare {dur:.3f}s"
def test_il_budget_conta_i_rate_limit_e_gli_errori():
b = Budget(rps=100.0)
b.note_error("boom")
b.rate_limited += 2
assert b.errors == 1 and b.rate_limited == 2 and b.err_samples == ["boom"]
# ------------------------------------------------- scrittura
def test_write_deduplica_e_tiene_l_ultima_osservazione(tmp_path, monkeypatch):
import collect_chain as CC
monkeypatch.setattr(CC, "STORE", tmp_path)
a = pd.DataFrame([snapshot_row(INST, _ob(bid=0.010), TS)])
b = pd.DataFrame([snapshot_row(INST, _ob(bid=0.020), TS)]) # stesso (ts, strumento)
CC.write(a, TS)
p = CC.write(b, TS)
out = pd.read_parquet(p)
assert len(out) == 1, "stesso istante e stesso strumento = una riga sola"
assert out.iloc[0]["bid"] == pytest.approx(0.020), "vince l'osservazione piu' recente"
def test_write_separa_i_giorni(tmp_path, monkeypatch):
import collect_chain as CC
monkeypatch.setattr(CC, "STORE", tmp_path)
CC.write(pd.DataFrame([snapshot_row(INST, _ob(), TS)]), TS)
dopo = TS.replace(day=31)
CC.write(pd.DataFrame([snapshot_row(INST, _ob(), dopo)]), dopo)
assert sorted(p.name for p in tmp_path.glob("*.parquet")) == \
["2026-07-30.parquet", "2026-07-31.parquet"]