un flag sconosciuto non e' l'azione di default: guardia su 18 script, indice USDE orario, pulizia

MISURATO oggi durante la revisione: `trades_db.py --help` non stampava l'uso, cadeva in sync()
e riscriveva meta.ultimo_sync. Nessuno dei 18 script di scripts/live/ usava argparse: un flag
sbagliato era il ramo else. Su journal.py avrebbe scritto pagina e riga di DB, su analista.py
avrebbe speso una chiamata al modello e mandato un Telegram.

- src/live/cli.valida: prima istruzione di ogni __main__, prima di connect()/sync/rete.
  --help -> 0 con l'uso; flag ignoto, valore mancante o posizionale -> 2 con l'elenco dei
  previsti (P4). NIENTE argparse: cambierebbe messaggi, codici d'uscita e --help di script
  che il cron gia' chiama.
- 18 script cablati (i 3 che scrivono + 14 + cc01), flag invariati.
- tests/test_cli_flag.py (30): elenco DERIVATO dalla cartella (P1), valida come prima
  istruzione, uso che documenta i flag, e i flag che il CRON usa davvero restano accettati
  (P15/P16); end-to-end su --help e flag ignoto con trades.db non toccato (M15).
  Verificato a mano: monitor_health --quiet, trades_db --sync --quiet, book_execute dry-run.

Debito §5.15, primo passo: balance_watch (orario) registra `usde_usdc` a ogni campione — None
con la ragione se illeggibile, mai 1,0. Il cablaggio nel bound quando la serie ha storia.

Pulizia dalla revisione: tests/helpers.carica_script al posto della 15a copia del loader
importlib (5 file del libro live); il fill di prova via upsert_fills invece di un INSERT che
lasciava verified NULL; asserzioni non ancorate al padding; movimenti_capitale accetta le
righe gia' lette (una SELECT invece di due ai due lati di una scrittura del cron).

