Files
PythagorasGoal/scripts/live/telegram_daily.py
T
Adriano Dal Pastro 71b39c2c86 ops(live): staleness-gate bloccante + report Telegram giornaliero
Presa in carico operativa del conto Deribit (delega dell'utente). Nessun cambio a
strategie, pesi o sizing: il libro resta TP01 0.75 + SKH01 0.25.

1) STALENESS-GATE (protezione di capitale, era un follow-up mai cablato)
Il 2026-07-14 alle 14:00 UTC il book ha COMPRATO ETH $75 con l'ultima barra del
feed certificato ferma al 2026-07-08: feed congelato da 6 giorni (diario
2026-07-15-feed-freeze). I due gate esistenti non potevano vederlo — il conto ERA
online e la posizione ERA leggibile: proteggono dai problemi di CONTO, non da un
feed morto. Il diario raccomandava un alert; qui il gate e' BLOCCANTE, coerente
con gli altri due ("non opero a cieco"), col disaster-SL on-book come rete su
eventuali posizioni gia' aperte.
- config/live.json: max_data_age_days = 2;
- book_execute: _data_age_days() + blocco PRIMA di costruire DeribitTrader
  (nessuna sessione autenticata aperta su dati morti) + alert Telegram con il
  comando di sblocco; data illeggibile => trattata come stantia;
- letto con cfg.get(default): una config priva della chiave ricade sulla soglia
  sicura invece di sollevare KeyError dentro il percorso con soldi veri;
- tests/test_book_staleness_gate.py: 8 casi, incluso il funzionale che riproduce
  la situazione del 14/07 (online + posizione leggibile + ordine pronto) e
  verifica che DeribitTrader NON venga costruito.
- tests/test_book_live.py: i 3 report finti avevano last_data="2026-07-01"
  hardcoded -> ora data fresca calcolata. Quei test riguardano skh_error /
  pos_error / eq_fallback, non la staleness: con la data fissa sarebbero marciti
  al superamento della soglia.

