Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a7f670966 | |||
| e4efdaaa53 | |||
| b46041d11d |
@@ -81,6 +81,10 @@ data/chain_collect/
|
||||
data/vrp_f_watch/
|
||||
# giri misurati del trigger di regime CC01 (letti da monitor_health, #12)
|
||||
data/cc01_regime/
|
||||
# giri misurati dei sorveglianti generalizzati da cc01 (#16), letti da monitor_health
|
||||
data/edge_watch/
|
||||
data/scale_watch/
|
||||
data/venue_news/
|
||||
|
||||
# log esecuzioni del book live (stato runtime, contiene fill/fee del conto reale)
|
||||
data/live/
|
||||
|
||||
@@ -72,10 +72,14 @@ scrive in un registro, **senza inviare**. Finestra dichiarata fino al **30/09/20
|
||||
`data/live/equity_seen.json`. Sui giri delle 18:17, 19:17 e 20:44 del 16/09 **nessun file** di
|
||||
`data/` o `logs/` di questo progetto e' cambiato: il watermark di qui non lo tocca (P16).
|
||||
- **perche' non invia**: `book_ombra.py` non contiene il percorso d'invio, e
|
||||
`TradingBook/tests/test_ombra_non_invia.py` lo verifica sul sorgente. 🚨 **La garanzia e' il CODICE,
|
||||
non il permesso**: la chiave puo' fare trading. Una chiave Deribit di **sola lettura** per l'ombra
|
||||
la crea l'operatore; finche' non c'e', qualunque script di TradingBook con un percorso d'invio
|
||||
opererebbe con soldi veri.
|
||||
`TradingBook/tests/test_ombra_non_invia.py` lo verifica sul sorgente. ✅ **Dal 21/09 anche il
|
||||
PERMESSO** (TradingBook#19): il gateway chiede a Deribit un token con scope
|
||||
`account:read trade:read wallet:none block_trade:none block_rfq:none custody:none` e rifiuta il token
|
||||
se lo scope concesso contiene un `:read_write`. Misurato il 21/09: senza scope la chiave dava
|
||||
**tutto in read_write, anche `wallet` e `custody`**; con lo scope ridotto Deribit concede
|
||||
`trade:read account:read` e risponde `13021 forbidden` a un `private/cancel`. 🚨 **Il segreto resta
|
||||
quello pieno**: chi legge `TradingBook/secrets/deribit.json` puo' chiedere un token senza scope. Una
|
||||
chiave Deribit di **sola lettura** per l'ombra la crea solo l'operatore.
|
||||
- **orario**: `:44` con `timeout -k 10 150`, quindi finisce entro le `:46:40`, prima del giro di qui.
|
||||
Era `:17` fino al 16/09 e cadeva nello slot di release del martedi' (15/09 09:17: manutenzione
|
||||
11051, due BUY calcolati sul paper capital nel registro d'ombra); da TradingBook#16 un conto o una
|
||||
@@ -111,9 +115,9 @@ fino al 31,8%*, che dimostra ≥, non =. Non spiegato (D5): puo' tornare. `venue
|
||||
`quota_max_frac` → **0,85** (il valore che l'operatore aveva scelto il 30/08 per questo scenario).
|
||||
✅ **Il cuscino USDC e' sorvegliato dal 10/09** (`cuscino_watch`, §5.17): una perdita del libro (che si regola in USDC)
|
||||
fa salire la quota da sola, e sotto zero di slack il sorvegliante vende USDE; prima nessuno lo guardava — a −$58 di USDC
|
||||
il cuscino era gia' scoperto. GATE USDE-01 in §4). ⚠️ **Ogni versamento abbassa la quota**: dopo un bonifico si
|
||||
il cuscino era gia' scoperto. GATE USDE-01 in §4). ⚠️ **Ogni versamento abbassa la quota**: dopo un bonifico va riportata al bersaglio, e
|
||||
**dal 10/09 lo fa `cuscino_watch` da solo** (stato ECCEDENTE: slack > 40% del cuscino ⇒ ricompra USDE fino a quota **0,64**, issue #7); prima si rilanciava `usde_convert --quota 0.70` a mano, e il 04/09 nessuno l'aveva fatto. Autorita' dei parametri: sezione `usde` di `config/live.json`
|
||||
(haircut **5%** — ✅ divergenza chiusa il 31/08, la config diceva 10% ed era sbagliato (`r0831_margini_conto.py`); oggi comunque **non si applica**, vedi sopra — tetto ALLERTA quota 50%, soglie depeg 0,99/0,95 coi criteri in `_nota_usde`), letta
|
||||
(haircut **5%** — ✅ divergenza chiusa il 31/08, la config diceva 10% ed era sbagliato (`r0831_margini_conto.py`); oggi comunque **non si applica**, vedi sopra — tetto ALLERTA `quota_max_frac` **0,85** (era 50% fino al 06/09; 0,50 resta solo come default di codice in `src/live/usde.py`, la config lo sovrascrive), soglie depeg 0,99/0,95 coi criteri in `_nota_usde`), letta
|
||||
da `src/live/usde.py` — shadow (sizing orario) e `usde_watch` (sorveglianza 12:35 UTC, cron
|
||||
proprio, sotto `monitor_health`) derivano entrambi da li'. ⚠️ Il difetto d'origine: `shadow._equity`
|
||||
leggeva SOLO il conto USDC — la prima conversione avrebbe causato un falso "USCITA DI FONDI −24%"
|
||||
@@ -277,7 +281,7 @@ sull'hold-out; cambiarne le soglie guardando l'esito, pure.
|
||||
| **USDE-01 (eligibilita' reward)** | 2026-08-29 | ≥1 reward entro il 29/08 → IDONEO e si apre la decisione di QUOTA (dell'operatore; tetto allerta 50%, mai 100% — R1 emittente non recuperabile); zero → si riconverte e la pista si chiude | ✅ **CHIUSO IDONEO** il 2026-08-28 12:35Z: primo reward **+0,052055 USDE** rilevato per delta al netto dei trade, coi criteri pre-registrati il 26/08 PRIMA dell'esito. **QUOTA DECISA il 30/08** (l'operatore ha anticipato il rinvio: *"porta in usde tutto il capitale che non viene usato"*): bersaglio **70%**, `usde.quota_target` in config (📌 **dal 10/09 la quota decisa e' 0,64, derivata dalle bande del cuscino: §3 e §5.17**). Tasso a 4 finestre (30/08): **3,26% annuo, banda bootstrap [1,13% – 4,51%]** — 3/4 hanno pagato (0,00 · 3,80 · 4,51 · 4,51%), e la banda larga 3,4 punti **e' il risultato**: a n=4 il tasso non e' misurato. `usde_watch.rendimento()` lo stampa **col suo n**; `quota_da_decidere()` lo chiede ogni giorno da lunedi'. 🚨 **L'EV non puo' scegliere la quota** — e' lineare in q, quindi il suo argmax e' sempre un angolo (0% o 100%): ogni quota interna nasce da un criterio sulla **coda**, dichiarato non ottimizzato (M8). Il resto e' misurato (`r0830_usde_quota.py`, diario 30/08): **hazard di pareggio = l'APR** (EV-positiva sse Ethena sta sotto il 3,3%/anno, numero **assunto** come il `p` di Deribit) · l'haircut **non morde a nessuna quota** fino al 90%, nemmeno a leva 1,50x con USDE marcato a 0,95 (29× di cuscino): **il vincolo non e' il margine** · la resa di oggi vale **$16/anno**, meno di UN mese di versamento ⇒ la leva binding resta il **bonifico** · gia' a 24,2% l'evento emittente vale **3,1× il maxDD dell'intero libro**. 📌 **Aspettare costa $1,33**: alzare a 50% fra 4 settimane invece che subito lascia sul tavolo $1,33 di resa (conversione reversibile a ~6 bps) e porta il campione da **4 a 32 finestre**, stringendo la banda ~2,8×. 🚨 **DUE FATTI DEL 30/08 CHE CAMBIANO IL GATE** (diario `2026-08-30-usde-quota-tetto-venue`). **(a) TETTO DEL VENUE ≈31-32% dell'equity — e' una FRAZIONE, non un livello.** 🚨 **SUPERATO IL 06/09: il
|
||||
tetto non c'era piu'** — quota portata al **68,7%** con 39 ordini senza rifiuti, fermata dal cuscino di
|
||||
regolamento (§1). Quanto segue e' la misura del 30-31/08, vera allora, tenuta perche' il tetto puo' tornare. Ogni acquisto oltre il tetto e' rifiutato con `not_enough_funds_in_currency` pur avendo **$1.400 disponibili**: messaggio **fuorviante**, che ha fatto inseguire tre ipotesi sbagliate (taglia, cadenza, prezzo). Che sia un tetto e non un problema d'ordine e' provato a saldo neutro (BUY rifiutato → SELL OK → BUY riaccettato → BUY rifiutato); che sia una **frazione** e' provato il 31/08 lasciando scendere l'equity di $8: **il tetto e' sceso con lei** — 30/08 in [644,18·645,18) con equity $2.063,79 = **31,21-31,26%**, 31/08 in [643,18·644,18) con equity $2.055,56 = **31,29-31,34%** (0,05pp di scarto, dentro il rumore dell'equity che oscilla ±$2-8/ora). ⚠️ **Ri-sondato il 31/08 dopo il passaggio a X:SM** (l'ipotesi era che il tetto dipendesse dal modello segregato): **[654,18-655,18) con equity $2.054,90 = 31,84-31,88%**. Il modello ENTRA nel tetto ma **non lo spiega** — ha comprato **+0,55pp = +11 USDE (~$11)**, non le centinaia che servirebbero. ⇒ **il tetto SCALA col conto**: la quota resta ~31% e il valore in dollari cresce col capitale, ma **il 70% non e' raggiungibile ne' ora ne' mai**. 🚨 **Deribit non lo documenta**: l'articolo ufficiale *Yield/reward bearing coins* descrive un **Cap ETHENA** che diluisce il **TASSO** a livello di exchange (`User APR = min(bal_exchange, cap)/bal_exchange × Ethena APR`) e **nessun limite sulle quantita' detenibili** da un conto. Ora e' in config (`usde.venue_cap_frac` 0.312, bordo basso dei bracket) e `usde_convert` **rifiuta il piano prima di sparare** invece di scoprirlo dal venue. **(b) L'USDC NON frutta SUL NOSTRO CONTO — misurato, e la premessa del gate e' RESTAURATA.** `public/get_currencies` dice USDC 3,4000% (USDE 4,1071%) e per qualche ora quel numero e' stato scritto qui come se fosse nostro: **era un listino del venue, non un accredito**. Deribit paga i reward USDC **una volta al MESE** («paid out as a single monthly payment early in the following month», accrual 00:00 UTC sulla **minima equity**) — non ogni giorno come l'USDE, ed e' per questo che una sorveglianza giornaliera non poteva vederli. Verificato sui **due confini di mese a libro FLAT**, dove `equity == balance == USDC` e ogni scalino sarebbe un accredito: **29/06→08/07 equity 598,06 costante, 237/237 ore ferme, 0 scalini** (attesi $0,45) e **31/07→03/08 equity 596,92 costante per 4 giorni** (attesi **$1,72 contro una risoluzione di $0,01, 172×**). **Zero misurato, non zero sotto soglia.** ⇒ il guadagno di tenere USDE e' il **tasso pieno ~4,1%**, non uno spread: **~$26/anno** sui $644. 🚨 **Il motivo ora e' DOCUMENTATO, e non e' MiCA come avevo ipotizzato**: l'articolo ufficiale elenca le giurisdizioni escluse dai reward USDC e **l'ITALIA c'e'** (con Canada e Giappone: non e' una lista MiCA, e' policy di giurisdizione Deribit). ⚠️ **E la finestra di pagamento e' piu' larga di quella che avevo controllato**: «within the **first two weeks** of the following month». Rifatta la verifica sulle due settimane vere: **luglio e' conclusivo** (atteso $1,72 contro un'escursione TOTALE dell'equity di **$0,48** in 1-16/08, zero scalini compatibili), **giugno NO** (il libro ha iniziato a operare a meta' mese e il rumore e' della taglia del segnale). Quindi: **una finestra conclusiva piu' la lista ufficiale**, non due finestre — il «172× la risoluzione» del 30/08 era sovra-affermato. 📌 **Un tasso pubblicato non e' un tasso incassato: si verifica sul CONTO** — ed e' costato una query su una serie che avevamo gia'. Strumento: **`scripts/live/balance_watch.py`** + `cron_balance.sh` (orario :42, sola lettura) registra il **balance a 8 decimali**, la serie che mancava. 📌 **Il vincolo che l'analisi del margine non aveva guardato**: P&L e funding dei perp USDC-lineari **si regolano in USDC**, quindi la quota massima difendibile non e' data dall'haircut ma dal **cuscino di regolamento** = disaster-SL sulla massima esposizione lorda (`n_asset × frac × disaster_sl_pct` = **30% dell'equity**), che lascia esattamente il **70%** — coincidenza per costruzione, zero slack (M8: non e' un argmax). 📌 **Il costo del NON averlo e' ora documentato e vale il criterio**: un'equity negativa in una valuta paga una *collateral fee* dello **0,05% AL GIORNO** — **18,25%/anno**, addebitata al secondo, **4,3× la resa USDE che si starebbe comprando** (fonte: KB *Cross collateral specifications*). Il ribilanciamento automatico NON salva: scatta solo a **$1M** assoluti o al **100% della cross equity**, soglie che a $2k non si toccano mai — si sanguina la fee e basta. Il 30/08 avevo scritto «Deribit lo finanzia a interesse» **senza il numero**: il numero e' questo, e rende il cuscino piu' giustificato, non meno. Attrezzo: **`scripts/live/usde_convert.py`** (dry-run di default, guardie proprie; NON passa da `execution.ALLOWED`, che resta ai soli due perp) |
|
||||
| **STATARB** | 2026-09-27 | soglie 24/07 (non toccate) + diagnostica statica sempre-short | ✅ monitor riparato e rigenerato il 26/08: la serie vera da' Sharpe **−1,61** a oggi → coi criteri invariati punta al **RITIRO**. Si legge il 27/09, non prima |
|
||||
| **STATARB** | 2026-09-27 | soglie 24/07 (non toccate), **copiate verbatim** da `scripts/research/r0724_statarb_deploy_gate.py` (docstring e costanti; diario `2026-07-24-goal-50eur-roadmap.md` §Migliorie 1). Metrica: «Sharpe annualizzato di net_modeled su TUTTA la finestra forward (nessuna sottofinestra), da data/paper_statarb/returns.jsonl». «Sharpe >= 0.5 -> CANDIDATO AL DEPLOY: proporre peso {10,15,20}% del book Deribit, giudicato con weights_tilt_null + stima maxDD combinata < 15%. Deploy solo se il tilt-null passa.» · «0 <= Sh < 0.5 -> ESTENDI una sola volta di altri 90g (decisione 2026-12-26). Alla seconda scadenza: o >=0.5 o RITIRO. Niente terza proroga.» · «Sharpe < 0 -> RITIRO dal forward-monitor». Guardie «tutte richieste per il deploy»: «fill-haircut cumulato \|MODELED - REAL\| < 0.5 pp» · «config invariata (W=45, sgn=+1)» · «maxDD forward < 10%». Addendum 25/07: Sharpe della **statica sempre-short** a pari vol-target sulla stessa finestra — «Non e' una soglia binding», diagnostica obbligatoria; renderla binding spetta all'operatore. La lettura si fa lanciando lo script (`uv run python scripts/research/r0724_statarb_deploy_gate.py`), che applica la regola dal 27/09 | ✅ monitor riparato e rigenerato il 26/08: la serie vera da' Sharpe **−1,61** a oggi → coi criteri invariati punta al **RITIRO**. Si legge il 27/09, non prima |
|
||||
| **XSR01** | 2026-10-23 | Sharpe ≥1,0 **E** haircut ≤40% — dal 26/08 il numero decisivo e' `r0826_xsr_haircut.py` a **pavimento $10**, citato con la frazione di ordini eseguiti — poi `weights_tilt_null` e **capitale ≥$20k** | ✅ **riscritto il 26/08 su decisione dell'operatore** (dichiarato prima dell'esito, stringe: diario `2026-08-26-xsr01-gate-riscritto`): capitale allineato a "100% Deribit fino a $20k"; serie forward rigenerata (31 barre vere, Sharpe −2,72). Sotto la lente RENDITA il sleeve resta **eguagliato da un conto remunerato** (📌 sotto) |
|
||||
| **DVOLSPREAD** | kill 2026-10-24 se Sh<−0,50; decisione 2027-01-24 | (a) Sharpe>0 (b) marginale ADDS+robust_oos+insample_edge (c) **DSR ≥0,95** (d) `weights_tilt_null`; **veto** se barre attive <80% → si estende, non si decide | ✅ serie rigenerata il 26/08 (−4,41 a oggi, era −14,73 sulla serie rotta); il veto barre-attive ora conta barre VERE e la guardia PREMATURO sorveglia il monitor |
|
||||
| **GATE PROP-01** | chiuso 22-23/08 | (a) listino 13/13 ✅ (b) ancora SKH01 23/23 ✅ (c) economica+sopravvivenza 2/2 ✅ | **chiuso 3/3** — ma il numero operativo e' sceso 42% → 7,8% → 4,4% → 2,6% |
|
||||
|
||||
@@ -42,10 +42,13 @@ import pandas as pd
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.live.giri import registra_giro # noqa: E402
|
||||
from src.live.notifier import notify # noqa: E402
|
||||
from src.portfolio.portfolio import combine_outer # noqa: E402
|
||||
from src.portfolio.sleeves import deribit_book_sleeves, tp01_sleeve # noqa: E402
|
||||
|
||||
RUNS = ROOT / "data" / "edge_watch" / "runs.jsonl"
|
||||
|
||||
# --- soglie CONGELATE (cambiarle invalida la taratura di r0726_edge_death.py) ---
|
||||
WIN_MONTHS = 36
|
||||
SHARPE_KILL = -0.5
|
||||
@@ -119,6 +122,7 @@ def run() -> dict:
|
||||
|
||||
def main() -> int:
|
||||
r = run()
|
||||
registra_giro(RUNS, sharpe=r["sharpe"], n_alert=len(r["alerts"]))
|
||||
if "--quiet" not in sys.argv:
|
||||
print("=" * 78)
|
||||
print(" EDGE WATCH — il book live sta ancora funzionando?")
|
||||
|
||||
@@ -49,9 +49,11 @@ ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.live.book import INSTRUMENT as BOOK_INSTRUMENT # noqa: E402
|
||||
from src.live.giri import registra_giro # noqa: E402
|
||||
from src.live.notifier import notify # noqa: E402
|
||||
|
||||
STATE = ROOT / "data" / "fee_watch" / "state.json"
|
||||
RUNS = ROOT / "data" / "fee_watch" / "runs.jsonl"
|
||||
# ⚠️ CORREZIONE 2026-08-21. Qui c'era la tupla CABLATA ("BTC-PERPETUAL", "ETH-PERPETUAL") — gli
|
||||
# INVERSE, regolati in BTC/ETH — mentre il book esegue sui LINEARI USDC (BTC_USDC-PERPETUAL).
|
||||
# Sono due linee di prodotto distinte e Deribit le cambia in modo indipendente: il 18/08 ha
|
||||
@@ -249,6 +251,7 @@ def main() -> int:
|
||||
**{f"cambio {i+1}": c for i, c in enumerate(r["changes"])},
|
||||
})
|
||||
save_state(r["current"])
|
||||
registra_giro(RUNS, level=r["level"])
|
||||
return 2 if r["level"] == "AZIONE" else 0
|
||||
|
||||
|
||||
|
||||
@@ -13,11 +13,15 @@ from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.live.giri import registra_giro # noqa: E402
|
||||
from src.live.notifier import notify # noqa: E402
|
||||
from src.live.scale_watch import ALLARME, OK, run_once # noqa: E402
|
||||
|
||||
RUNS = ROOT / "data" / "scale_watch" / "runs.jsonl"
|
||||
|
||||
|
||||
def _sender(testo: str):
|
||||
try:
|
||||
@@ -31,6 +35,7 @@ def main() -> None:
|
||||
secco = "--secco" in sys.argv
|
||||
quiet = "--quiet" in sys.argv
|
||||
rep = run_once(sender=None if secco else _sender)
|
||||
registra_giro(RUNS, stato=rep["stato"])
|
||||
icona = {OK: "✅", ALLARME: "🚨"}.get(rep["stato"], "⚠️")
|
||||
if quiet:
|
||||
fb = rep["fallback"]
|
||||
|
||||
@@ -40,10 +40,12 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.live.giri import registra_giro # noqa: E402
|
||||
from src.live.notifier import notify # noqa: E402
|
||||
|
||||
FEED = "https://insights.deribit.com/exchange-updates/feed/"
|
||||
STATE = ROOT / "data" / "live" / "venue_news.jsonl"
|
||||
RUNS = ROOT / "data" / "venue_news" / "runs.jsonl"
|
||||
|
||||
# Parole che alzano un annuncio a 🚨. Le prime sono FISSE (categorie di rischio che ci toccano
|
||||
# per costruzione); le altre si DERIVANO dal codice, sotto.
|
||||
@@ -140,6 +142,7 @@ def main() -> int:
|
||||
v["seed"] = primo_giro
|
||||
f.write(json.dumps(v) + "\n")
|
||||
nuove.append(v)
|
||||
registra_giro(RUNS, voci=len(tutte), nuove=len(nuove))
|
||||
|
||||
# Primo giro: si semina lo storico SENZA allertare (P9: l'allarme massimo non si spende
|
||||
# per un arretrato di 10 voci, o non verra' letto il giorno che e' vero).
|
||||
|
||||
@@ -16,10 +16,13 @@ ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.live.deribit import check_specs # noqa: E402
|
||||
from src.live.giri import registra_giro # noqa: E402
|
||||
from src.live.notifier import notify, ultimo_errore # noqa: E402
|
||||
from src.live.venue_watch import (PERSIST_HOURS, THRESHOLD_BPS, # noqa: E402
|
||||
run_once)
|
||||
|
||||
RUNS = ROOT / "data" / "venue_watch" / "runs.jsonl"
|
||||
|
||||
|
||||
def _titolo(rep: dict) -> str:
|
||||
"""⚠️ Il titolo dice cosa FARE, non solo cosa e' successo: chi lo legge sul telefono deve
|
||||
@@ -70,6 +73,7 @@ def main() -> int:
|
||||
# che manda ordini. Il 18/08 Deribit ha cambiato tick e size dei perpetual lineari USDC e
|
||||
# niente se n'era accorto: e' andata bene perche' i cambi erano riduzioni.
|
||||
spec = check_specs()
|
||||
registra_giro(RUNS, livelli=rep["levels"])
|
||||
if spec["divergenze"]:
|
||||
rifiuto = [d for d in spec["divergenze"] if d["rischio"] == "rifiuto"]
|
||||
righe = {f"{d['strumento']} {d['campo']}": f"dichiarato {d['dichiarato']} / "
|
||||
|
||||
@@ -51,9 +51,11 @@ sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research"))
|
||||
from cblib import ( # noqa: E402
|
||||
dvol_series, f_factors, load_chain, pick_legs, puts, spot_series,
|
||||
)
|
||||
from src.live.giri import registra_giro # noqa: E402
|
||||
from src.live.notifier import notify # noqa: E402
|
||||
|
||||
STATE = PROJECT_ROOT / "data" / "vrp_f_watch" / "state.json"
|
||||
RUNS = PROJECT_ROOT / "data" / "vrp_f_watch" / "runs.jsonl"
|
||||
|
||||
# --- config CONGELATA il 2026-07-30 ---------------------------------------
|
||||
STRUCTS = {
|
||||
@@ -190,6 +192,8 @@ def main() -> int:
|
||||
a["notified"] = bool(prev.get("notified", False))
|
||||
|
||||
_save_state(a)
|
||||
if not R.empty:
|
||||
registra_giro(RUNS, pairs=a["pairs"], ready=a["ready"])
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -58,9 +58,13 @@ REGOLE VINCOLANTI:
|
||||
5. Se noti qualcosa che merita un controllo umano, dillo in una frase, senza drammatizzare.
|
||||
6. Massimo %d parole. Prosa, niente elenchi puntati, niente titoli.
|
||||
|
||||
Il libro e' TP01 (trend long-flat, 75%%) + SKH01 (breakout, 25%%) su Deribit, capitale ~$635.
|
||||
Il libro e' TP01 (trend long-flat, 75%%) + SKH01 (breakout, 25%%) su Deribit; il capitale e'
|
||||
l'equity scritta nella pagina qui sotto, non un valore da ricordare.
|
||||
Il progetto e' esplicitamente scettico: il suo campione e' corto e il P&L non distingue ancora
|
||||
l'edge dalla fortuna. Scrivere con quella consapevolezza e' parte del compito.
|
||||
Un round-trip e' un lotto chiuso in FIFO (tradesdb.fifo_roundtrips): una vendita puo' chiudere
|
||||
piu' lotti in una volta, quindi vedere piu' round-trip che fill nello stesso giorno e' atteso,
|
||||
non un'anomalia da segnalare.
|
||||
""" % MAX_PAROLE
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""registra_giro — il giro che un sorvegliante ha misurato, come riga jsonl (2026-09-21, #16).
|
||||
|
||||
PERCHE' ESISTE. `cc01_regime_watch` e' morto 14 notti di fila (#10) senza che nessuno lo vedesse,
|
||||
perche' non scriveva niente da sorvegliare: `monitor_health` guarda solo le serie che i monitor
|
||||
scrivono, e un monitor silenzioso non e' distinguibile da uno che dice "tutto ok". La riparazione
|
||||
di cc01 (`data/cc01_regime/runs.jsonl`, commit 31f5915) e' il modello: qui si generalizza a ogni
|
||||
sorvegliante, cosi' un monitor fermo diventa FERMO in `monitor_health`, non invisibile.
|
||||
|
||||
Un registro che fallisce non deve cambiare l'esito del sorvegliante che lo scrive: `registra_giro`
|
||||
non solleva mai, e su errore lo dice su stderr e ritorna False.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def registra_giro(path: Path, **campi) -> bool:
|
||||
"""Appende `{"ts": <ms epoch>, **campi}` a `path` (jsonl, una riga). Crea la cartella se manca.
|
||||
|
||||
Ritorna True se la riga e' stata scritta, False su OSError (disco pieno, permessi, percorso
|
||||
invalido...) dopo aver scritto il motivo su stderr. Non solleva mai: la misura del
|
||||
sorvegliante e' gia' fatta, e un registro che fallisce non deve buttarla via.
|
||||
"""
|
||||
riga = {"ts": int(time.time() * 1000), **campi}
|
||||
try:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("a", encoding="utf-8") as fh:
|
||||
fh.write(json.dumps(riga, default=str) + "\n")
|
||||
return True
|
||||
except OSError as e:
|
||||
print(f"registra_giro: non scrivo {path}: {type(e).__name__}: {e}", file=sys.stderr)
|
||||
return False
|
||||
@@ -105,6 +105,29 @@ MONITORS: tuple[MonitorSpec, ...] = (
|
||||
MonitorSpec("cc01_regime_watch", "cc01_regime", "runs.jsonl", 24.0, max_age_h=30.0,
|
||||
gate="trigger di regime CC01: WARN>=10% / ALERT>=15% funding 30g (riapre il dossier)",
|
||||
open_labeled=False),
|
||||
# I sei sorveglianti che generalizzano la riparazione di cc01 (#16): ognuno registra il
|
||||
# proprio giro in `data/<dir>/runs.jsonl` (src.live.giri.registra_giro), timbri di
|
||||
# ESECUZIONE come cc01 -> open_labeled=False. cron_daily.sh (GIORNALIERO) per i primi
|
||||
# cinque: max_age 30h = un giro mancato + margine.
|
||||
MonitorSpec("edge_watch", "edge_watch", "runs.jsonl", 24.0, max_age_h=30.0,
|
||||
gate="kill del libro live (Sharpe 36m < -0,5 / protezione TP01)",
|
||||
open_labeled=False),
|
||||
MonitorSpec("fee_watch", "fee_watch", "runs.jsonl", 24.0, max_age_h=30.0,
|
||||
gate="regola fee §3 decisa in anticipo",
|
||||
open_labeled=False),
|
||||
MonitorSpec("scale_watch", "scale_watch", "runs.jsonl", 24.0, max_age_h=30.0,
|
||||
gate="GATE SCALA-01 criterio A2",
|
||||
open_labeled=False),
|
||||
MonitorSpec("vrp_f_watch", "vrp_f_watch", "runs.jsonl", 24.0, max_age_h=30.0,
|
||||
gate="criteri (a)(b) §5.18",
|
||||
open_labeled=False),
|
||||
MonitorSpec("venue_news", "venue_news", "runs.jsonl", 24.0, max_age_h=30.0,
|
||||
gate="annunci Deribit, debito 8",
|
||||
open_labeled=False),
|
||||
# cron_book.sh: gira OGNI ORA accanto al libro vivo. max_age 3h = due giri persi.
|
||||
MonitorSpec("venue_watch", "venue_watch", "runs.jsonl", 1.0, max_age_h=3.0,
|
||||
gate="tripwire di venue orario",
|
||||
open_labeled=False),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ giorno che non risponde.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import date
|
||||
@@ -75,6 +76,18 @@ def test_il_prompt_non_contiene_l_analisi_vecchia():
|
||||
assert "Testo vecchio" not in p and "storico" in p
|
||||
|
||||
|
||||
# ------------------------------------------------- le istruzioni non portano un capitale fisso
|
||||
|
||||
def test_istruzioni_non_hanno_un_importo_in_dollari():
|
||||
"""Il capitale e' l'equity in pagina, non una cifra da ricordare (#13)."""
|
||||
assert re.search(r"\$\s?\d", A.ISTRUZIONI) is None
|
||||
|
||||
|
||||
def test_istruzioni_nominano_fifo_e_lotto():
|
||||
"""Piu' round-trip che fill nello stesso giorno e' atteso, non un'anomalia (#13)."""
|
||||
assert "FIFO" in A.ISTRUZIONI and "lotto" in A.ISTRUZIONI
|
||||
|
||||
|
||||
# ------------------------------------------------- i numeri inventati
|
||||
|
||||
def test_scopre_un_numero_che_non_c_e():
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""registra_giro (src/live/giri.py, #16): il registro che i sorveglianti scrivono a ogni giro.
|
||||
|
||||
Il contratto e' quello di cc01_regime_watch, generalizzato: appende `{"ts": <ms>, **campi}` come
|
||||
riga jsonl e non solleva MAI — un registro che fallisce non deve buttare via una misura gia' fatta.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.live.giri import registra_giro # noqa: E402
|
||||
|
||||
|
||||
def test_scrive_una_riga_con_ts_in_millisecondi(tmp_path):
|
||||
p = tmp_path / "sub" / "runs.jsonl"
|
||||
prima = time.time() * 1000
|
||||
assert registra_giro(p, stato="OK", n=3) is True
|
||||
riga = json.loads(p.read_text().strip())
|
||||
assert abs(riga["ts"] - prima) < 5_000
|
||||
assert isinstance(riga["ts"], int)
|
||||
assert riga["stato"] == "OK" and riga["n"] == 3
|
||||
|
||||
|
||||
def test_crea_la_cartella_se_manca(tmp_path):
|
||||
p = tmp_path / "non-esiste-ancora" / "runs.jsonl"
|
||||
assert not p.parent.exists()
|
||||
registra_giro(p, x=1)
|
||||
assert p.exists()
|
||||
|
||||
|
||||
def test_appende_senza_troncare(tmp_path):
|
||||
p = tmp_path / "runs.jsonl"
|
||||
registra_giro(p, giro=1)
|
||||
registra_giro(p, giro=2)
|
||||
righe = [json.loads(r) for r in p.read_text().splitlines()]
|
||||
assert [r["giro"] for r in righe] == [1, 2]
|
||||
|
||||
|
||||
def test_default_str_copre_i_valori_non_serializzabili(tmp_path):
|
||||
"""json.dumps(default=str): un valore che non e' nativamente JSON (es. un Path, o un
|
||||
dataclass senza __dict__ diretto) non deve far fallire l'append — diventa la sua str()."""
|
||||
p = tmp_path / "runs.jsonl"
|
||||
assert registra_giro(p, percorso=Path("/tmp/x")) is True
|
||||
riga = json.loads(p.read_text().strip())
|
||||
assert riga["percorso"] == str(Path("/tmp/x"))
|
||||
|
||||
|
||||
def test_oserror_ritorna_false_senza_sollevare(tmp_path, capsys):
|
||||
"""Un percorso impossibile (qui: dentro un FILE, non una cartella) e' un OSError alla
|
||||
mkdir/scrittura. Il chiamante non deve vedere un'eccezione: la misura e' gia' fatta."""
|
||||
ostacolo = tmp_path / "non-e-una-cartella"
|
||||
ostacolo.write_text("sono un file")
|
||||
p = ostacolo / "runs.jsonl"
|
||||
assert registra_giro(p, stato="OK") is False
|
||||
err = capsys.readouterr().err
|
||||
assert "registra_giro" in err and str(p) in err
|
||||
|
||||
|
||||
def test_un_valore_non_serializzabile_diventa_stringa_non_solleva(tmp_path):
|
||||
p = tmp_path / "runs.jsonl"
|
||||
oggetto = object()
|
||||
assert registra_giro(p, oggetto=oggetto) is True
|
||||
riga = json.loads(p.read_text().strip())
|
||||
assert riga["oggetto"] == str(oggetto)
|
||||
@@ -0,0 +1,270 @@
|
||||
"""registra_giro nei sei sorveglianti generalizzati da cc01_regime_watch (2026-09-21, #16).
|
||||
|
||||
PERCHE' ESISTE. `cc01_regime_watch` e' morto 14 notti di fila (#10) senza che nessuno lo vedesse,
|
||||
perche' non scriveva niente da sorvegliare (`monitor_health` guarda solo le serie che i monitor
|
||||
scrivono). La riparazione di cc01 (`data/cc01_regime/runs.jsonl`, commit 31f5915,
|
||||
tests/test_cc01_regime_watch.py) e' il modello: qui si verifica che sia stata applicata agli
|
||||
altri sei sorveglianti giornalieri/orari, con lo stesso incrocio P1 con `monitor_health.MONITORS`
|
||||
e lo stesso doppio ramo (misura -> una riga, non-misura -> nessuna riga).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "live"))
|
||||
|
||||
from src.live.monitor_health import MONITORS # noqa: E402
|
||||
|
||||
import edge_watch as EW # noqa: E402
|
||||
import fee_watch as FW # noqa: E402
|
||||
import scale_watch as SW # noqa: E402
|
||||
|
||||
|
||||
def _carica(nome: str):
|
||||
sp = importlib.util.spec_from_file_location(nome, ROOT / "scripts" / "live" / f"{nome}.py")
|
||||
m = importlib.util.module_from_spec(sp)
|
||||
sp.loader.exec_module(m)
|
||||
return m
|
||||
|
||||
|
||||
VF = _carica("vrp_f_watch")
|
||||
VN = _carica("venue_news")
|
||||
VW = _carica("venue_watch")
|
||||
|
||||
|
||||
def _righe(path: Path) -> list[dict]:
|
||||
import json
|
||||
if not path.exists():
|
||||
return []
|
||||
return [json.loads(r) for r in path.read_text().splitlines() if r.strip()]
|
||||
|
||||
|
||||
def _spec(nome: str):
|
||||
s = next((m for m in MONITORS if m.name == nome), None)
|
||||
assert s is not None, f"{nome} non e' in monitor_health.MONITORS"
|
||||
return s
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# P1 — il bersaglio della sorveglianza si deriva dal codice sorvegliato
|
||||
# ===========================================================================
|
||||
@pytest.mark.parametrize("nome,modulo,cadence_h,max_age_h", [
|
||||
("edge_watch", EW, 24.0, 30.0),
|
||||
("fee_watch", FW, 24.0, 30.0),
|
||||
("scale_watch", SW, 24.0, 30.0),
|
||||
("vrp_f_watch", VF, 24.0, 30.0),
|
||||
("venue_news", VN, 24.0, 30.0),
|
||||
("venue_watch", VW, 1.0, 3.0),
|
||||
])
|
||||
def test_registrato_in_monitor_health_sul_file_che_scrive(nome, modulo, cadence_h, max_age_h):
|
||||
s = _spec(nome)
|
||||
assert ROOT / "data" / s.directory / s.series == modulo.RUNS
|
||||
assert s.open_labeled is False
|
||||
assert s.cadence_h == cadence_h
|
||||
assert s.max_age_h <= max_age_h
|
||||
|
||||
|
||||
def test_venue_watch_ha_la_cadenza_oraria_del_cron_book():
|
||||
"""⚠️ venue_watch gira OGNI ORA in cron_book.sh: max_age stretto, non i 30h dei giornalieri."""
|
||||
assert _spec("venue_watch").cadence_h == 1.0 and _spec("venue_watch").max_age_h == 3.0
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# edge_watch — dopo run(): sharpe, n_alert
|
||||
# ===========================================================================
|
||||
def test_edge_watch_un_giro_misurato_scrive_una_riga(tmp_path, monkeypatch):
|
||||
runs = tmp_path / "runs.jsonl"
|
||||
monkeypatch.setattr(EW, "RUNS", runs)
|
||||
monkeypatch.setattr(sys, "argv", ["edge_watch.py", "--quiet"])
|
||||
monkeypatch.setattr(EW, "run", lambda: dict(sharpe=0.7, protection=[], consec_fail=0, alerts=[]))
|
||||
assert EW.main() == 0
|
||||
righe = _righe(runs)
|
||||
assert len(righe) == 1
|
||||
assert righe[0]["sharpe"] == 0.7 and righe[0]["n_alert"] == 0
|
||||
assert isinstance(righe[0]["ts"], int)
|
||||
|
||||
|
||||
def test_edge_watch_run_che_fallisce_non_scrive(tmp_path, monkeypatch):
|
||||
runs = tmp_path / "runs.jsonl"
|
||||
monkeypatch.setattr(EW, "RUNS", runs)
|
||||
monkeypatch.setattr(sys, "argv", ["edge_watch.py", "--quiet"])
|
||||
|
||||
def boom():
|
||||
raise RuntimeError("dati di mercato non leggibili")
|
||||
monkeypatch.setattr(EW, "run", boom)
|
||||
with pytest.raises(RuntimeError):
|
||||
EW.main()
|
||||
assert not runs.exists()
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# fee_watch — dopo save_state(): level
|
||||
# ===========================================================================
|
||||
def test_fee_watch_un_giro_misurato_scrive_una_riga(tmp_path, monkeypatch):
|
||||
runs = tmp_path / "runs.jsonl"
|
||||
monkeypatch.setattr(FW, "RUNS", runs)
|
||||
monkeypatch.setattr(sys, "argv", ["fee_watch.py", "--quiet"])
|
||||
monkeypatch.setattr(FW, "run", lambda: dict(current={}, changes=[], worst_taker_bps=0.0,
|
||||
level="OK", reason="tutto in regola", first_read=True))
|
||||
monkeypatch.setattr(FW, "save_state", lambda cur: None)
|
||||
assert FW.main() == 0
|
||||
righe = _righe(runs)
|
||||
assert len(righe) == 1 and righe[0]["level"] == "OK"
|
||||
|
||||
|
||||
def test_fee_watch_run_che_fallisce_non_scrive(tmp_path, monkeypatch):
|
||||
runs = tmp_path / "runs.jsonl"
|
||||
monkeypatch.setattr(FW, "RUNS", runs)
|
||||
monkeypatch.setattr(sys, "argv", ["fee_watch.py", "--quiet"])
|
||||
|
||||
def boom():
|
||||
raise RuntimeError("API Deribit irraggiungibile")
|
||||
monkeypatch.setattr(FW, "run", boom)
|
||||
with pytest.raises(RuntimeError):
|
||||
FW.main()
|
||||
assert not runs.exists()
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# scale_watch — dopo run_once(): stato
|
||||
# ===========================================================================
|
||||
_REP_SCALA = dict(stato="OK", scala_dichiarata=1.0, ladder="0.5/1/2", tetto=2.0,
|
||||
domande={}, fallback={"stato": "OK", "quota": 0.0, "fallback_veri": 0,
|
||||
"giri": 10, "soglia": 0.1, "paper": 0},
|
||||
lettore={"stato": "OK", "motivo": "ok"}, invio="non tentato")
|
||||
|
||||
|
||||
def test_scale_watch_un_giro_misurato_scrive_una_riga(tmp_path, monkeypatch):
|
||||
runs = tmp_path / "runs.jsonl"
|
||||
monkeypatch.setattr(SW, "RUNS", runs)
|
||||
monkeypatch.setattr(sys, "argv", ["scale_watch.py", "--quiet", "--secco"])
|
||||
monkeypatch.setattr(SW, "run_once", lambda sender=None: dict(_REP_SCALA))
|
||||
SW.main()
|
||||
righe = _righe(runs)
|
||||
assert len(righe) == 1 and righe[0]["stato"] == "OK"
|
||||
|
||||
|
||||
def test_scale_watch_run_once_che_fallisce_non_scrive(tmp_path, monkeypatch):
|
||||
runs = tmp_path / "runs.jsonl"
|
||||
monkeypatch.setattr(SW, "RUNS", runs)
|
||||
monkeypatch.setattr(sys, "argv", ["scale_watch.py", "--quiet", "--secco"])
|
||||
|
||||
def boom(sender=None):
|
||||
raise RuntimeError("lettore di produzione irraggiungibile")
|
||||
monkeypatch.setattr(SW, "run_once", boom)
|
||||
with pytest.raises(RuntimeError):
|
||||
SW.main()
|
||||
assert not runs.exists()
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# vrp_f_watch — dopo _save_state(), SOLO se R non e' vuoto: pairs, ready
|
||||
# ===========================================================================
|
||||
def _frame_coppie(n: int = 45) -> pd.DataFrame:
|
||||
rows = []
|
||||
for i in range(n):
|
||||
exp = pd.Timestamp("2026-01-01") + pd.Timedelta(days=i)
|
||||
for name, v in (("canonico", 0.70), ("candidato", 0.80)):
|
||||
rows.append(dict(asset="BTC", struct=name, exp=exp, ts=pd.Timestamp("2026-01-01"),
|
||||
dte=7.0, f_short=1.0, f_long=2.0, f_net=v, f_net_mid=v,
|
||||
quota_lunga=0.18))
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def test_vrp_f_watch_un_giro_misurato_scrive_una_riga(tmp_path, monkeypatch):
|
||||
runs = tmp_path / "runs.jsonl"
|
||||
monkeypatch.setattr(VF, "RUNS", runs)
|
||||
monkeypatch.setattr(sys, "argv", ["vrp_f_watch.py", "--quiet"])
|
||||
frame = _frame_coppie()
|
||||
monkeypatch.setattr(VF, "measure", lambda chain=None: frame)
|
||||
monkeypatch.setattr(VF, "_save_state", lambda d: None)
|
||||
monkeypatch.setattr(VF, "_load_state", lambda: {})
|
||||
monkeypatch.setattr(VF, "notify", lambda *a, **k: True)
|
||||
assert VF.main() == 0
|
||||
righe = _righe(runs)
|
||||
atteso = VF.assess(frame)
|
||||
assert len(righe) == 1
|
||||
assert righe[0]["pairs"] == atteso["pairs"] and righe[0]["ready"] == atteso["ready"]
|
||||
|
||||
|
||||
def test_vrp_f_watch_senza_osservazioni_non_scrive(tmp_path, monkeypatch):
|
||||
"""P5: R vuoto non e' un giro misurato — nessuna riga (a differenza di cc01, qui non
|
||||
c'e' nemmeno un `return` anticipato: la guardia sta nella condizione prima di registrare)."""
|
||||
runs = tmp_path / "runs.jsonl"
|
||||
monkeypatch.setattr(VF, "RUNS", runs)
|
||||
monkeypatch.setattr(sys, "argv", ["vrp_f_watch.py", "--quiet"])
|
||||
monkeypatch.setattr(VF, "measure", lambda chain=None: pd.DataFrame())
|
||||
monkeypatch.setattr(VF, "_save_state", lambda d: None)
|
||||
monkeypatch.setattr(VF, "_load_state", lambda: {})
|
||||
assert VF.main() == 0
|
||||
assert not runs.exists()
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# venue_news — dopo la scrittura delle voci: voci, nuove. Feed illeggibile -> nessuna riga
|
||||
# ===========================================================================
|
||||
_FEED = """<rss><channel>
|
||||
<item><title>Deribit Welcomes Orbit Markets</title>
|
||||
<link>https://x/a</link><guid>https://x/?p=1</guid><pubDate>Fri, 10 Jul 2026 00:00:00 +0000</pubDate></item>
|
||||
<item><title>Another Unrelated Announcement</title>
|
||||
<link>https://x/b</link><guid>https://x/?p=2</guid><pubDate>Fri, 11 Jul 2026 00:00:00 +0000</pubDate></item>
|
||||
</channel></rss>"""
|
||||
|
||||
|
||||
def test_venue_news_un_giro_misurato_scrive_una_riga(tmp_path, monkeypatch):
|
||||
runs = tmp_path / "runs.jsonl"
|
||||
state = tmp_path / "venue_news.jsonl"
|
||||
monkeypatch.setattr(VN, "RUNS", runs)
|
||||
monkeypatch.setattr(VN, "STATE", state)
|
||||
monkeypatch.setattr(VN, "scarica", lambda *a, **k: _FEED)
|
||||
monkeypatch.setattr(VN, "notify", lambda *a, **k: True)
|
||||
monkeypatch.setattr(sys, "argv", ["venue_news.py", "--quiet"])
|
||||
assert VN.main() == 0
|
||||
righe = _righe(runs)
|
||||
assert len(righe) == 1 and righe[0]["voci"] == 2 and righe[0]["nuove"] == 2
|
||||
|
||||
|
||||
def test_venue_news_feed_illeggibile_non_scrive(tmp_path, monkeypatch):
|
||||
runs = tmp_path / "runs.jsonl"
|
||||
monkeypatch.setattr(VN, "RUNS", runs)
|
||||
monkeypatch.setattr(VN, "STATE", tmp_path / "venue_news.jsonl")
|
||||
monkeypatch.setattr(VN, "scarica", lambda *a, **k: None)
|
||||
monkeypatch.setattr(VN, "notify", lambda *a, **k: True)
|
||||
monkeypatch.setattr(sys, "argv", ["venue_news.py", "--quiet"])
|
||||
assert VN.main() == 2
|
||||
assert not runs.exists()
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# venue_watch — dopo run_once() e check_specs(): livelli (rep["levels"])
|
||||
# ===========================================================================
|
||||
def test_venue_watch_un_giro_misurato_scrive_una_riga(tmp_path, monkeypatch):
|
||||
runs = tmp_path / "runs.jsonl"
|
||||
monkeypatch.setattr(VW, "RUNS", runs)
|
||||
monkeypatch.setattr(sys, "argv", ["venue_watch.py", "--quiet"])
|
||||
monkeypatch.setattr(VW, "run_once", lambda sender=None: dict(
|
||||
levels={"BTC": "OK", "ETH": "OK"}, alerts=[], platform_locked=False))
|
||||
monkeypatch.setattr(VW, "check_specs", lambda: dict(divergenze=[]))
|
||||
assert VW.main() == 0
|
||||
righe = _righe(runs)
|
||||
assert len(righe) == 1 and righe[0]["livelli"] == {"BTC": "OK", "ETH": "OK"}
|
||||
|
||||
|
||||
def test_venue_watch_run_once_che_fallisce_non_scrive(tmp_path, monkeypatch):
|
||||
runs = tmp_path / "runs.jsonl"
|
||||
monkeypatch.setattr(VW, "RUNS", runs)
|
||||
monkeypatch.setattr(sys, "argv", ["venue_watch.py", "--quiet"])
|
||||
|
||||
def boom(sender=None):
|
||||
raise RuntimeError("osservazione del consenso fallita")
|
||||
monkeypatch.setattr(VW, "run_once", boom)
|
||||
with pytest.raises(RuntimeError):
|
||||
VW.main()
|
||||
assert not runs.exists()
|
||||
Reference in New Issue
Block a user