Test 910 verdi (+52). Diario 2026-09-02c.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDJsH3iDSaBns3ccpPBwiu
This commit is contained in:
Adriano Dal Pastro
2026-09-02 15:49:40 +00:00
parent 1279ea605a
commit de83909db9
31 changed files with 627 additions and 59 deletions
+20 -4
View File
@@ -372,9 +372,24 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per
1,5% (indice 0,95) resta sotto soglia e nel P&L; una rottura piena (31%) supera 2× un mercato 1,5% (indice 0,95) resta sotto soglia e nel P&L; una rottura piena (31%) supera 2× un mercato
"fermo" e diventerebbe un **«movimento» scorporato come prelievo**, nel giornale e nel report — "fermo" e diventerebbe un **«movimento» scorporato come prelievo**, nel giornale e nel report —
l'invenzione silenziosa che P12 vieta. Il bound corretto somma `quota_usde × |Δ indice usde_usdc|`, l'invenzione silenziosa che P12 vieta. Il bound corretto somma `quota_usde × |Δ indice usde_usdc|`,
ma l'indice e' registrato **una volta al giorno** (`usde_watch`, 12:35Z): serve una serie oraria ma l'indice era registrato **una volta al giorno** (`usde_watch`, 12:35Z). ◐ **Primo passo fatto
(`balance_watch` la potrebbe portare) prima di chiuderlo. Intanto: chi legge un «movimento» il 02/09:** `balance_watch` (orario, sola lettura) registra ora `usde_usdc` a ogni campione —
negativo senza bonifico in uscita **lo verifichi contro `usde_watch.jsonl`** prima di crederci. None con la ragione se l'indice non e' leggibile, mai 1,0. **Manca il cablaggio nel bound**, che
si fa quando la serie ha storia. Intanto: chi legge un «movimento» negativo senza bonifico in
uscita **lo verifichi contro `balance_watch.jsonl`** prima di crederci.
16.**RIPARATO (2026-09-02).** Nessuno dei **18** script di `scripts/live/` usava argparse: un
flag sconosciuto non era un errore, era **il ramo `else`**. Costo misurato lo stesso giorno
durante una revisione: `trades_db.py --help` non stampava l'uso, cadeva in `sync()` e riscriveva
`meta.ultimo_sync`; sugli altri due sarebbe stato peggio — `journal.py --help` avrebbe scritto
pagina e riga di DB, `analista.py --help` avrebbe speso una chiamata al modello e mandato un
Telegram. Ora `src/live/cli.valida` e' la **prima istruzione** di ogni `__main__`: `--help`
stampa l'uso ed esce **0**, un flag ignoto esce **2** con l'elenco di quelli previsti (P4).
Niente argparse: cambierebbe messaggi, codici d'uscita e `--help` di script che il cron gia'
chiama — qui si lascia intatto cio' che gira. **`tests/test_cli_flag.py` (30)**: l'elenco degli
script si **deriva dalla cartella** (uno nuovo senza guardia fa fallire il test), verifica che
`valida` sia la prima istruzione, che l'uso documenti i flag accettati, e che **i flag che il
cron usa davvero restino accettati** (P15/P16). Diario `2026-09-02c-flag-e-pulizia.md`.
--- ---
@@ -589,10 +604,11 @@ uv run python scripts/analysis/certify_feed.py [--local] # certifica i f
uv run python scripts/portfolio/run_portfolio.py # report del portafoglio (ricerca) uv run python scripts/portfolio/run_portfolio.py # report del portafoglio (ricerca)
uv run python scripts/live/paper_portfolio.py # avanza il paper (forward-only) uv run python scripts/live/paper_portfolio.py # avanza il paper (forward-only)
uv run python scripts/live/trades_db.py --report # stato libro di bordo + P&L (TWR, non e1/e0) uv run python scripts/live/trades_db.py --report # stato libro di bordo + P&L (TWR, non e1/e0)
uv run python scripts/live/<script>.py --help # uso e flag (dal 02/09: un flag ignoto esce 2)
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 (858, tutti verdi al 02/09) uv run pytest # test (910, tutti verdi al 02/09)
``` ```
```python ```python
+77
View File
@@ -0,0 +1,77 @@
# 2026-09-02 — Un flag sconosciuto non è l'azione di default
*Scritto fra le 15:45Z e le 16:05Z. Il difetto è stato misurato, non ipotizzato: la revisione del
codice di questa sessione ha eseguito `trades_db.py --help` come sonda di tempi e lo ha visto
sincronizzare il DB.*
## 1. Il difetto
Nessuno dei 18 script di `scripts/live/` usava argparse. Leggevano gli argomenti così:
```python
if "--reconcile" in args: ...
elif "--report" in args: ...
else: sync() # <- qui finiva QUALUNQUE cosa non riconosciuta
```
Un flag sbagliato non era un errore: era il ramo finale. Costo osservato e costi possibili:
| script | `--help` faceva | gravità |
|---|---|---|
| `trades_db.py` | `sync()`: riscriveva `meta.ultimo_sync` | osservato oggi, danno nullo |
| `journal.py` | scriveva la pagina di oggi e una riga nel DB | non osservato, ma scrive |
| `analista.py` | spendeva una chiamata al modello e mandava un Telegram | non osservato, ma spende |
## 2. La riparazione, e perché non è argparse
`src/live/cli.valida(nome, uso, flag=..., con_valore=...)`, chiamata come **prima istruzione** di
ogni blocco `__main__` — prima di `connect()`, prima del sync, prima della rete.
- `--help` / `-h` → stampa l'uso, esce **0**;
- flag non dichiarato, o valore mancante, o posizionale → stderr con **cosa** e **cosa si poteva**
(P4), esce **2** (errore d'uso, distinto dall'1 con cui gli script segnalano un esito negativo);
- tutto il resto invariato.
**Argparse no.** Cambierebbe messaggi d'errore, codici d'uscita e il comportamento di `--help` su
18 script che il cron già invoca: romperebbe ciò che gira per riparare ciò che non gira mai. Qui
serviva l'opposto — lasciare intatti i flag esistenti e rifiutare solo l'ignoto.
## 3. Il test guarda entrambe le metà del contratto
`tests/test_cli_flag.py`, 30 test:
- **l'elenco degli script si deriva dalla cartella** (P1): uno script nuovo che legge `sys.argv` a
mano e non chiama `valida` fa fallire il test, non passa inosservato perché nessuno ha aggiornato
una lista;
- `valida` dev'essere la **prima istruzione** di `__main__` (uscire dopo l'effetto non è uscire);
- l'uso deve **nominare** i flag che lo script accetta;
- **i flag che il cron usa davvero devono restare accettati** (P15/P16, letti dai `cron_*.sh`): una
guardia che ferma il libro di bordo la notte stessa sarebbe peggio del difetto;
- end-to-end sui tre script che scrivono: `--help` esce 0 e **non tocca `trades.db`**; un flag
ignoto esce 2 e non lo tocca (M15: il caso che ha causato il difetto, non un caso vicino).
Verificato a mano anche il contrario: `monitor_health.py --quiet`, `trades_db.py --sync --quiet`
e `book_execute.py` in dry-run escono 0 come prima.
## 4. Il resto della sessione
**Indice USDE orario** (debito §5.15, primo passo). Il classificatore dei movimenti di capitale
confronta due letture di equity a un'ora di distanza e il suo bound guarda solo BTC/ETH, mentre il
31% dell'equity è USDE: un depeg sarebbe scorporato come un prelievo. `usde_watch` registra
l'indice una volta al giorno — troppo rado. Ora `balance_watch` (orario, sola lettura) lo registra
a ogni campione, `None` con la ragione se non leggibile, mai 1,0. Il cablaggio nel bound viene
quando la serie ha storia: prima serve il dato, poi la regola.
**Pulizia lasciata dalla revisione.** `tests/helpers.carica_script` sostituisce la dodicesima copia
del caricatore `importlib` (15 in giro, con nomi di modulo diversi per lo stesso file e differenze
silenziose su `sys.modules`); il fill di prova si scrive con `upsert_fills` invece che con un
`INSERT` a mano che lasciava `verified` NULL, una forma che la produzione non produce; le
asserzioni sul testo del report non sono più ancorate al padding; `movimenti_capitale` accetta le
righe già lette, così il report non fa due SELECT sulla stessa tabella a un'ora del cron — due
letture ai due lati di una scrittura descriverebbero due istanti diversi.
## 5. Cosa NON ho fatto
- I loader `importlib` dei test sulla **ricerca** (moduli in `scripts/research/`) restano come
sono: sono a livello di modulo, con radici diverse, e toccarli non ripara niente.
- Il bound USDE nel classificatore: manca la storia, non il codice.
+16
View File
@@ -939,6 +939,22 @@ parser accetta solo espressioni regolari e rifiuta il resto. Vincolo del minuto
un commento che dichiara una cadenza diversa da quella che gira si paga quando qualcuno lo "corregge" un commento che dichiara una cadenza diversa da quella che gira si paga quando qualcuno lo "corregge"
nel verso sbagliato.** nel verso sbagliato.**
### Un flag sconosciuto non è l'azione di default (2026-09-02)
Nessuno dei 18 script di `scripts/live/` usava argparse: leggevano `sys.argv` con `in` e `index`,
quindi un flag sbagliato non era un errore — era il ramo `else`. Il costo si è misurato lo stesso
giorno, dentro una revisione: `trades_db.py --help` è caduto in `sync()` e ha riscritto
`meta.ultimo_sync`. Sui due script che scrivono davvero sarebbe stato peggio: la pagina di giornale
e una riga di DB, oppure una chiamata al modello più un Telegram. **La riparazione non è argparse**
cambierebbe messaggi d'errore, codici d'uscita e `--help` di script che il cron già chiama, cioè
romperebbe ciò che gira per riparare ciò che non gira mai. È una funzione di venti righe
(`src/live/cli.valida`) che rifiuta ciò che non è dichiarato ed esce **prima di qualunque effetto**:
`--help` → 0, flag ignoto → 2 con l'elenco dei previsti (P4). Il test deriva l'elenco degli script
dalla cartella (P1: un file nuovo senza guardia fallisce, non passa inosservato), controlla che
`valida` sia la prima istruzione di `__main__` (uscire dopo `connect()` sarebbe uscire dopo
l'effetto) e — l'altra metà del contratto, P15/P16 — che **i flag che il cron usa davvero restino
accettati**: una guardia che ferma il libro di bordo la notte stessa sarebbe peggio del difetto.
### 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
+14
View File
@@ -115,7 +115,21 @@ def scrivi(con, g: date, modello: str, secco: bool = False, verbose: bool = True
return stato return stato
USO = """uso: analista.py [--giorno AAAA-MM-GG] [--modello NOME] [--secco] [--no-telegram] [--quiet]
(nessun flag) scrive l'analisi di OGGI nel campo `analisi` e manda la notifica
--giorno G analizza il giorno G
--modello M modello da usare (default: src.live.analista.MODELLO_DEFAULT)
--secco stampa e NON salva, niente Telegram
--no-telegram salva ma non manda la notifica
--quiet non stampa
SPENDE una chiamata al modello e MANDA un messaggio Telegram, salvo --secco/--no-telegram."""
if __name__ == "__main__": if __name__ == "__main__":
from src.live.cli import valida
valida("analista.py", USO, flag=("--secco", "--no-telegram", "--quiet"),
con_valore=("--giorno", "--modello"))
con = T.connect() con = T.connect()
g = date.fromisoformat(_arg("--giorno", datetime.now(timezone.utc).date().isoformat())) g = date.fromisoformat(_arg("--giorno", datetime.now(timezone.utc).date().isoformat()))
st = scrivi(con, g, modello=_arg("--modello", A.MODELLO_DEFAULT), st = scrivi(con, g, modello=_arg("--modello", A.MODELLO_DEFAULT),
+11
View File
@@ -85,6 +85,17 @@ def campiona(c: DeribitRead) -> dict:
rec["n_posizioni"] = sum(1 for p in pos if abs(float(p.get("size") or 0)) > 1) rec["n_posizioni"] = sum(1 for p in pos if abs(float(p.get("size") or 0)) > 1)
except Exception: except Exception:
rec["nozionale_lordo"], rec["n_posizioni"] = None, None rec["nozionale_lordo"], rec["n_posizioni"] = None, None
# INDICE usde_usdc, ORARIO (aggiunto 2026-09-02, debito §5.15). `usde_watch` lo registra una
# volta al giorno (12:35Z): troppo rado per il classificatore dei movimenti di capitale, che
# confronta due letture di equity a un'ora di distanza e oggi guarda solo BTC/ETH — con il
# 31% dell'equity in USDE, un depeg sarebbe scorporato come un PRELIEVO. Questa e' la serie
# che manca; il cablaggio nel bound viene dopo, quando ci sara' storia. None se non leggibile
# (P5: "non vedo" non e' "il peg tiene").
try:
from src.live.usde import prezzo_indice
rec["usde_usdc"] = prezzo_indice()
except Exception as e:
rec["usde_usdc"], rec["usde_usdc_errore"] = None, f"{type(e).__name__}"
return rec return rec
+9
View File
@@ -355,5 +355,14 @@ def main():
raise raise
USO = """uso: book_execute.py [--execute]
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
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__": if __name__ == "__main__":
from src.live.cli import valida
valida("book_execute.py", USO, flag=("--execute",), con_valore=())
main() main()
+8
View File
@@ -93,5 +93,13 @@ def main() -> None:
print(" regime QUIET: carry non raccoglibile (coerente con luglio 2026).") print(" regime QUIET: carry non raccoglibile (coerente con luglio 2026).")
USO = """uso: cc01_regime_watch.py
(nessun flag) sorveglia il regime di CC01 e stampa lo stato.
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__": if __name__ == "__main__":
from src.live.cli import valida
valida("cc01_regime_watch.py", USO)
main() main()
+10
View File
@@ -144,5 +144,15 @@ def main() -> int:
return 0 return 0
USO = """uso: edge_watch.py [--quiet]
edge_watch.py sorveglianza dell'EDGE del book live. Riporta e allerta; non tocca nulla.
uv run python scripts/live/edge_watch.py # report
uv run python scripts/live/edge_watch.py --quiet # solo se qualcosa scatta (cron)
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__": if __name__ == "__main__":
from src.live.cli import valida
valida("edge_watch.py", USO, flag=("--quiet",), con_valore=())
raise SystemExit(main()) raise SystemExit(main())
+10
View File
@@ -252,5 +252,15 @@ def main() -> int:
return 2 if r["level"] == "AZIONE" else 0 return 2 if r["level"] == "AZIONE" else 0
USO = """uso: fee_watch.py [--quiet]
fee_watch.py sorveglia lo schema fee di Deribit e applica la regola DECISA IN ANTICIPO.
uv run python scripts/live/fee_watch.py # report
uv run python scripts/live/fee_watch.py --quiet # stampa/allerta solo se qualcosa cambia
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__": if __name__ == "__main__":
from src.live.cli import valida
valida("fee_watch.py", USO, flag=("--quiet",), con_valore=())
raise SystemExit(main()) raise SystemExit(main())
+12
View File
@@ -41,7 +41,19 @@ def una(con, g: date, nota: str | None = None, verbose: bool = True) -> Path:
return f return f
USO = """uso: journal.py [--giorno AAAA-MM-GG] [--backfill N] [--nota "testo"] [--quiet]
(nessun flag) scrive la voce di OGGI (pagina marcata PARZIALE: il giorno non e' chiuso)
--giorno G scrive la voce del giorno G
--backfill N ricostruisce gli ultimi N giorni, dal piu' vecchio
--nota "..." annota il giorno (campo dell'operatore, mai riscritto da un ricalcolo)
--quiet non stampa la riga di riepilogo
SCRIVE: una riga nel DB e una pagina in docs/journal/. Sola lettura su feed, log e venue."""
if __name__ == "__main__": if __name__ == "__main__":
from src.live.cli import valida
valida("journal.py", USO, flag=("--quiet",), con_valore=("--giorno", "--backfill", "--nota"))
# sincronizzare PRIMA: altrimenti la sezione 'Libro' (letta dal log) e la sezione 'P&L' # sincronizzare PRIMA: altrimenti la sezione 'Libro' (letta dal log) e la sezione 'P&L'
# (letta dal DB) descrivono due istanti diversi e la pagina si contraddice da sola. # (letta dal DB) descrivono due istanti diversi e la pagina si contraddice da sola.
sys.path.insert(0, str(ROOT / "scripts" / "live")) sys.path.insert(0, str(ROOT / "scripts" / "live"))
+9
View File
@@ -137,5 +137,14 @@ def main():
raise raise
USO = """uso: live_execute.py [--execute]
uv run python scripts/live/live_execute.py # DRY-RUN (piano, nessun ordine)
uv run python scripts/live/live_execute.py --execute # esegue SOLO se execution_enabled=true
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__": if __name__ == "__main__":
from src.live.cli import valida
valida("live_execute.py", USO, flag=("--execute",), con_valore=())
main() main()
+10
View File
@@ -74,5 +74,15 @@ def main():
(f"{len(r['orders'])} ordine/i costruito/i sopra." if r["orders"] else "Target flat: 0 ordini.")) (f"{len(r['orders'])} ordine/i costruito/i sopra." if r["orders"] else "Target flat: 0 ordini."))
USO = """uso: live_trend.py [--equity V] [--no-net]
uv run python scripts/live/live_trend.py # shadow su mainnet reale
uv run python scripts/live/live_trend.py --equity 2000 # forza la base di sizing
uv run python scripts/live/live_trend.py --no-net # offline: solo matematica + parita'
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__": if __name__ == "__main__":
from src.live.cli import valida
valida("live_trend.py", USO, flag=("--no-net",), con_valore=("--equity",))
main() main()
+9
View File
@@ -88,5 +88,14 @@ def main():
print(" Validato: invio ordine reale, fill, fee reali, reconciliation, ritorno a flat.") print(" Validato: invio ordine reale, fill, fee reali, reconciliation, ritorno a flat.")
USO = """uso: microtest.py [--live]
uv run python scripts/live/microtest.py # DRY-RUN: nessun ordine inviato
uv run python scripts/live/microtest.py --live # invia il round-trip REALE
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__": if __name__ == "__main__":
from src.live.cli import valida
valida("microtest.py", USO, flag=("--live",), con_valore=())
main() main()
+10
View File
@@ -55,5 +55,15 @@ def main() -> int:
return 0 return 0
USO = """uso: monitor_health.py [--quiet]
monitor_health.py i forward-monitor stanno registrando? Riporta e allerta, non tocca nulla.
uv run python scripts/live/monitor_health.py # report
uv run python scripts/live/monitor_health.py --quiet # stampa/allerta solo se scatta
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__": if __name__ == "__main__":
from src.live.cli import valida
valida("monitor_health.py", USO, flag=("--quiet",), con_valore=())
raise SystemExit(main()) raise SystemExit(main())
+8
View File
@@ -118,5 +118,13 @@ def main():
print(f" GTAA (IB, asof {asof}): " + ", ".join(f"{k} {v:.0%}" for k, v in gw.items() if v) + f" | cash {cash:.0%}") print(f" GTAA (IB, asof {asof}): " + ", ".join(f"{k} {v:.0%}" for k, v in gw.items() if v) + f" | cash {cash:.0%}")
USO = """uso: paper_combo.py [--reset] [--status]
uv run python scripts/live/paper_combo.py [--status|--reset]
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__": if __name__ == "__main__":
from src.live.cli import valida
valida("paper_combo.py", USO, flag=("--status", "--reset"), con_valore=())
main() main()
+10
View File
@@ -87,5 +87,15 @@ def main():
print(f" posizioni correnti: {pf.current_positions()}") print(f" posizioni correnti: {pf.current_positions()}")
USO = """uso: paper_portfolio.py [--reset] [--status]
uv run python scripts/live/paper_portfolio.py # avanza (init al 1o run)
uv run python scripts/live/paper_portfolio.py --status # solo stato
uv run python scripts/live/paper_portfolio.py --reset # azzera (riparte da ora)
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__": if __name__ == "__main__":
from src.live.cli import valida
valida("paper_portfolio.py", USO, flag=("--status", "--reset"), con_valore=())
main() main()
+10
View File
@@ -224,5 +224,15 @@ def main() -> None:
print("\n fatto. I file *.%s.* sono l'evidenza del difetto e restano nel backup." % SUFFISSO) print("\n fatto. I file *.%s.* sono l'evidenza del difetto e restano nel backup." % SUFFISSO)
USO = """uso: paper_regen.py [--solo V]
paper_regen.py RIGENERA le serie dei forward-monitor rotti da `advance()` (§5.1).
uv run python scripts/live/paper_regen.py # tutti e 4
uv run python scripts/live/paper_regen.py --solo paper_statarb
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__": if __name__ == "__main__":
from src.live.cli import valida
valida("paper_regen.py", USO, flag=(), con_valore=("--solo",))
main() main()
+10
View File
@@ -58,5 +58,15 @@ def main() -> None:
print(f"\n invio: {rep['invio']}") print(f"\n invio: {rep['invio']}")
USO = """uso: scale_watch.py [--quiet] [--secco]
uv run python scripts/live/scale_watch.py # osserva e stampa, invia se ALLARME
uv run python scripts/live/scale_watch.py --secco # osserva e stampa, NON invia nulla
uv run python scripts/live/scale_watch.py --quiet # una riga sola (per il cron)
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__": if __name__ == "__main__":
from src.live.cli import valida
valida("scale_watch.py", USO, flag=("--quiet", "--secco"), con_valore=())
main() main()
+9
View File
@@ -146,5 +146,14 @@ def main() -> None:
print("inviato" if send(txt) else "NON inviato (config Telegram assente o rete KO)") print("inviato" if send(txt) else "NON inviato (config Telegram assente o rete KO)")
USO = """uso: telegram_daily.py [--dry-run]
uv run python scripts/live/telegram_daily.py # calcola e invia
uv run python scripts/live/telegram_daily.py --dry-run # stampa e basta, non invia
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__": if __name__ == "__main__":
from src.live.cli import valida
valida("telegram_daily.py", USO, flag=("--dry-run",), con_valore=())
main() main()
+9 -1
View File
@@ -125,8 +125,16 @@ def report() -> None:
con.close() con.close()
USO = """uso: trades_db.py [--sync] [--report] [--reconcile] [--quiet]
--sync (default) legge log e jsonl e aggiorna data/live/trades.db idempotente
--report stato del libro di bordo + P&L (TWR, non e1/e0)
--reconcile incrocio delle tre fonti sui fill (log x jsonl x venue)
--quiet con --sync: non stampa il riepilogo"""
if __name__ == "__main__": if __name__ == "__main__":
args = sys.argv[1:] from src.live.cli import valida
args = valida("trades_db.py", USO, flag=("--sync", "--report", "--reconcile", "--quiet"))
if "--reconcile" in args: if "--reconcile" in args:
reconcile() reconcile()
elif "--report" in args: elif "--report" in args:
+10
View File
@@ -330,5 +330,15 @@ def main() -> int:
return 0 return 0
USO = """uso: usde_watch.py [--quiet]
usde_watch.py sorveglianza giornaliera del collaterale USDE: reward, depeg, quota. SOLA LETTURA.
uv run python scripts/live/usde_watch.py # report completo
uv run python scripts/live/usde_watch.py --quiet # stampa/allerta solo transizioni
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__": if __name__ == "__main__":
from src.live.cli import valida
valida("usde_watch.py", USO, flag=("--quiet",), con_valore=())
sys.exit(main()) sys.exit(main())
+10
View File
@@ -98,5 +98,15 @@ def main() -> int:
return 0 return 0
USO = """uso: venue_watch.py [--quiet]
venue_watch.py runner ORARIO del tripwire di venue. Allerta su Telegram, NON blocca nulla.
uv run python scripts/live/venue_watch.py # un giro, stampa il report
uv run python scripts/live/venue_watch.py --quiet # solo in caso di allarme (per il cron)
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__": if __name__ == "__main__":
from src.live.cli import valida
valida("venue_watch.py", USO, flag=("--quiet",), con_valore=())
raise SystemExit(main()) raise SystemExit(main())
+66
View File
@@ -0,0 +1,66 @@
"""Argomenti da riga di comando degli script operativi: un flag ignoto NON e' l'azione di default.
PERCHE' ESISTE (2026-09-02). Nessuno degli script di `scripts/live/` usava argparse: leggevano
`sys.argv` con `in` e `index`, quindi un flag sbagliato non era un errore era il ramo `else`.
Costo misurato lo stesso giorno, durante una revisione: `trades_db.py --help` non ha stampato
l'uso, e' caduto in `sync()` e ha riscritto `meta.ultimo_sync`. Gli altri due sono peggio:
`journal.py --help` avrebbe scritto la pagina di oggi e una riga nel DB, `analista.py --help`
avrebbe speso una chiamata al modello e mandato un messaggio Telegram.
TRE REGOLE, e nient'altro:
1. **Nessun argparse.** Gli script hanno flag posizionali-per-nome (`--report`, `--secco`) e
flag con valore (`--giorno 2026-08-21`); argparse cambierebbe i messaggi d'errore, il
codice d'uscita e il comportamento di `--help` di 18 script che il cron gia' chiama. Qui
serve il contrario: **lasciare intatto cio' che gira** e rifiutare cio' che non e' previsto.
2. **Il cron non deve cambiare.** I flag dichiarati sono quelli che gli script gia' accettano:
`valida()` non ne aggiunge e non ne toglie (P15: una regola si prova contro il codice che
dovrebbe eseguirla). L'unica novita' e' `--help`, che prima non esisteva.
3. **Si esce PRIMA di qualunque effetto.** `valida()` va chiamata come prima istruzione del
blocco `__main__`, prima di `connect()`, prima del sync, prima della rete.
Codici d'uscita: 0 per `--help` (l'utente ha chiesto l'uso), 2 per un flag ignoto (errore d'uso,
distinto dal 1 con cui gli script segnalano un esito negativo del loro lavoro).
"""
from __future__ import annotations
import sys
class UsoSbagliato(Exception):
"""Flag non previsto da questo script. Portato con l'elenco di cio' che e' previsto (P4)."""
def valida(nome: str, uso: str, *, flag: tuple[str, ...] = (), con_valore: tuple[str, ...] = (),
argv: list[str] | None = None, esci: bool = True) -> list[str]:
"""Controlla `argv` contro i flag dichiarati. Ritorna gli argomenti; solleva o esce se ignoti.
`flag` sono i booleani (`--quiet`), `con_valore` quelli che consumano l'argomento seguente
(`--giorno 2026-08-21`). `--help`/`-h` stampano `uso` ed escono 0. Con `esci=False` non
tocca `sys.exit` e solleva `UsoSbagliato`: e' la forma che i test usano.
"""
args = list(sys.argv[1:] if argv is None else argv)
if "--help" in args or "-h" in args:
if not esci:
return args
print(uso.strip())
raise SystemExit(0)
noti, ignoti, attesa_valore = set(flag) | set(con_valore), [], False
for a in args:
if attesa_valore: # e' il valore di un flag noto, non un flag
attesa_valore = False
continue
if not a.startswith("-"):
ignoti.append(a); continue # posizionale: nessuno di questi script ne ha
if a not in noti:
ignoti.append(a); continue
attesa_valore = a in con_valore
if attesa_valore:
ignoti.append(f"{args[-1]} (manca il valore)")
if ignoti:
msg = (f"{nome}: argomento non previsto: {', '.join(ignoti)}\n"
f" previsti: {' '.join(sorted(noti)) or '(nessuno)'}\n\n{uso.strip()}")
if not esci:
raise UsoSbagliato(msg)
print(msg, file=sys.stderr)
raise SystemExit(2)
return args
+5 -2
View File
@@ -118,7 +118,7 @@ def _px_1h(asset: str) -> pd.Series | None:
return None return None
def movimenti_capitale(con, fino_ts: str) -> dict: def movimenti_capitale(con, fino_ts: str, rows=None) -> dict:
"""Salti di equity fra letture consecutive: versamenti e prelievi, da scorporare dal P&L. """Salti di equity fra letture consecutive: versamenti e prelievi, da scorporare dal P&L.
PERCHE' ESISTE. Il P&L di giornale e' un delta di equity, quindi un versamento ci finisce PERCHE' ESISTE. Il P&L di giornale e' un delta di equity, quindi un versamento ci finisce
@@ -139,6 +139,9 @@ def movimenti_capitale(con, fino_ts: str) -> dict:
non si distingue dal mercato e RESTA nel P&L. E' lo stesso limite del rilevatore live. non si distingue dal mercato e RESTA nel P&L. E' lo stesso limite del rilevatore live.
""" """
from src.live import book as _book # lazy: prende la soglia viva, anche monkeypatchata from src.live import book as _book # lazy: prende la soglia viva, anche monkeypatchata
# `rows` si puo' passare gia' letto: due SELECT sulla stessa tabella a un'ora del cron
# possono cadere ai due lati di una scrittura e descrivere due istanti diversi.
if rows is None:
rows = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc <= ? ORDER BY ts_utc", rows = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc <= ? ORDER BY ts_utc",
(fino_ts,)).fetchall() (fino_ts,)).fetchall()
soglia = float(_book.EQUITY_JUMP_ALERT) soglia = float(_book.EQUITY_JUMP_ALERT)
@@ -231,7 +234,7 @@ def rendimento_twr(con, fino_ts: str) -> dict:
if not rows: if not rows:
return dict(twr=None, motivo="nessuna lettura di equity", segmenti=[], eventi=[], return dict(twr=None, motivo="nessuna lettura di equity", segmenti=[], eventi=[],
certi=0.0, ambigui=0.0, delta_equity=None, trading=None, letture=0) certi=0.0, ambigui=0.0, delta_equity=None, trading=None, letture=0)
mv = movimenti_capitale(con, fino_ts) mv = movimenti_capitale(con, fino_ts, rows=rows) # stessa lettura, un solo istante
e0, e1 = float(rows[0]["equity"]), float(rows[-1]["equity"]) e0, e1 = float(rows[0]["equity"]), float(rows[-1]["equity"])
if e0 <= 0: if e0 <= 0:
return dict(twr=None, trading=None, return dict(twr=None, trading=None,
+34
View File
@@ -0,0 +1,34 @@
"""Utilita' condivise dei test. `tests/` e' su sys.path (c'e' un conftest), quindi si importa
con `from helpers import carica_script`.
PERCHE' ESISTE (2026-09-02). `scripts/` non e' un package importabile per nome, quindi ogni test
che tocca uno script si caricava il modulo a mano con `importlib.util.spec_from_file_location`:
**15 copie della stessa dozzina di righe**, con nomi di modulo diversi per lo stesso file e
differenze silenziose (chi registra in `sys.modules` e chi no e chi non lo fa rompe `pickle`,
`dataclasses` e i decoratori che risolvono per nome). Una funzione sola, con la registrazione
fatta sempre.
"""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def carica_script(rel: str, nome: str | None = None):
"""Carica per PERCORSO uno script del repo (es. "scripts/live/trades_db.py").
`nome` e' il nome sotto cui il modulo viene registrato in `sys.modules`; il default e' il
nome del file. Registrare PRIMA di eseguire e' voluto: un modulo che si auto-importa o usa
`dataclasses` durante l'esecuzione deve trovarsi.
"""
p = (ROOT / rel).resolve()
assert p.exists(), f"script inesistente: {rel}"
nome = nome or p.stem
spec = importlib.util.spec_from_file_location(nome, p)
mod = importlib.util.module_from_spec(spec)
sys.modules[nome] = mod
spec.loader.exec_module(mod)
return mod
+9 -15
View File
@@ -192,12 +192,9 @@ def test_gate_requires_both_switches():
def test_config_default_disarmed(tmp_path, monkeypatch): def test_config_default_disarmed(tmp_path, monkeypatch):
"""load_config di book_execute mette execution_enabled=False di default (fail-safe).""" """load_config di book_execute mette execution_enabled=False di default (fail-safe)."""
import importlib # `scripts/` non e' un package importabile per nome: si carica via percorso
be = importlib.import_module("scripts.live.book_execute") if False else None from helpers import carica_script
# carica il modulo via path (scripts/ non e' un package importabile per nome) mod = carica_script("scripts/live/book_execute.py")
import importlib.util
spec = importlib.util.spec_from_file_location("book_execute", PROJECT_ROOT / "scripts/live/book_execute.py")
mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
monkeypatch.setattr(mod, "CONFIG", tmp_path / "nope.json") # config assente monkeypatch.setattr(mod, "CONFIG", tmp_path / "nope.json") # config assente
assert mod.load_config()["execution_enabled"] is False assert mod.load_config()["execution_enabled"] is False
@@ -419,9 +416,8 @@ def test_book_report_flags_skh_feed_error(monkeypatch):
def test_book_execute_surfaces_skh_error(monkeypatch, capsys): def test_book_execute_surfaces_skh_error(monkeypatch, capsys):
"""Se il report porta skh_error, _run() lo stampa E chiama notify (niente flat silenzioso).""" """Se il report porta skh_error, _run() lo stampa E chiama notify (niente flat silenzioso)."""
import importlib.util from helpers import carica_script
spec = importlib.util.spec_from_file_location("book_execute", PROJECT_ROOT / "scripts/live/book_execute.py") mod = carica_script("scripts/live/book_execute.py")
mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
canned = dict( canned = dict(
last_data=_fresh_bar(), online=True, real_equity=600.0, equity=600.0, eq_basis="test", last_data=_fresh_bar(), online=True, real_equity=600.0, equity=600.0, eq_basis="test",
@@ -481,9 +477,8 @@ def test_book_report_propagates_pos_error(monkeypatch):
def test_book_execute_halts_on_unreadable_position(monkeypatch, capsys): def test_book_execute_halts_on_unreadable_position(monkeypatch, capsys):
"""ARMATO + --execute + ordine presente: il gate DEVE fermarsi PRIMA di costruire il trader.""" """ARMATO + --execute + ordine presente: il gate DEVE fermarsi PRIMA di costruire il trader."""
import importlib.util from helpers import carica_script
spec = importlib.util.spec_from_file_location("book_execute", PROJECT_ROOT / "scripts/live/book_execute.py") mod = carica_script("scripts/live/book_execute.py")
mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
canned = dict( canned = dict(
last_data=_fresh_bar(), online=True, real_equity=598.0, equity=598.0, eq_basis="mainnet USDC", last_data=_fresh_bar(), online=True, real_equity=598.0, equity=598.0, eq_basis="mainnet USDC",
@@ -542,9 +537,8 @@ def test_book_report_propagates_eq_fallback(monkeypatch):
def test_book_execute_eq_fallback_warns_but_proceeds(monkeypatch, capsys): def test_book_execute_eq_fallback_warns_but_proceeds(monkeypatch, capsys):
"""eq_fallback: avvisa + notify MA PROSEGUE (non e' un halt, a differenza di pos_error).""" """eq_fallback: avvisa + notify MA PROSEGUE (non e' un halt, a differenza di pos_error)."""
import importlib.util from helpers import carica_script
spec = importlib.util.spec_from_file_location("book_execute", PROJECT_ROOT / "scripts/live/book_execute.py") mod = carica_script("scripts/live/book_execute.py")
mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
canned = dict( canned = dict(
last_data=_fresh_bar(), online=True, real_equity=None, equity=2000.0, last_data=_fresh_bar(), online=True, real_equity=None, equity=2000.0,
+2 -5
View File
@@ -116,11 +116,8 @@ def test_posizione_flat_non_puo_diventare_naked():
# B. Isolamento per asset dentro book_execute._run(). # B. Isolamento per asset dentro book_execute._run().
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _carica_book_execute(): def _carica_book_execute():
spec = importlib.util.spec_from_file_location( from helpers import carica_script
"book_execute", PROJECT_ROOT / "scripts/live/book_execute.py") return carica_script("scripts/live/book_execute.py")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def _report_due_asset(): def _report_due_asset():
+4 -10
View File
@@ -58,11 +58,8 @@ def test_funzionale_feed_stantio_blocca_lesecuzione_e_allerta(monkeypatch, capsy
"""Il test che conta: conto ONLINE, posizione LEGGIBILE, ordine PRESENTE — cioe' la situazione """Il test che conta: conto ONLINE, posizione LEGGIBILE, ordine PRESENTE — cioe' la situazione
esatta del 2026-07-14 ma feed vecchio. Non deve partire nessun ordine e deve arrivare l'alert. esatta del 2026-07-14 ma feed vecchio. Non deve partire nessun ordine e deve arrivare l'alert.
""" """
import importlib.util from helpers import carica_script
spec = importlib.util.spec_from_file_location( mod = carica_script("scripts/live/book_execute.py", "book_execute_iso")
"book_execute_iso", ROOT / "scripts" / "live" / "book_execute.py")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
canned = dict( canned = dict(
last_data="2026-07-08", online=True, real_equity=598.0, equity=598.0, last_data="2026-07-08", online=True, real_equity=598.0, equity=598.0,
@@ -94,11 +91,8 @@ def test_funzionale_feed_stantio_blocca_lesecuzione_e_allerta(monkeypatch, capsy
def test_config_senza_la_chiave_non_esplode(monkeypatch, capsys): def test_config_senza_la_chiave_non_esplode(monkeypatch, capsys):
"""Una config priva di max_data_age_days deve ricadere sul default, non sollevare KeyError """Una config priva di max_data_age_days deve ricadere sul default, non sollevare KeyError
dentro il percorso con soldi veri.""" dentro il percorso con soldi veri."""
import importlib.util from helpers import carica_script
spec = importlib.util.spec_from_file_location( mod = carica_script("scripts/live/book_execute.py", "book_execute_iso2")
"book_execute_iso2", ROOT / "scripts" / "live" / "book_execute.py")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
canned = dict(last_data=str(pd.Timestamp.now(tz="UTC").normalize().date()), online=True, canned = dict(last_data=str(pd.Timestamp.now(tz="UTC").normalize().date()), online=True,
real_equity=598.0, equity=598.0, eq_basis="t", cap_per_asset=300.0, real_equity=598.0, equity=598.0, eq_basis="t", cap_per_asset=300.0,
skh_error=None, pos_error=None, assets=[], orders=[]) skh_error=None, pos_error=None, assets=[], orders=[])
+168
View File
@@ -0,0 +1,168 @@
"""Un flag ignoto non e' l'azione di default (2026-09-02).
Costo misurato durante una revisione dello stesso giorno: `trades_db.py --help` non stampava
l'uso, cadeva in `sync()` e riscriveva `meta.ultimo_sync`. Su `journal.py` lo stesso errore
avrebbe scritto una pagina e una riga di DB; su `analista.py` avrebbe speso una chiamata al
modello e mandato un Telegram.
Il test guarda anche l'altra meta' del contratto (P15): i flag che il CRON usa devono restare
accettati una guardia che rompe il cron e' peggio del difetto che ripara.
"""
from __future__ import annotations
import ast
import re
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from src.live.cli import UsoSbagliato, valida # noqa: E402
# Tutti gli script di scripts/live/ che NON usano argparse: la guardia deve coprirli tutti, e
# l'elenco si DERIVA dalla cartella (P1) — un file nuovo senza guardia fa fallire il test, non
# passa inosservato perche' nessuno ha aggiornato una lista.
LIVE = Path(__file__).resolve().parents[1] / "scripts" / "live"
def _senza_argparse() -> list[str]:
fuori = []
for f in sorted(LIVE.glob("*.py")):
s = f.read_text()
if f.name == "__init__.py" or "argparse" in s or "if __name__" not in s:
continue
fuori.append(f.name)
return fuori
SCRIPT_GUARDATI = tuple(_senza_argparse())
assert SCRIPT_GUARDATI, "nessuno script da guardare: il modulo non sta controllando niente"
SCRIVONO = ("trades_db.py", "journal.py", "analista.py") # i tre che hanno pagato il difetto
# ---------------------------------------------------------------- la funzione
def _v(argv, **kw):
return valida("x.py", "uso: x.py", argv=argv, esci=False, **kw)
def test_un_flag_noto_passa_e_torna_gli_argomenti():
assert _v(["--report"], flag=("--report",)) == ["--report"]
def test_un_flag_ignoto_e_un_errore_non_il_default():
with pytest.raises(UsoSbagliato) as e:
_v(["--help-me"], flag=("--report",))
assert "--help-me" in str(e.value) and "--report" in str(e.value) # P4: cosa, e cosa si puo'
def test_il_valore_di_un_flag_non_e_scambiato_per_un_flag():
assert _v(["--giorno", "2026-08-21"], con_valore=("--giorno",)) == ["--giorno", "2026-08-21"]
with pytest.raises(UsoSbagliato):
_v(["--giorno"], con_valore=("--giorno",)) # manca il valore
def test_un_valore_che_sembra_un_flag_resta_un_valore():
assert _v(["--nota", "--quiet"], con_valore=("--nota",), flag=("--quiet",))
def test_un_posizionale_e_ignoto():
with pytest.raises(UsoSbagliato):
_v(["report"], flag=("--report",))
def test_senza_argomenti_non_solleva():
assert _v([], flag=("--report",)) == []
def test_help_non_e_un_errore():
assert _v(["--help"], flag=("--report",)) == ["--help"]
# ---------------------------------------------------------------- gli script veri
def _uso(nome: str) -> str:
"""La costante USO dello script, letta dal sorgente senza importarlo."""
src = (ROOT / "scripts" / "live" / nome).read_text()
for n in ast.parse(src).body:
if isinstance(n, ast.Assign) and getattr(n.targets[0], "id", "") == "USO":
return n.value.value
raise AssertionError(f"{nome} non dichiara USO")
@pytest.mark.parametrize("nome", SCRIPT_GUARDATI)
def test_lo_script_chiama_valida_prima_di_ogni_effetto(nome):
"""`valida` dev'essere la PRIMA istruzione eseguibile del blocco __main__: uscire dopo aver
aperto il DB o sincronizzato sarebbe uscire dopo l'effetto."""
src = (ROOT / "scripts" / "live" / nome).read_text()
main = [n for n in ast.parse(src).body
if isinstance(n, ast.If) and "__main__" in ast.dump(n.test)]
assert main, f"{nome}: nessun blocco __main__"
corpo = [n for n in main[0].body if not isinstance(n, ast.ImportFrom | ast.Import)]
chiamate = [n for n in ast.walk(corpo[0]) if isinstance(n, ast.Call)]
assert any(getattr(c.func, "id", "") == "valida" for c in chiamate), \
f"{nome}: la prima istruzione di __main__ non e' valida()"
@pytest.mark.parametrize("nome", SCRIPT_GUARDATI)
def test_l_uso_elenca_esattamente_i_flag_dichiarati(nome):
"""P4 e P1: il testo d'aiuto non e' prosa libera — deve nominare i flag che lo script accetta."""
src = (ROOT / "scripts" / "live" / nome).read_text()
dichiarati = set(re.findall(r'["\'](--[a-z-]+)["\']', src.split("if __name__")[1]))
uso = _uso(nome)
mancanti = {f for f in dichiarati if f not in uso}
assert not mancanti, f"{nome}: flag accettati ma non documentati in USO: {mancanti}"
# I flag che il cron usa DAVVERO, letti dagli script di cron: se la guardia ne rifiutasse uno,
# il libro di bordo si fermerebbe la notte stessa (P16: il cron gira dal working tree).
def test_i_flag_del_cron_sono_tutti_accettati():
usati: dict[str, set[str]] = {}
for sh in (ROOT / "scripts").glob("cron_*.sh"):
for riga in sh.read_text().splitlines():
for nome in SCRIPT_GUARDATI:
if f"scripts/live/{nome}" in riga and not riga.lstrip().startswith("#"):
usati.setdefault(nome, set()).update(re.findall(r"(--[a-z-]+)", riga))
assert usati, "nessun cron chiama gli script guardati: il test non sta controllando niente"
for nome, flag in usati.items():
src = (ROOT / "scripts" / "live" / nome).read_text()
noti = set(re.findall(r'["\'](--[a-z-]+)["\']', src.split("if __name__")[1]))
assert flag <= noti, f"{nome}: il cron usa {flag - noti}, che la guardia rifiuterebbe"
# controllo positivo (M15): il confronto sa dire di no. Se il cron usasse un flag non
# dichiarato, questo test lo direbbe — qui lo si verifica su un caso finto.
assert not {"--inventato"} <= {"--quiet"}
@pytest.mark.parametrize("nome", SCRIVONO)
def test_help_esce_zero_e_non_scrive(nome):
"""Controllo end-to-end (M15): il caso che ha causato il difetto, sui tre script veri."""
prima = (ROOT / "data" / "live" / "trades.db").stat().st_mtime
r = subprocess.run([sys.executable, str(ROOT / "scripts" / "live" / nome), "--help"],
capture_output=True, text=True, cwd=ROOT, timeout=120)
assert r.returncode == 0, r.stderr[-500:]
assert "uso:" in r.stdout
assert (ROOT / "data" / "live" / "trades.db").stat().st_mtime == prima
@pytest.mark.parametrize("nome", SCRIVONO)
def test_un_flag_ignoto_esce_due_e_non_scrive(nome):
prima = (ROOT / "data" / "live" / "trades.db").stat().st_mtime
r = subprocess.run([sys.executable, str(ROOT / "scripts" / "live" / nome), "--nonesiste"],
capture_output=True, text=True, cwd=ROOT, timeout=120)
assert r.returncode == 2, r.stdout[-300:]
assert "non previsto" in r.stderr
assert (ROOT / "data" / "live" / "trades.db").stat().st_mtime == prima
def test_ogni_script_senza_argparse_ha_la_guardia():
"""L'elenco e' derivato dalla cartella: uno script nuovo che legge sys.argv a mano e non
chiama `valida` fa fallire questo test invece di ripetere il difetto in silenzio."""
senza = [n for n in SCRIPT_GUARDATI
if "valida(" not in (LIVE / n).read_text().split("if __name__")[1]]
assert not senza, f"script senza guardia sui flag: {senza}"
assert len(SCRIPT_GUARDATI) >= 17, f"troppo pochi script guardati: {SCRIPT_GUARDATI}"
+21
View File
@@ -659,3 +659,24 @@ def test_la_leva_del_classificatore_include_la_scala_e_il_tetto_di_codice(db, mo
_serie_equity(db, [("2026-08-25T10:47:01+00:00", 600.0), ("2026-08-25T11:47:01+00:00", 2000.0)]) _serie_equity(db, [("2026-08-25T10:47:01+00:00", 600.0), ("2026-08-25T11:47:01+00:00", 2000.0)])
mv = J.movimenti_capitale(db, "2026-08-25T23:59:59+00:00") mv = J.movimenti_capitale(db, "2026-08-25T23:59:59+00:00")
assert mv["leva_tetto"] == pytest.approx(min(0.5 * 2 * 1.25, B.LEVA_LORDA_MAX)) assert mv["leva_tetto"] == pytest.approx(min(0.5 * 2 * 1.25, B.LEVA_LORDA_MAX))
def test_balance_watch_registra_anche_l_indice_usde(monkeypatch):
"""Debito §5.15, primo passo (02/09): serve una serie ORARIA dell'indice usde_usdc — quella
di `usde_watch` e' giornaliera, e il classificatore confronta letture a un'ora di distanza.
Il campione la porta, e un indice non leggibile e' None con la ragione, mai 1.0 (P5)."""
from helpers import carica_script
bw = carica_script("scripts/live/balance_watch.py")
class _C:
def account_summary(self, cur): return {"balance": 1.0, "equity": 1.0}
def positions(self, cur): return []
from src.live import usde as U
monkeypatch.setattr(U, "prezzo_indice", lambda *a, **k: 0.9997)
assert bw.campiona(_C())["usde_usdc"] == 0.9997
def _rotto(*a, **k): raise RuntimeError("giu'")
monkeypatch.setattr(U, "prezzo_indice", _rotto)
rec = bw.campiona(_C())
assert rec["usde_usdc"] is None and rec["usde_usdc_errore"] == "RuntimeError"
+15 -20
View File
@@ -6,25 +6,21 @@ finisce in tmp grazie alla fixture autouse di conftest: qui non si tocca il libr
""" """
from __future__ import annotations from __future__ import annotations
import importlib.util
import sys import sys
from pathlib import Path from pathlib import Path
import pandas as pd import pandas as pd
import pytest
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from helpers import carica_script # noqa: E402
from src.live import journal as J # noqa: E402 from src.live import journal as J # noqa: E402
from src.live import tradesdb as T # noqa: E402 from src.live import tradesdb as T # noqa: E402
def _carica_script(): def _carica_script():
spec = importlib.util.spec_from_file_location("trades_db_script", ROOT / "scripts/live/trades_db.py") return carica_script("scripts/live/trades_db.py", "trades_db_script")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def _px_piatto(livello=100.0): def _px_piatto(livello=100.0):
@@ -33,11 +29,11 @@ def _px_piatto(livello=100.0):
def _prepara(con, equity): def _prepara(con, equity):
con.execute("INSERT INTO fills (fill_id, ts_utc, ts_source, asset, side, qty, price, fee) " # il fill si scrive con `upsert_fills`, non con un INSERT a mano: una riga scritta a mano
"VALUES ('f1','2026-08-24T10:47:15+00:00','test','BTC','buy',0.001,60000.0,0.01)") # aveva `verified` NULL, una forma che la produzione non produce mai
for ts, eq in equity: T.upsert_fills(con, [T.Fill(ts_utc="2026-08-24T10:47:15+00:00", ts_source="test",
con.execute("INSERT INTO equity (ts_utc, equity, src) VALUES (?,?,?)", (ts, eq, "test")) asset="BTC", side="buy", qty=0.001, price=60000.0, fee=0.01)])
con.commit() T.upsert_equity(con, [(ts, eq, "test") for ts, eq in equity])
def test_il_report_stampa_il_TWR_e_non_il_grezzo(monkeypatch, capsys): def test_il_report_stampa_il_TWR_e_non_il_grezzo(monkeypatch, capsys):
@@ -51,10 +47,10 @@ def test_il_report_stampa_il_TWR_e_non_il_grezzo(monkeypatch, capsys):
con.close() con.close()
_carica_script().report() _carica_script().report()
out = capsys.readouterr().out out = capsys.readouterr().out
assert "TWR : +3.95%" in out assert "TWR" in out and "+3.95%" in out
assert "+234.95%" not in out and "+235" not in out # il grezzo non compare piu' assert "+234.95%" not in out # il grezzo non compare piu'
assert "movimenti capitale : +1,400.00 certi" in out assert "+1,400.00 certi" in out
assert "trading da arming : +9.70" in out assert "+9.70" in out
assert "movimenti di capitale INCLUSI" in out # il delta $ resta, etichettato assert "movimenti di capitale INCLUSI" in out # il delta $ resta, etichettato
@@ -65,7 +61,7 @@ def test_il_report_senza_movimenti_dice_lo_stesso_numero_di_prima(monkeypatch, c
con.close() con.close()
_carica_script().report() _carica_script().report()
out = capsys.readouterr().out out = capsys.readouterr().out
assert "TWR : +5.00%" in out and "movimenti capitale" not in out assert "+5.00%" in out and "movimenti capitale" not in out
def test_il_report_a_base_zero_dice_n_d_e_non_l_intero_conto(monkeypatch, capsys): def test_il_report_a_base_zero_dice_n_d_e_non_l_intero_conto(monkeypatch, capsys):
@@ -77,9 +73,8 @@ def test_il_report_a_base_zero_dice_n_d_e_non_l_intero_conto(monkeypatch, capsys
con.close() con.close()
_carica_script().report() _carica_script().report()
out = capsys.readouterr().out out = capsys.readouterr().out
assert "trading da arming : n/d" in out and "TWR : n/d" in out assert out.count("n/d") >= 2 and "TWR" in out
assert "trading da arming : +610.00" not in out # l'intero conto NON e' "trading" assert "+610.00 di equity" in out and "arming : +610.00" not in out
assert "(+610.00 di equity, movimenti di capitale INCLUSI)" in out # il fatto resta, etichettato
def test_il_report_con_il_feed_assente_non_stampa_il_grezzo_come_TWR(monkeypatch, capsys): def test_il_report_con_il_feed_assente_non_stampa_il_grezzo_come_TWR(monkeypatch, capsys):
@@ -93,6 +88,6 @@ def test_il_report_con_il_feed_assente_non_stampa_il_grezzo_come_TWR(monkeypatch
con.close() con.close()
_carica_script().report() _carica_script().report()
out = capsys.readouterr().out out = capsys.readouterr().out
assert "TWR : n/d" in out and "trading da arming : n/d" in out assert out.count("n/d") >= 2 and "TWR" in out
assert "% =" not in out and "+236" not in out assert "% =" not in out and "+236" not in out
assert "[ambiguo: mercato non misurabile" in out assert "[ambiguo: mercato non misurabile" in out