2) REPORT TELEGRAM GIORNALIERO (scripts/live/telegram_daily.py, in cron_daily.sh)
Gli alert esistenti scattano solo su ordine o errore: con il libro flat — stato
normale e corretto col trend giu' — significava silenzio per settimane,
indistinguibile da un sistema morto. Il report dice ogni giorno dove sta il conto,
perche' non opera e quanto manca perche' operi (con la convenzione TP01 corretta:
media dei SEGNI, quindi servono 2 orizzonti su 3, non basta il piu' breve).
Sola lettura: un test verifica che il modulo non possa inviare ordini.

Stato al commit: equity $596.92, flat e a target, disaster-SL -30% verificato
(placed @ $1,308.7 sull'ultima posizione). TP01 0.00 su entrambi: accensione a
BTC $78.680 (+22,7%) / ETH $2.370 (+27,2%).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 10:05:47 +00:00

151 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""REPORT GIORNALIERO su Telegram — stato del conto reale, cosa sta facendo il sistema e perche'.
PERCHE' ESISTE (2026-07-25). Gli alert Telegram esistenti (`src/live/notifier`) scattano SOLO su
ordine eseguito o errore. Con il libro flat — che e' lo stato normale e corretto quando il trend e'
giu' — questo significa **silenzio per settimane**, indistinguibile da un sistema morto. Questo
report rompe il silenzio ogni giorno e dice tre cose: dove sta il conto, perche' il sistema non
opera, e quanto manca perche' operi.
NON invia ordini, non tocca posizioni, non legge segreti oltre a quelli gia' usati dall'esecutore.
E' sola lettura: se fallisce, l'esecuzione oraria del libro non ne risente in alcun modo.
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
"""
from __future__ import annotations
import json
import sys
from datetime import date, datetime, timezone
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live.notifier import send
HORIZONS = (30, 90, 180)
GATES = [("STATARB-RESID", date(2026, 9, 27), "data/paper_statarb/returns.jsonl", "net_modeled"),
("XSR01", date(2026, 10, 23), "data/paper_xsr/returns.jsonl", "net_modeled")]
def _fmt_usd(x: float) -> str:
return f"${x:,.2f}"
def trend_state() -> list[str]:
"""Stato TSMOM per asset + quanto manca all'accensione (2 orizzonti su 3 positivi).
NB convenzione TP01: la direzione e' la media dei SEGNI (-1/+1) sugli orizzonti, poi clippata
a >=0 (long-flat). Con 1 orizzonte su 3 positivo la media e' -0.33 -> target 0. Serve quindi
che DUE orizzonti siano positivi perche' il libro si accenda: non basta il piu' breve."""
from src.data.downloader import load_data
out = []
for a in ("BTC", "ETH"):
try:
d = load_data(a, "1h")
s = pd.Series(d["close"].astype(float).values,
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
c = s.resample("1D").last().dropna().values
if len(c) < max(HORIZONS) + 2:
continue
px = float(c[-1])
refs = {h: float(c[-1 - h]) for h in HORIZONS}
sg = {h: (1 if px > refs[h] else -1) for h in HORIZONS}
m = float(np.mean(list(sg.values())))
tgt = max(0.0, m)
line = f" {a} {px:,.0f} · segni " + "/".join(f"{sg[h]:+d}" for h in HORIZONS) + \
f" → TP01 {tgt:.2f}"
neg = [(h, refs[h]) for h in HORIZONS if sg[h] < 0]
if neg and tgt == 0.0:
h, r = min(neg, key=lambda t: t[1])
line += f"\n accensione a {r:,.0f} ({(r/px-1)*100:+.1f}%)"
out.append(line)
except Exception as e:
out.append(f" {a}: stato trend non calcolabile ({type(e).__name__})")
return out
def book_state() -> list[str]:
"""Conto reale, posizioni e target netto correnti. Sola lettura."""
try:
from src.live.book import book_report
r = book_report()
eq = r.get("equity")
lines = [f" equity {_fmt_usd(eq)}" if eq else " equity non leggibile"]
for a in r.get("assets", []):
pos, net = a.get("position_usd", 0.0), a.get("net_target", 0.0)
stato = "flat" if abs(pos) < 1 and abs(net) < 1 else f"pos {_fmt_usd(pos)} → target {_fmt_usd(net)}"
lines.append(f" {a['asset']}: {stato} (TP {a.get('tp_frac', 0):+.2f} · SKH {a.get('skh_sign', 0):+d})")
if r.get("skh_error"):
lines.append(f" ⚠️ SKH feed KO: {r['skh_error']}")
return lines
except Exception as e:
return [f" ⚠️ stato libro non leggibile: {type(e).__name__}: {e}"]
def monitors() -> list[str]:
out = []
for name, dec, path, key in GATES:
p = ROOT / path
giorni = (dec - date.today()).days
if not p.exists():
out.append(f" {name}: in attesa della prima barra · gate {dec} ({giorni}g)")
continue
try:
rows = [json.loads(x) for x in p.read_text().splitlines() if x.strip()]
if not rows:
out.append(f" {name}: 0 barre · gate {dec} ({giorni}g)")
continue
r = np.array([x[key] for x in rows], float)
sh = float(r.mean() / r.std() * np.sqrt(365)) if r.std() > 0 else 0.0
tot = float(np.prod(1 + r) - 1) * 100
out.append(f" {name}: {len(rows)}g · {tot:+.2f}% · Sh {sh:+.2f} · gate {dec} ({giorni}g)")
except Exception:
out.append(f" {name}: log illeggibile · gate {dec} ({giorni}g)")
return out
def last_trade() -> str:
p = ROOT / "data" / "live" / "book_executions.jsonl"
if not p.exists():
return " nessun ordine registrato"
try:
rows = [json.loads(x) for x in p.read_text().splitlines() if x.strip()]
if not rows:
return " nessun ordine registrato"
t = rows[-1]
d = datetime.fromisoformat(t["ts_utc"]).replace(tzinfo=timezone.utc)
giorni = (datetime.now(timezone.utc) - d).days
return (f" ultimo: {t['ts_utc'][:10]} ({giorni}g fa) {t['asset']} {t['action']} "
f"@ {t.get('price')} · totale ordini: {len(rows)}")
except Exception:
return " ledger ordini illeggibile"
def build() -> str:
oggi = datetime.now(timezone.utc).strftime("%d/%m %H:%M UTC")
L = [f"📊 <b>Book Deribit — {oggi}</b>", "", "<b>Conto</b>"]
L += book_state()
L += ["", "<b>Perche' non opera</b>"]
L += trend_state()
L += ["", "<b>Ordini</b>", last_trade()]
L += ["", "<b>Monitor in osservazione</b>"]
L += monitors()
return "\n".join(L)
def main() -> None:
txt = build()
if "--dry-run" in sys.argv[1:]:
print(txt.replace("<b>", "").replace("</b>", ""))
return
print("inviato" if send(txt) else "NON inviato (config Telegram assente o rete KO)")
if __name__ == "__main__":
main()