From 861cc7fc273d28394a650b2a9e37b859f6c7d349 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Wed, 2 Sep 2026 14:38:53 +0000 Subject: [PATCH] =?UTF-8?q?debito=207:=20la=20cadenza=20del=20libro=20e'?= =?UTF-8?q?=20ORARIA=20=E2=80=94=20docstring=20riscritto,=20tre=20fonti=20?= =?UTF-8?q?tenute=20d'accordo=20da=20un=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Il docstring di book_execute.py prescriveva «ogni ~230 minuti» mentre il cron gira ogni ora (47 * * * *). Era il docstring a sbagliare: sulla riga 4h BTC raddoppia gli scatti del disaster-SL rotolante (r0823_sl_anchor.py), e chi avesse "corretto" il cron verso il docstring avrebbe spostato il libro sulla riga peggiore. - scripts/live/book_execute.py: CADENZA: ORARIA, la riga di crontab, la ragione (giro idempotente, latenza SKH01 <=1h, ri-ancoraggio orario) e il divieto esplicito con la data. - tests/test_book_cadenza.py (10): deriva e confronta docstring, intestazione di cron_book.sh e crontab installata (crontab -l; SALTATO se illeggibile, non verde); parser a 5 campi solo per cadenze regolari; 60 < 230 < 240; minuto != :00. - docs: CLAUDE.md §5.7 chiuso, §13 conteggio 842; memoria 40; diario 02/09b. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01RziUCB336YPUUyDJ29x4Ke --- CLAUDE.md | 15 ++- .../2026-09-02b-debito-7-cadenza-docstring.md | 45 +++++++ docs/memory/40-produzione-e-deploy.md | 17 +++ scripts/live/book_execute.py | 13 +- tests/test_book_cadenza.py | 121 ++++++++++++++++++ 5 files changed, 204 insertions(+), 7 deletions(-) create mode 100644 docs/diary/2026-09-02b-debito-7-cadenza-docstring.md create mode 100644 tests/test_book_cadenza.py diff --git a/CLAUDE.md b/CLAUDE.md index 70bac81..09775c6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -282,9 +282,16 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per 6. ⚠️ **TLT ha 13,5 anni di storia in meno** (parte 2016-02 invece del 2002) → **GTAA01 gira su CINQUE gambe prima del 2016**, e l'assente e' quella obbligazionaria. Non e' un fetch da rifare (IB ritorna 0 barre). ⇒ in-sample e hold-out di GTAA01 **non sono la stessa strategia**. -7. ⚠️ **Il docstring di `book_execute.py` prescrive "ogni ~230 minuti", il cron gira OGNI ORA.** - **Il docstring e' sbagliato e la configurazione che gira e' quella giusta** — chi lo "correggesse" - sposterebbe il libro sulla riga 4h, dove BTC **raddoppia** gli scatti del disaster-SL. +7. ✅ **RIPARATO (2026-09-02).** Il docstring di `book_execute.py` prescriveva "ogni ~230 minuti" + mentre il cron gira **ogni ora** (`47 * * * *`): era il docstring a essere sbagliato, e chi lo + avesse "corretto" nel verso del cron avrebbe spostato il libro sulla riga 4h, dove BTC + **raddoppia** gli scatti del disaster-SL (`r0823_sl_anchor.py`). Ora il docstring dichiara + **CADENZA: ORARIA** con la ragione (giro idempotente: latenza SKH01 ≤1h, ri-ancoraggio del + rotolante ogni ora) e il divieto esplicito. **`tests/test_book_cadenza.py` (10 test)** tiene + d'accordo le tre dichiarazioni — docstring, riga in `cron_book.sh`, **crontab installata** + (letta con `crontab -l`; dove non e' leggibile il test si dichiara SALTATO, non verde) — e + verifica che la cadenza dichiarata sia 60 min, sotto la griglia 230m e lontana dai 240 della + riga peggiore. Diario `2026-09-02b-debito-7-cadenza-docstring.md`. 8. ⚠️ **Il Rulebook Deribit** (ADL, perdita socializzata, *emergency powers*, conti dormienti) **non lo sorveglia nessuno**; `MAINT_GRACE_HOURS`=2 **presume** gli annunci invece di leggerli. ✅ *Ridotto in parte il 2026-08-25:* `src/live/venue_probe.py` interroga l'API **pubblica** @@ -560,7 +567,7 @@ uv run python scripts/live/trades_db.py --report # stato libro d uv run python scripts/live/trades_db.py --reconcile # incrocio delle 3 fonti sui fill uv run python scripts/live/journal.py # voce del giorno (numeri + lettura) uv run python scripts/live/analista.py --secco # analisi del giorno, senza salvare -uv run pytest # test (832, tutti verdi al 02/09) +uv run pytest # test (842, tutti verdi al 02/09) ``` ```python diff --git a/docs/diary/2026-09-02b-debito-7-cadenza-docstring.md b/docs/diary/2026-09-02b-debito-7-cadenza-docstring.md new file mode 100644 index 0000000..aca1f5f --- /dev/null +++ b/docs/diary/2026-09-02b-debito-7-cadenza-docstring.md @@ -0,0 +1,45 @@ +# 2026-09-02 — Debito 7: il docstring diceva 230 minuti, il cron gira ogni ora + +*Scritto il 2026-09-02 alle 14:45Z. Cadenza misurata dal log (`r0823_sl_anchor.py`: 1.443 giri in +1.442 ore) e dalla crontab installata, letta dal test.* + +## Il difetto + +| fonte | cosa diceva | dal | +|---|---|---| +| `scripts/live/book_execute.py`, docstring | «va lanciato ogni ~230 minuti» | 20/06 | +| `scripts/cron_book.sh`, intestazione | cadenza ORARIA, `47 * * * *` | 25/08 (prima `:07`, sempre orario) | +| crontab della VPS | `47 * * * *` | 25/08 | + +Era il **docstring** a sbagliare. E non era una svista innocua: sulla riga 4h (la più vicina ai 230 +minuti) BTC **raddoppia** gli scatti del disaster-SL rotolante (2 contro 1; a 24h ETH arriva a 5, con +2 in 30 giorni). Il rischio era un lettore diligente che "allineasse" il cron al docstring. + +## La riparazione + +1. **Docstring riscritto**: `CADENZA: ORARIA`, la riga di crontab citata, la ragione (giro + idempotente → girare più fitto della griglia non costa ordini; latenza ≤1h sugli ingressi/uscite + software di SKH01; ri-ancoraggio orario del rotolante) e il divieto esplicito, con la data. +2. **`tests/test_book_cadenza.py`, 10 test.** P1: non ridichiara «60 minuti», **deriva** le tre + fonti e le confronta fra loro: + - il docstring non contiene più «ogni ~230» e dichiara ORARIA; + - la riga di crontab citata nel docstring è quella dichiarata in `cron_book.sh`; + - la riga dichiarata è oraria (parser a 5 campi, solo casi regolari, rifiuta il resto), + sotto i 230 della griglia e lontana dai 240 della riga peggiore, e fuori dal minuto `:00`; + - **la crontab installata** (`crontab -l`) coincide con la dichiarata. Dove la crontab non è + leggibile il test è **saltato**, non verde (P5). Sulla VPS oggi gira e passa. + +## Numeri + +| | | +|---|---| +| test nuovi | 10, tutti verdi; suite dei moduli che toccano `book_execute`: 231 verdi | +| cadenza dichiarata / installata | 60 min / 60 min | +| griglia SKH01 | 230 min (la riga da non usare) | +| riga peggiore misurata | 240 min (BTC 2 scatti contro 1) | + +## Cosa NON ho fatto + +- Non ho toccato il cron né `cron_book.sh`: la configurazione che gira era quella giusta. +- Non ho aggiunto un sorvegliante a runtime sulla cadenza (contare i giri per ora nel log): il + giornale già stampa «giri mancanti» ogni giorno, e il test copre la deriva di configurazione. diff --git a/docs/memory/40-produzione-e-deploy.md b/docs/memory/40-produzione-e-deploy.md index e410b95..477ba35 100644 --- a/docs/memory/40-produzione-e-deploy.md +++ b/docs/memory/40-produzione-e-deploy.md @@ -915,6 +915,23 @@ Sulle 4 finestre osservate 3 sono rientrate entro l'ora → il `:47` ne avrebbe **Se al prossimo martedì il `:47` becca comunque la manutenzione, la previsione è sbagliata** e lo slot non è quello descritto in `venue_probe.RELEASE_*`: rileggerlo prima di spostare ancora. +### La cadenza è ORARIA, e ora lo dicono tutte e tre le fonti (debito §5.7, chiuso il 2026-09-02) + +Il docstring di `book_execute.py` prescriveva «ogni ~230 minuti» (la griglia di SKH01) mentre il cron +gira ogni ora. Non era cosmetico: `r0823_sl_anchor.py` misura che l'accumulo di scatti del disaster-SL +rotolante è funzione della **cadenza del cron** (1h: BTC 0 / ETH 1 in 7-8 anni · 4h: BTC 2 · 24h: ETH 5 +con 2 in 30 giorni), quindi chi avesse "corretto" il cron verso il docstring avrebbe spostato il libro +sulla riga peggiore. La riparazione è il docstring stesso — cadenza ORARIA, la ragione (giro +idempotente: girare più fitto della griglia non costa ordini, e compra latenza ≤1h per gli +ingressi/uscite software di SKH01 e un ri-ancoraggio orario del rotolante) e il divieto scritto — +più `tests/test_book_cadenza.py`, che **deriva** (P1) le tre dichiarazioni e le confronta: la riga +citata nel docstring, quella dichiarata nell'intestazione di `cron_book.sh` (`47 * * * *`) e la +crontab **installata** (`crontab -l`; se non è leggibile il test è SALTATO, non verde — P5). Il +parser accetta solo espressioni regolari e rifiuta il resto. Vincolo del minuto tondo verificato +(≠ `:00`). La regola generale è la stessa del `:47`: **una cadenza si difende nella sua ragione, e +un commento che dichiara una cadenza diversa da quella che gira si paga quando qualcuno lo "corregge" +nel verso sbagliato.** + ### Cosa resta scoperto 1. **La sonda dice di chi è il guasto, non lo aggira.** Col gateway giù il libro continua ad diff --git a/scripts/live/book_execute.py b/scripts/live/book_execute.py index 217bf25..1326297 100644 --- a/scripts/live/book_execute.py +++ b/scripts/live/book_execute.py @@ -10,9 +10,16 @@ DOPPIO GATE DI SICUREZZA (entrambi necessari per inviare ordini reali): Senza entrambi e' un DRY-RUN (stampa il piano, NON invia). Reconciliation dopo ogni ordine; log in data/live/book_executions.jsonl. -⚠️ CADENZA: SKH01 decide su griglia 230m -> questo script va lanciato ogni ~230 minuti con la feed -fresca all'ultima barra chiusa (NON il cron giornaliero, che mancherebbe gli ingressi). Gli exit di -SKH sono SOFTWARE (latenza fino a fine barra 230m); solo il disaster-SL (-30%) e' on-book. +⚠️ CADENZA: ORARIA — `47 * * * *` in `scripts/cron_book.sh` (il minuto e' spiegato li'). SKH01 +decide su griglia 230m, ma il giro e' IDEMPOTENTE (riconcilia al target netto corrente: se non +cambia nulla -> HOLD), quindi girare piu' fitto della griglia non costa ordini e compra due cose +misurate: (1) gli ingressi/uscite SOFTWARE di SKH01 arrivano con latenza <=1h invece che fino a +230m; (2) il disaster-SL rotolante (-30%, l'unico on-book) si ri-ancora ogni ora — a cadenza 1h in +7-8 anni BTC 0 scatti / ETH 1, sulla riga 4h BTC ne fa 2 (`r0823_sl_anchor.py`). +🚨 NON "correggere" la cadenza a ~230 minuti: fino al 2026-09-02 questo docstring lo prescriveva, +ed era il docstring a essere sbagliato, non il cron (debito §5.7). `tests/test_book_cadenza.py` +tiene d'accordo docstring, `cron_book.sh` e crontab installata. NON il cron giornaliero: le entrate +di SKH01 le mancherebbe. uv run python scripts/live/book_execute.py # DRY-RUN (piano, nessun ordine) uv run python scripts/live/book_execute.py --execute # esegue SOLO se execution_enabled=true diff --git a/tests/test_book_cadenza.py b/tests/test_book_cadenza.py new file mode 100644 index 0000000..6c38883 --- /dev/null +++ b/tests/test_book_cadenza.py @@ -0,0 +1,121 @@ +"""La cadenza del libro live e' ORARIA, e le tre dichiarazioni devono restare d'accordo. + +Debito §5.7 (chiuso il 2026-09-02): il docstring di `book_execute.py` prescriveva «ogni ~230 +minuti» mentre il cron gira ogni ora. Non era cosmetico: `r0823_sl_anchor.py` misura che sulla +riga 4h BTC RADDOPPIA gli scatti del disaster-SL rotolante (2 contro 1; a 24h ETH arriva a 5). +Chi avesse "corretto" il cron verso il docstring avrebbe spostato il libro sulla riga peggiore. + +P1: il bersaglio si DERIVA da cio' che gira. Qui le fonti sono tre — il docstring (cio' che un +umano legge), `cron_book.sh` (cio' che dichiara di essere installato) e la crontab (cio' che +gira davvero) — e il test le confronta fra loro invece di ridichiarare «60 minuti». +""" +from __future__ import annotations + +import ast +import re +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +BOOK_EXECUTE = ROOT / "scripts" / "live" / "book_execute.py" +CRON_BOOK = ROOT / "scripts" / "cron_book.sh" +GRIGLIA_SKH_MIN = 230 # data1 di SKH01 (src/strategies/skyhook.py): la riga da NON usare +RIGA_PEGGIORE_MIN = 240 # 4h: dove BTC raddoppia gli scatti (r0823_sl_anchor.py) + + +def cadenza_minuti(expr: str) -> int: + """Minuti fra due esecuzioni di un'espressione crontab a 5 campi, per i soli casi regolari.""" + campi = expr.split() + assert len(campi) == 5, expr + m, h, dom, mon, dow = campi + assert dom == mon == dow == "*", f"cadenza non regolare: {expr}" + if h == "*" and m.isdigit(): + return 60 + if h == "*" and m.startswith("*/"): + return int(m[2:]) + if h.startswith("*/") and m.isdigit(): + return 60 * int(h[2:]) + raise ValueError(f"espressione non riconosciuta: {expr}") + + +def _docstring() -> str: + return ast.get_docstring(ast.parse(BOOK_EXECUTE.read_text())) or "" + + +def _cron_dichiarato() -> str: + """L'espressione crontab che `cron_book.sh` dichiara nel suo intestazione, fra backtick.""" + m = re.search(r"`(\d+ \* \* \* \*)`", CRON_BOOK.read_text()) + assert m, "cron_book.sh non dichiara piu' la sua riga di crontab" + return m.group(1) + + +def _cron_installato() -> str | None: + if not shutil.which("crontab"): + return None + try: + out = subprocess.run(["crontab", "-l"], capture_output=True, text=True, timeout=10) + except Exception: + return None + if out.returncode != 0: + return None + for riga in out.stdout.splitlines(): + if "cron_book.sh" in riga and not riga.lstrip().startswith("#"): + return " ".join(riga.split()[:5]) + return None + + +# ---------------------------------------------------------------- regressione sul docstring + +def test_il_docstring_non_prescrive_piu_i_230_minuti(): + d = _docstring() + assert "ogni ~230" not in d and "ogni 230" not in d + assert re.search(r"CADENZA:\s*ORARIA", d), "il docstring deve dichiarare la cadenza ORARIA" + + +def test_il_docstring_dichiara_la_stessa_riga_del_cron(): + # la riga di crontab citata nel docstring e' quella di cron_book.sh, non un'altra + d = _docstring() + assert _cron_dichiarato() in d + + +# ---------------------------------------------------------------- cron_book.sh + +def test_cron_book_e_orario_e_sta_lontano_dalla_riga_peggiore(): + expr = _cron_dichiarato() + c = cadenza_minuti(expr) + assert c == 60 + assert c < GRIGLIA_SKH_MIN < RIGA_PEGGIORE_MIN + assert "ORARIA" in CRON_BOOK.read_text() + + +def test_cron_book_evita_il_minuto_tondo(): + # vincolo 1 del commento di cron_book.sh: fuori dai ~26s del :00 (rate-limit per-IP) + minuto = int(_cron_dichiarato().split()[0]) + assert minuto != 0 + + +@pytest.mark.parametrize("expr,atteso", [("47 * * * *", 60), ("*/30 * * * *", 30), + ("7 */4 * * *", 240), ("0 */24 * * *", 1440)]) +def test_il_parser_della_cadenza(expr, atteso): + assert cadenza_minuti(expr) == atteso + + +def test_il_parser_rifiuta_cio_che_non_sa_leggere(): + with pytest.raises((ValueError, AssertionError)): + cadenza_minuti("47 9 * * 2") + + +# ---------------------------------------------------------------- la crontab che GIRA + +def test_la_crontab_installata_e_quella_dichiarata(): + """P16: il cron gira dalla macchina, non dal repo. Dove la crontab e' leggibile (la VPS), + deve coincidere con cio' che cron_book.sh dichiara; altrove il test si dichiara saltato, + non verde (P5: 'non vedo' non e' 'va tutto bene').""" + vivo = _cron_installato() + if vivo is None: + pytest.skip("crontab non leggibile o senza cron_book.sh: non verificabile qui") + assert vivo == _cron_dichiarato() + assert cadenza_minuti(vivo) == 60