From 31f5915a41f7e172682cc84eacce8771934c4694 Mon Sep 17 00:00:00 2001 From: "Adriano (goldrake)" Date: Wed, 16 Sep 2026 21:10:12 +0000 Subject: [PATCH] =?UTF-8?q?ombra=20di=20TradingBook=20documentata=20al=20?= =?UTF-8?q?=C2=A71;=20cc01=5Fregime=5Fwatch=20registra=20i=20giri=20e=20st?= =?UTF-8?q?a=20in=20monitor=5Fhealth=20(Fixes=20#11,=20Fixes=20#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLAUDE.md §1 OMBRA: cron :44 sulla stessa crontab, legge il conto con la STESSA chiave live di cerbero-mcp (verificato per hash), scrive solo nel suo albero (3 giri misurati, nessun file di data/ o logs/ di qui cambiato), non invia per assenza di codice e non per permesso. §12 rimanda. - cc01: ogni giro con entrambi gli asset misurati scrive data/cc01_regime/runs.jsonl (ts ms, stato, funding 30g/7g); se non vede, non scrive e lo dice. monitor_health: MonitorSpec giornaliera, max_age 30h, percorso incrociato con cc01.RUNS da un test (P1). - tests/test_cc01_regime_watch.py (4, rossi prima). Suite completa: 1074 passed. Autorizzato da Adriano il 16/09 dopo la revisione settimanale. Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 2 + CLAUDE.md | 23 ++++++++ scripts/live/cc01_regime_watch.py | 19 ++++++- src/live/monitor_health.py | 7 +++ tests/test_cc01_regime_watch.py | 90 +++++++++++++++++++++++++++++++ 5 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 tests/test_cc01_regime_watch.py diff --git a/.gitignore b/.gitignore index 40599b1..2a6d3e5 100644 --- a/.gitignore +++ b/.gitignore @@ -79,6 +79,8 @@ data/fee_watch/ # battuta di cuore del collettore catena (stato runtime, come gli altri monitor) data/chain_collect/ data/vrp_f_watch/ +# giri misurati del trigger di regime CC01 (letti da monitor_health, #12) +data/cc01_regime/ # log esecuzioni del book live (stato runtime, contiene fill/fee del conto reale) data/live/ diff --git a/CLAUDE.md b/CLAUDE.md index 5f01a28..09653ce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,6 +59,28 @@ dal 2026-08-25; era `:07`). Due vincoli, entrambi misurati: fuori dai ~26s del m annunciati), che il `:07` beccava — 4 volte in 63 giorni. **GTAA01, XS01, VRP01, XSR01 NON sono nel book live.** +**OMBRA (non e' questo repo, ma gira sullo stesso conto).** Dal 2026-09-13 la crontab ha anche +`44 * * * * /opt/AI-OS/automations/TradingBook/scripts/cron_ombra.sh`: il **book traslocato** +(`Adriano/TradingBook`, stesso codice di questo repo al 13/09) calcola ogni ora il target e lo +scrive in un registro, **senza inviare**. Finestra dichiarata fino al **30/09/2026**; confronto col +`:47` di qui con `TradingBook/scripts/confronta_giornata.py`. Verificato il 16/09 (issue #11): +- **cosa legge**: il conto Deribit tramite il gateway interno di TradingBook, con + `TradingBook/secrets/deribit.json`, il cui `client_id` **coincide** (confronto per hash) con + `DERIBIT_CLIENT_ID_LIVE` di cerbero-mcp, cioe' **la chiave con cui questo libro manda gli ordini**; + il feed tramite `TradingBook/data/raw`, un **collegamento** a `data/raw` di qui (sola lettura). +- **dove scrive**: solo nel suo albero, `logs/cron_ombra.log`, `data/live/ombra.jsonl` e + `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. +- **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 + posizione illeggibile danno un giro **saltato**, come qui danno lo stop. + **RICERCA (paper).** Portafoglio a 5 sleeve `src/portfolio/sleeves.active_sleeves`: TP01 33 / XS01 15 / VRP01 12 / SKH01 20 / GTAA01 20. **Non e' il book live** — quasi tutti i numeri di portafoglio del progetto sono su questa serie, non su quella che gira. @@ -675,6 +697,7 @@ scripts/live/ → book_execute · venue_watch · fee_watch · edg monitor_health · collect_chain · paper_* · trades_db · journal · analista scripts/cron_{book,daily,chain,opt_snapshot,vol_term,review}.sh + (la crontab ha anche cron_ombra.sh di TradingBook: vedi §1, OMBRA) docs/memory/ → LA MEMORIA (indice in testa a questo file) docs/research/ → BRIEF-0822 · RESULTS-0822 (§1-77) · SPEC-scale-key docs/diary/YYYY-MM-DD.md → una voce per esperimento (146) diff --git a/scripts/live/cc01_regime_watch.py b/scripts/live/cc01_regime_watch.py index 380c9aa..4e9e363 100644 --- a/scripts/live/cc01_regime_watch.py +++ b/scripts/live/cc01_regime_watch.py @@ -13,11 +13,18 @@ Hyperliquid (tokenless, stessa fonte certificata di fetch_hl_funding.py) e stamp che resta comunque vincolato a ~$20k+ di capitale e venue con funding eseguibile). Uso: `uv run python scripts/live/cc01_regime_watch.py` -(candidato a una riga in cron_daily.sh: e' read-only, nessun ordine, nessuno stato.) +(in cron_daily.sh: read-only verso il mercato, nessun ordine.) + +REGISTRA OGNI GIRO MISURATO (dal 2026-09-16, #12) in `data/cc01_regime/runs.jsonl`: `ts` in ms, +stato, funding 30g/7g per asset. Serve a `monitor_health`, che ne legge il percorso da `RUNS` +(P1). Dal 03/09 al 16/09 lo script e' morto 14 notti di fila (#10) e nessuno l'ha visto, perche' +non lasciava niente da sorvegliare. Un giro che non misura entrambi gli asset NON scrive: non e' +una barra, e se lo fosse un cc01 cieco resterebbe «OK». """ from __future__ import annotations import datetime as dt +import json import sys import time from pathlib import Path @@ -28,6 +35,7 @@ import requests ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) +RUNS = ROOT / "data" / "cc01_regime" / "runs.jsonl" HL_INFO = "https://api.hyperliquid.xyz/info" HOURS_PER_YEAR = 24 * 365 WARN_ANN = 0.10 # >=10% ann. su media 30g -> WARN @@ -75,6 +83,7 @@ def main() -> None: print(" CC01 REGIME WATCH — funding medio 1-mese (Hyperliquid, orario, tokenless)") print("=" * 78) worst = "QUIET" + misure = {} for coin in ("BTC", "ETH"): f = fetch_recent_funding(coin) if len(f) < 24 * 7: @@ -86,8 +95,16 @@ def main() -> None: "WARN" if ann30 >= WARN_ANN else "QUIET") if state == "ALERT" or (state == "WARN" and worst == "QUIET"): worst = state + misure[coin] = {"ann30": ann30, "ann7": ann7, "stato": state} print(f" {coin}: media 30g {ann30:+7.1%} ann. media 7g {ann7:+7.1%} ann. -> {state}") print("-" * 78) + if len(misure) < 2: + # P5: «non vedo» non e' «QUIET». Nessuna riga: monitor_health lo vedra' FERMO. + print(f" NON VEDO: misurati {len(misure)} asset su 2 — nessuna riga registrata in {RUNS.name}.") + return + RUNS.parent.mkdir(parents=True, exist_ok=True) + with RUNS.open("a", encoding="utf-8") as fh: + fh.write(json.dumps({"ts": int(time.time() * 1000), "stato": worst, **misure}) + "\n") if worst == "ALERT": print(" >>> ALERT CC01: funding 30g >= 15% ann. — regime ricco SOSTENUTO.") print(" Riaprire il dossier cash-and-carry (vincoli invariati: ~$20k+,") diff --git a/src/live/monitor_health.py b/src/live/monitor_health.py index 2497872..e2b0117 100644 --- a/src/live/monitor_health.py +++ b/src/live/monitor_health.py @@ -98,6 +98,13 @@ MONITORS: tuple[MonitorSpec, ...] = ( MonitorSpec("cuscino_watch", "live", "cuscino_watch.jsonl", 1.0, max_age_h=3.0, gate="cuscino USDC di regolamento: allerta + riconversione automatica", open_labeled=False), + # Trigger di regime CC01 (cron_daily 00:30, GIORNALIERO, #12): dal 03/09 al 16/09 e' morto 14 + # notti di fila senza che nessuno lo vedesse (#10), perche' non scriveva niente da sorvegliare. + # Il percorso e' quello di `cc01_regime_watch.RUNS` (un test li incrocia, P1). max_age 30h = + # un giro mancato + margine; timbri di esecuzione -> open_labeled=False. + 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), ) diff --git a/tests/test_cc01_regime_watch.py b/tests/test_cc01_regime_watch.py new file mode 100644 index 0000000..5784d99 --- /dev/null +++ b/tests/test_cc01_regime_watch.py @@ -0,0 +1,90 @@ +"""cc01_regime_watch registra ogni giro misurato, e monitor_health lo sorveglia (#12). + +PERCHE' ESISTE. Dal 03/09 al 16/09/2026 lo script e' morto 14 notti di fila con +ModuleNotFoundError (#10) e nessuno se n'e' accorto: `cron_daily.sh` va avanti dopo un errore, +e `monitor_health` guarda solo le serie che i monitor scrivono — cc01 non ne scriveva. Ora ogni +giro misurato lascia una riga, e un cc01 fermo diventa FERMO come ogni altro sorvegliante. + +Niente rete: il funding di Hyperliquid e' finto. +""" +from __future__ import annotations + +import importlib.util +import json +import sys +from datetime import datetime, timedelta, timezone +from pathlib import Path + +import numpy as np + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from src.live.monitor_health import MONITORS, check_all # noqa: E402 + + +def _carica(): + sp = importlib.util.spec_from_file_location("cc01_regime_watch", + ROOT / "scripts" / "live" / "cc01_regime_watch.py") + m = importlib.util.module_from_spec(sp) + sp.loader.exec_module(m) + return m + + +W = _carica() +SPEC = next((m for m in MONITORS if m.name == "cc01_regime_watch"), None) + + +def _funding_annuo(ann: float, ore: int = 24 * 35): + """Una serie oraria costante che annualizza a `ann`.""" + return lambda coin, days=35: np.full(ore, ann / W.HOURS_PER_YEAR) + + +def test_e_registrato_in_monitor_health_sul_file_che_scrive(): + # P1: il bersaglio della sorveglianza si deriva dal codice sorvegliato, non si ridichiara + assert SPEC is not None, "cc01_regime_watch non e' in monitor_health.MONITORS" + assert ROOT / "data" / SPEC.directory / SPEC.series == W.RUNS + assert SPEC.cadence_h == 24.0 and SPEC.open_labeled is False and SPEC.max_age_h <= 30.0 + + +def test_un_giro_misurato_scrive_una_riga(tmp_path, monkeypatch): + runs = tmp_path / "runs.jsonl" + monkeypatch.setattr(W, "RUNS", runs) + monkeypatch.setattr(W, "fetch_recent_funding", _funding_annuo(0.12)) + W.main() + righe = [json.loads(r) for r in runs.read_text().splitlines()] + assert len(righe) == 1 + r = righe[0] + ora_ms = datetime.now(timezone.utc).timestamp() * 1000 + assert abs(r["ts"] - ora_ms) < 60_000, "ts in millisecondi epoch, come legge monitor_health" + assert r["stato"] == "WARN" + assert abs(r["BTC"]["ann30"] - 0.12) < 1e-9 and abs(r["ETH"]["ann7"] - 0.12) < 1e-9 + + +def test_un_giro_senza_dati_non_scrive_e_non_dice_quiet(tmp_path, monkeypatch, capsys): + # un giro che non ha misurato non e' una barra: se scrivesse, un cc01 cieco resterebbe «OK» + runs = tmp_path / "runs.jsonl" + monkeypatch.setattr(W, "RUNS", runs) + monkeypatch.setattr(W, "fetch_recent_funding", _funding_annuo(0.12, ore=10)) + W.main() + assert not runs.exists() + out = capsys.readouterr().out + assert "regime QUIET" not in out and "nessuna riga registrata" in out + + +def test_monitor_health_legge_le_righe_di_cc01(tmp_path, monkeypatch): + # integrazione: le righe scritte da cc01 sono barre per monitor_health, e un cc01 fermo e' FERMO + runs = tmp_path / "data" / SPEC.directory / SPEC.series + runs.parent.mkdir(parents=True) + monkeypatch.setattr(W, "RUNS", runs) + monkeypatch.setattr(W, "fetch_recent_funding", _funding_annuo(0.05)) + W.main() + ieri = json.loads(runs.read_text()) + ieri["ts"] -= 24 * 3600 * 1000 + runs.write_text(json.dumps(ieri) + "\n" + runs.read_text()) + + adesso = datetime.now(timezone.utc) + stato = {r["name"]: r for r in check_all(tmp_path, now=adesso + timedelta(minutes=1), monitors=(SPEC,))} + assert stato["cc01_regime_watch"]["status"] == "OK" + fermo = check_all(tmp_path, now=adesso + timedelta(hours=31), monitors=(SPEC,))[0] + assert fermo["status"] == "FERMO"