debito 7: la cadenza del libro e' ORARIA — docstring riscritto, tre fonti tenute d'accordo da un test

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RziUCB336YPUUyDJ29x4Ke
This commit is contained in:
Adriano Dal Pastro
2026-09-02 14:38:53 +00:00
parent 2f701b8469
commit 861cc7fc27
5 changed files with 204 additions and 7 deletions
+11 -4
View File
@@ -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 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 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**. 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.** 7. **RIPARATO (2026-09-02).** Il docstring di `book_execute.py` prescriveva "ogni ~230 minuti"
**Il docstring e' sbagliato e la configurazione che gira e' quella giusta** — chi lo "correggesse" mentre il cron gira **ogni ora** (`47 * * * *`): era il docstring a essere sbagliato, e chi lo
sposterebbe il libro sulla riga 4h, dove BTC **raddoppia** gli scatti del disaster-SL. 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 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. 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** *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/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/journal.py # voce del giorno (numeri + lettura)
uv run python scripts/live/analista.py --secco # analisi del giorno, senza salvare 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 ```python
@@ -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.
+17
View File
@@ -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 **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. 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 ### Cosa resta scoperto
1. **La sonda dice di chi è il guasto, non lo aggira.** Col gateway giù il libro continua ad 1. **La sonda dice di chi è il guasto, non lo aggira.** Col gateway giù il libro continua ad
+10 -3
View File
@@ -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 Senza entrambi e' un DRY-RUN (stampa il piano, NON invia). Reconciliation dopo ogni ordine; log in
data/live/book_executions.jsonl. data/live/book_executions.jsonl.
⚠️ CADENZA: SKH01 decide su griglia 230m -> questo script va lanciato ogni ~230 minuti con la feed ⚠️ CADENZA: ORARIA — `47 * * * *` in `scripts/cron_book.sh` (il minuto e' spiegato li'). SKH01
fresca all'ultima barra chiusa (NON il cron giornaliero, che mancherebbe gli ingressi). Gli exit di decide su griglia 230m, ma il giro e' IDEMPOTENTE (riconcilia al target netto corrente: se non
SKH sono SOFTWARE (latenza fino a fine barra 230m); solo il disaster-SL (-30%) e' on-book. 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 # DRY-RUN (piano, nessun ordine)
uv run python scripts/live/book_execute.py --execute # esegue SOLO se execution_enabled=true uv run python scripts/live/book_execute.py --execute # esegue SOLO se execution_enabled=true
+121
View File
@@ -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