ombra di TradingBook documentata al §1; cc01_regime_watch registra i giri e sta in monitor_health (Fixes #11, Fixes #12)

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-09-16 21:10:12 +00:00
parent e42ab5369f
commit 31f5915a41
5 changed files with 140 additions and 1 deletions
+2
View File
@@ -79,6 +79,8 @@ data/fee_watch/
# battuta di cuore del collettore catena (stato runtime, come gli altri monitor) # battuta di cuore del collettore catena (stato runtime, come gli altri monitor)
data/chain_collect/ data/chain_collect/
data/vrp_f_watch/ 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) # log esecuzioni del book live (stato runtime, contiene fill/fee del conto reale)
data/live/ data/live/
+23
View File
@@ -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 annunciati), che il `:07` beccava — 4 volte in 63 giorni. **GTAA01, XS01, VRP01, XSR01 NON sono
nel book live.** 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`: **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 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. 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 · monitor_health · collect_chain · paper_* · trades_db ·
journal · analista journal · analista
scripts/cron_{book,daily,chain,opt_snapshot,vol_term,review}.sh 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/memory/ → LA MEMORIA (indice in testa a questo file)
docs/research/ → BRIEF-0822 · RESULTS-0822 (§1-77) · SPEC-scale-key docs/research/ → BRIEF-0822 · RESULTS-0822 (§1-77) · SPEC-scale-key
docs/diary/YYYY-MM-DD.md → una voce per esperimento (146) docs/diary/YYYY-MM-DD.md → una voce per esperimento (146)
+18 -1
View File
@@ -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). che resta comunque vincolato a ~$20k+ di capitale e venue con funding eseguibile).
Uso: `uv run python scripts/live/cc01_regime_watch.py` 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 from __future__ import annotations
import datetime as dt import datetime as dt
import json
import sys import sys
import time import time
from pathlib import Path from pathlib import Path
@@ -28,6 +35,7 @@ import requests
ROOT = Path(__file__).resolve().parents[2] ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
RUNS = ROOT / "data" / "cc01_regime" / "runs.jsonl"
HL_INFO = "https://api.hyperliquid.xyz/info" HL_INFO = "https://api.hyperliquid.xyz/info"
HOURS_PER_YEAR = 24 * 365 HOURS_PER_YEAR = 24 * 365
WARN_ANN = 0.10 # >=10% ann. su media 30g -> WARN 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(" CC01 REGIME WATCH — funding medio 1-mese (Hyperliquid, orario, tokenless)")
print("=" * 78) print("=" * 78)
worst = "QUIET" worst = "QUIET"
misure = {}
for coin in ("BTC", "ETH"): for coin in ("BTC", "ETH"):
f = fetch_recent_funding(coin) f = fetch_recent_funding(coin)
if len(f) < 24 * 7: if len(f) < 24 * 7:
@@ -86,8 +95,16 @@ def main() -> None:
"WARN" if ann30 >= WARN_ANN else "QUIET") "WARN" if ann30 >= WARN_ANN else "QUIET")
if state == "ALERT" or (state == "WARN" and worst == "QUIET"): if state == "ALERT" or (state == "WARN" and worst == "QUIET"):
worst = state 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(f" {coin}: media 30g {ann30:+7.1%} ann. media 7g {ann7:+7.1%} ann. -> {state}")
print("-" * 78) 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": if worst == "ALERT":
print(" >>> ALERT CC01: funding 30g >= 15% ann. — regime ricco SOSTENUTO.") print(" >>> ALERT CC01: funding 30g >= 15% ann. — regime ricco SOSTENUTO.")
print(" Riaprire il dossier cash-and-carry (vincoli invariati: ~$20k+,") print(" Riaprire il dossier cash-and-carry (vincoli invariati: ~$20k+,")
+7
View File
@@ -98,6 +98,13 @@ MONITORS: tuple[MonitorSpec, ...] = (
MonitorSpec("cuscino_watch", "live", "cuscino_watch.jsonl", 1.0, max_age_h=3.0, MonitorSpec("cuscino_watch", "live", "cuscino_watch.jsonl", 1.0, max_age_h=3.0,
gate="cuscino USDC di regolamento: allerta + riconversione automatica", gate="cuscino USDC di regolamento: allerta + riconversione automatica",
open_labeled=False), 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),
) )
+90
View File
@@ -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"