Files
PythagorasGoal/scripts/live/telegram_daily.py
T
Adriano Dal Pastro de83909db9 un flag sconosciuto non e' l'azione di default: guardia su 18 script, indice USDE orario, pulizia
MISURATO oggi durante la revisione: `trades_db.py --help` non stampava l'uso, cadeva in sync()
e riscriveva meta.ultimo_sync. Nessuno dei 18 script di scripts/live/ usava argparse: un flag
sbagliato era il ramo else. Su journal.py avrebbe scritto pagina e riga di DB, su analista.py
avrebbe speso una chiamata al modello e mandato un Telegram.

- src/live/cli.valida: prima istruzione di ogni __main__, prima di connect()/sync/rete.
  --help -> 0 con l'uso; flag ignoto, valore mancante o posizionale -> 2 con l'elenco dei
  previsti (P4). NIENTE argparse: cambierebbe messaggi, codici d'uscita e --help di script
  che il cron gia' chiama.
- 18 script cablati (i 3 che scrivono + 14 + cc01), flag invariati.
- tests/test_cli_flag.py (30): elenco DERIVATO dalla cartella (P1), valida come prima
  istruzione, uso che documenta i flag, e i flag che il CRON usa davvero restano accettati
  (P15/P16); end-to-end su --help e flag ignoto con trades.db non toccato (M15).
  Verificato a mano: monitor_health --quiet, trades_db --sync --quiet, book_execute dry-run.

Debito §5.15, primo passo: balance_watch (orario) registra `usde_usdc` a ogni campione — None
con la ragione se illeggibile, mai 1,0. Il cablaggio nel bound quando la serie ha storia.

Pulizia dalla revisione: tests/helpers.carica_script al posto della 15a copia del loader
importlib (5 file del libro live); il fill di prova via upsert_fills invece di un INSERT che
lasciava verified NULL; asserzioni non ancorate al padding; movimenti_capitale accetta le
righe gia' lette (una SELECT invece di due ai due lati di una scrittura del cron).

Test 910 verdi (+52). Diario 2026-09-02c.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDJsH3iDSaBns3ccpPBwiu
2026-09-02 15:49:40 +00:00

160 lines
6.4 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)")
USO = """uso: telegram_daily.py [--dry-run]
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
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("telegram_daily.py", USO, flag=("--dry-run",), con_valore=())
main()