31f5915a41
- 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>
128 lines
5.1 KiB
Python
128 lines
5.1 KiB
Python
"""cc01_regime_watch — trigger di regime per riaprire il dossier CASH-AND-CARRY (2026-07-24).
|
|
|
|
CONTESTO. CC01 (basis/funding carry) e' un LEAD archiviato: premio REALE ma procyclico
|
|
(diario 2026-06-26-cash-carry-hl.md), non uno sleeve. La ricerca 2026-07-24 (goal50, R1)
|
|
ha confermato il ciclo live: funding BTC +70% ann. (gen 2026) -> -3.4% (mag) -> ~0-4% (lug).
|
|
Regola decisa: il dossier si riapre SOLO quando il funding medio 1-MESE e' sostenutamente
|
|
ricco — non sugli spike (gen-2026 style, spariti in settimane).
|
|
|
|
Questo watcher legge gli ultimi ~35g di funding orario BTC/ETH dall'API pubblica
|
|
Hyperliquid (tokenless, stessa fonte certificata di fetch_hl_funding.py) e stampa:
|
|
- media 30g e 7g annualizzate per BTC/ETH
|
|
- stato trigger: QUIET (<10% ann.) | WARN (>=10%) | ALERT (>=15% -> riaprire CC01,
|
|
che resta comunque vincolato a ~$20k+ di capitale e venue con funding eseguibile).
|
|
|
|
Uso: `uv run python scripts/live/cc01_regime_watch.py`
|
|
(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
|
|
|
|
import numpy as np
|
|
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
|
|
ALERT_ANN = 0.15 # >=15% ann. su media 30g -> ALERT: riaprire il dossier CC01
|
|
|
|
|
|
def _post(payload: dict, max_retry: int = 6):
|
|
delay = 1.0
|
|
for _ in range(max_retry):
|
|
r = requests.post(HL_INFO, json=payload, timeout=30)
|
|
if r.status_code == 429 or r.status_code >= 500:
|
|
time.sleep(delay)
|
|
delay = min(delay * 2, 20)
|
|
continue
|
|
r.raise_for_status()
|
|
return r.json()
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
def fetch_recent_funding(coin: str, days: int = 35) -> np.ndarray:
|
|
"""Funding orario degli ultimi `days` giorni (paginato, max 500/req)."""
|
|
start = int((dt.datetime.now(dt.timezone.utc)
|
|
- dt.timedelta(days=days)).timestamp() * 1000)
|
|
rows, seen = [], set()
|
|
while True:
|
|
d = _post({"type": "fundingHistory", "coin": coin, "startTime": start})
|
|
if not d:
|
|
break
|
|
new = [x for x in d if x["time"] not in seen]
|
|
if not new:
|
|
break
|
|
for x in new:
|
|
seen.add(x["time"])
|
|
rows.append((x["time"], float(x["fundingRate"])))
|
|
start = new[-1]["time"] + 1
|
|
if len(d) < 500:
|
|
break
|
|
rows.sort()
|
|
return np.array([r for _, r in rows], dtype=float)
|
|
|
|
|
|
def main() -> None:
|
|
print("=" * 78)
|
|
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:
|
|
print(f" {coin}: dati insufficienti ({len(f)} ore) — check API")
|
|
continue
|
|
ann30 = float(f[-24 * 30:].mean()) * HOURS_PER_YEAR
|
|
ann7 = float(f[-24 * 7:].mean()) * HOURS_PER_YEAR
|
|
state = ("ALERT" if ann30 >= ALERT_ANN else
|
|
"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+,")
|
|
print(" venue con funding eseguibile, tail risk NON nel dataset).")
|
|
elif worst == "WARN":
|
|
print(" >> WARN: funding 30g >= 10% ann. — osservare, non agire.")
|
|
else:
|
|
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__":
|
|
from src.live.cli import valida
|
|
valida("cc01_regime_watch.py", USO)
|
|
main()
|