1279ea605a
Il feed 1h certificato si ferma alle 00:00: per le letture successive `asof` dava la stessa barra a t0 e t1, mercato "fermo" = 0, e qualunque calo >=10% del giorno sarebbe stato un "movimento" scorporato come prelievo (giornale dal 25/08, report da oggi). Con feed assente tutto era "ambiguo" e il report stampava e1/e0-1 sotto l'etichetta TWR. - journal.movimenti_capitale: stato "mercato non misurabile" (feed fermo/assente/barra mancante) => ambiguo con la ragione; leva_tetto = min(frac x n x scala, LEVA_LORDA_MAX) (P1). - journal.rendimento_twr: salto non classificabile => twr E trading None con motivo. - journal.pnl_giorno CHIAMA rendimento_twr (prima rifaceva cum - certi: "entrambi la chiamano" era falso); la pagina stampa il TWR; analista qualifica il cumulato "di cui versati". - trades_db --report: la classe di ogni salto con la sua misura (mercato max a leva piena). - book_execute docstring: banda dello stop -33,5%/-26,5% (era invertita), "non costa ordini" -> micro-ordini di ri-taglia (22/48, C2), latenza <=1h solo con feed fresca, conteggi di r0823 al posto di "raddoppia" (2 contro 1 e' rotolante vs pavimento sulla riga 4h; la peggiore misurata e' 24h). - test_book_cadenza: LTF_MIN importato da skyhook; tutte le righe attive della crontab (una sola); tre stati dello skip (col progetto ma senza cron_book => ROSSO); slot di release testato con venue_probe (il :07 come controllo positivo); parser regolare, passo > 0. - r0823_sl_anchor: guardia sul :47 (era sul vecchio :07), prosa al passato; cron_chain.sh idem. - CLAUDE.md: §5.7 frase invertita corretta, §5.14 limite vero (non l'artefatto della fixture), §2 ora della LETTURA (13:47Z), nuovo debito §5.15 (USDE fuori dal bound di mercato). - diari: tempi "scritto" corretti coi commit; sezione "Seconda tornata". Test +11 (858 verdi). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XDJsH3iDSaBns3ccpPBwiu
208 lines
9.8 KiB
Python
208 lines
9.8 KiB
Python
"""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 gli
|
|
scatti del disaster-SL rotolante dipendono dalla cadenza — 1h: BTC 0 / ETH 1 in 7-8 anni; 4h:
|
|
BTC 2 (contro 1 del pavimento); 24h: ETH 5 con 2 in 30 giorni. Chi avesse "corretto" il CRON
|
|
verso il docstring avrebbe spostato il libro dalla riga 1h a quella 4h.
|
|
|
|
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
|
|
import sys
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from src.live import venue_probe as VP # noqa: E402 lo slot di release, dichiarato li'
|
|
from src.strategies.skyhook import LTF_MIN # noqa: E402 la griglia di SKH01, NON ridichiarata
|
|
|
|
BOOK_EXECUTE = ROOT / "scripts" / "live" / "book_execute.py"
|
|
CRON_BOOK = ROOT / "scripts" / "cron_book.sh"
|
|
RIGA_4H_MIN = 240 # la riga in cui il rotolante di BTC passa da 0 a 2 scatti (r0823_sl_anchor.py);
|
|
# la PEGGIORE misurata e' 24h (ETH 5, 2 in 30 giorni), non questa
|
|
|
|
_RX_CRON5 = re.compile(r"`([\d*/,-]+(?: [\d*/,-]+){4})`")
|
|
|
|
|
|
def cadenza_minuti(expr: str) -> int:
|
|
"""Minuti fra due esecuzioni di un'espressione crontab a 5 campi, SOLO per cadenze regolari
|
|
(intervalli tutti uguali): `47 * * * *` -> 60, `*/30 * * * *` -> 30, `7 */4 * * *` -> 240.
|
|
`*/45` (intervalli 45/15), `0 */5` (ultimo intervallo 4h) e `*/0` vengono RIFIUTATI."""
|
|
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("*/"):
|
|
passo = int(m[2:])
|
|
if passo <= 0 or 60 % passo:
|
|
raise ValueError(f"passo di minuti non regolare: {expr}")
|
|
return passo
|
|
if h.startswith("*/") and m.isdigit():
|
|
passo = int(h[2:])
|
|
if passo <= 0 or 24 % passo:
|
|
raise ValueError(f"passo di ore non regolare: {expr}")
|
|
return 60 * passo
|
|
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:
|
|
"""La prima espressione crontab a 5 campi che `cron_book.sh` dichiara fra backtick — di
|
|
qualunque forma: e' `cadenza_minuti` a giudicarla, non questa regex (P4: un'espressione
|
|
presente ma non oraria deve fallire come 'non oraria', non come 'assente')."""
|
|
m = _RX_CRON5.search(CRON_BOOK.read_text())
|
|
assert m, "cron_book.sh non dichiara piu' la sua riga di crontab fra backtick"
|
|
return m.group(1)
|
|
|
|
|
|
def _crontab() -> str | None:
|
|
"""Testo della crontab dell'utente, None se non leggibile (P5: distinto da 'vuota')."""
|
|
if not shutil.which("crontab"):
|
|
return None
|
|
try:
|
|
out = subprocess.run(["crontab", "-l"], capture_output=True, text=True, timeout=10)
|
|
except Exception:
|
|
return None
|
|
return out.stdout if out.returncode == 0 else None
|
|
|
|
|
|
def _righe_attive(testo: str, marcatore: str) -> list[str]:
|
|
"""Le espressioni (5 campi) di TUTTE le righe attive che citano `marcatore`: due righe sono
|
|
due esecuzioni, e la seconda non deve nascondersi dietro la prima."""
|
|
return [" ".join(r.split()[:5]) for r in testo.splitlines()
|
|
if marcatore in r and not r.lstrip().startswith("#")]
|
|
|
|
|
|
# ---------------------------------------------------------------- regressione sul docstring
|
|
# La guardia non cerca due letterali («ogni ~230»): estrae OGNI prescrizione di cadenza scritta
|
|
# come «ogni/every N unita'» e pretende che valga 60 minuti. ⚠️ Limite dichiarato (P13): una
|
|
# guardia sui numeri non copre il ragionamento — una prosa che prescrive una cadenza senza la
|
|
# parola «ogni» passa. Il controllo positivo qui sotto dice cosa la guardia VEDE.
|
|
|
|
_NUMERI = {"un": 1, "una": 1, "due": 2, "tre": 3, "quattro": 4, "sei": 6, "otto": 8, "dodici": 12}
|
|
_RX_CADENZA = re.compile(
|
|
r"\b(?:ogni|every)\s*~?\s*(\d+|un'?|una|due|tre|quattro|sei|otto|dodici)?\s*'?\s*"
|
|
r"(minuti|minuto|minutes?|min|m|ore|ora|hours?|h)\b", re.I)
|
|
|
|
|
|
def prescrizioni_minuti(testo: str) -> list[int]:
|
|
"""Ogni «ogni N unita'» del testo, in minuti. «ogni ora» -> 60, «ogni ~230 minuti» -> 230."""
|
|
out = []
|
|
for m in _RX_CADENZA.finditer(testo):
|
|
n_raw, unita = m.group(1), m.group(2).lower()
|
|
n = 1 if n_raw is None else (int(n_raw) if n_raw.isdigit() else _NUMERI[n_raw.rstrip("'")])
|
|
out.append(n * (60 if unita.startswith(("or", "h")) else 1))
|
|
return out
|
|
|
|
|
|
def test_la_guardia_vede_le_prescrizioni_che_deve_vedere():
|
|
# controllo positivo (M15): i modi in cui il docstring potrebbe ri-prescrivere la riga 4h
|
|
assert prescrizioni_minuti("va lanciato ogni ~230 minuti con la feed") == [230]
|
|
assert prescrizioni_minuti("il giro va lanciato ogni quattro ore") == [240]
|
|
assert prescrizioni_minuti("every 4 hours, then every 230 min") == [240, 230]
|
|
assert prescrizioni_minuti("controllato ogni ora; ogni 60 minuti") == [60, 60]
|
|
assert prescrizioni_minuti("fino a 230m di latenza") == [] # non e' una prescrizione
|
|
|
|
|
|
def test_il_docstring_prescrive_solo_la_cadenza_oraria():
|
|
d = _docstring()
|
|
p = prescrizioni_minuti(d)
|
|
assert p, "il docstring deve contenere almeno una prescrizione di cadenza"
|
|
assert all(x == 60 for x in p), f"prescrizioni non orarie nel docstring: {p}"
|
|
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_sotto_la_griglia_skh_e_sotto_la_riga_4h():
|
|
c = cadenza_minuti(_cron_dichiarato())
|
|
assert c == 60
|
|
assert c < LTF_MIN, "la cadenza deve stare SOTTO la griglia di SKH01 (latenza <= 1 barra)"
|
|
assert c < RIGA_4H_MIN, "la riga 4h e' quella in cui il rotolante di BTC fa 2 scatti"
|
|
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
|
|
|
|
|
|
def test_cron_book_evita_lo_slot_di_release_deribit():
|
|
# vincolo 2 del commento di cron_book.sh — il motivo vero del :07 -> :47 (4 giri morti in
|
|
# 63 giorni): il giro del martedi' alle 09:MM deve cadere FUORI dallo slot dichiarato in
|
|
# venue_probe (derivato, non riscritto). Il :07 lo violava; se qualcuno tornasse a :15
|
|
# con docstring e crontab coerenti, gli altri test passerebbero e questo no.
|
|
minuto = int(_cron_dichiarato().split()[0])
|
|
base = datetime(2026, 9, 1, tzinfo=timezone.utc) # un lunedi'
|
|
martedi = base + timedelta(days=(VP.RELEASE_WEEKDAY - base.weekday()) % 7)
|
|
giro = martedi.replace(hour=VP.RELEASE_START_MIN // 60, minute=minuto)
|
|
assert not VP.in_release_window(giro), f"il giro :{minuto:02d} cade nello slot di release"
|
|
assert VP.in_release_window(giro.replace(minute=7)), "controllo positivo: il :07 ci cadeva"
|
|
|
|
|
|
@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
|
|
|
|
|
|
@pytest.mark.parametrize("expr", ["47 9 * * 2", "*/45 * * * *", "0 */5 * * *", "*/0 * * * *",
|
|
"47 * 1 * *"])
|
|
def test_il_parser_rifiuta_cio_che_non_e_regolare(expr):
|
|
with pytest.raises((ValueError, AssertionError)):
|
|
cadenza_minuti(expr)
|
|
|
|
|
|
# ---------------------------------------------------------------- la crontab che GIRA
|
|
|
|
def test_la_crontab_installata_e_quella_dichiarata_e_una_sola():
|
|
"""P16: il cron gira dalla macchina, non dal repo. TRE stati, non due (P5):
|
|
- crontab NON leggibile -> SALTATO (non verificabile qui);
|
|
- leggibile, e questo progetto non ci compare affatto -> SALTATO (macchina di sviluppo);
|
|
- leggibile con altre righe del progetto ma SENZA cron_book.sh attivo -> ROSSO: il libro
|
|
non e' schedulato dove il progetto e' installato (una riga commentata per manutenzione e
|
|
dimenticata e' esattamente questo caso).
|
|
E la riga deve essere UNA: due righe attive sono due esecuzioni l'ora."""
|
|
testo = _crontab()
|
|
if testo is None:
|
|
pytest.skip("crontab non leggibile: non verificabile qui")
|
|
if str(ROOT) not in testo and ROOT.name not in testo:
|
|
pytest.skip("la crontab non cita questo progetto: non e' la macchina che lo esegue")
|
|
righe = _righe_attive(testo, "cron_book.sh")
|
|
assert righe, "questa macchina esegue il progetto ma cron_book.sh NON e' schedulato"
|
|
assert len(righe) == 1, f"cron_book.sh schedulato {len(righe)} volte: {righe}"
|
|
assert righe[0] == _cron_dichiarato()
|
|
assert cadenza_minuti(righe[0]) == 60
|
|
|
|
|
|
def test_due_righe_attive_non_si_nascondono_dietro_la_prima():
|
|
finta = ("47 * * * * /x/PythagorasGoal/scripts/cron_book.sh\n"
|
|
"# 7 * * * * /x/PythagorasGoal/scripts/cron_book.sh\n"
|
|
"17 * * * * /x/PythagorasGoal/scripts/cron_book.sh\n")
|
|
assert _righe_attive(finta, "cron_book.sh") == ["47 * * * *", "17 * * * *"]
|
|
assert _righe_attive("# 47 * * * * /x/cron_book.sh\n", "cron_book.sh") == []
|