cuscino_watch: riacquisto automatico di USDE con isteresi (ECCEDENTE), bande in config, escape HTML nel sink di notify

Decisione dell'operatore 10/09 («riportarla a quota in autonomia»). Verificato da revisione fable
(10 segnalazioni, 9 applicate; loop numerico su usde_convert.piano: nessuna sequenza vendita→acquisto).

- quarto stato ECCEDENTE: slack > cuscino_riacquisto_frac (0,40) x cuscino => acquisto di USDE fino
  allo stesso bersaglio della vendita, q* = 1 - 0,30 x (1 + cuscino_margine_frac 0,20) = 0,64.
  Isteresi [0 ; 0,40] x cuscino con bersaglio unico 0,20: per oscillare servono +-8,6% di equity
  USDC (~$380); un bonifico alza lo slack di 0,7 x importo e sopra ~9% dell'equity ricompra da solo.
- le tre frazioni (preavviso/margine/riacquisto) in config/live.json sezione usde, lette da
  usde.bande_cuscino() che verifica l'ordine; quota_target 0,70 TOLTO (slack zero per costruzione).
- notifier.notify: html.escape su titolo e valori — la prima allerta vera (13:53Z) era andata persa
  per un '<' nudo; allerta_errore registrato nel record (P3).
- da ECCEDENTE un fallimento si ritenta ogni 24h (non 4 Telegram/giorno per sempre); bande fuori
  ordine => BLIND con motivo, non un traceback ingoiato dal cron.
- CLAUDE.md §1/§4/§5.17, config _nota_usde/_nota_cuscino, diario. 0,64 e' una DERIVAZIONE
  dell'autore, non la quota decisa il 30/08 (0,70): dichiarato, da confermare.
- test: 1070 (+8 netti).

Fixes #7

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kqvff47UBGeYfj1QeN4zE
This commit is contained in:
Adriano Dal Pastro
2026-09-10 14:14:29 +00:00
parent f82f685528
commit 37d1565a1e
8 changed files with 311 additions and 89 deletions
+8 -2
View File
@@ -87,9 +87,15 @@ def notify(title: str, data: dict | None = None, tentativi: int = 1) -> bool:
ver = f" <code>v{APP_VERSION}</code>"
except Exception:
ver = ""
lines = [f"<b>{title}</b>{ver}"]
# Telegram e' in parse_mode=HTML: un '<' o '&' nudo in un valore fa fallire l'INTERO invio
# (HTTP 400, "can't parse entities"). Successo il 2026-09-10 13:53Z: la prima allerta di
# cuscino_watch conteneva «(< 10% del cuscino)» ed e' andata persa. L'escape sta QUI, nel sink,
# perche' nessuno dei 45 chiamanti passa tag di proposito (verificato con grep) e un chiamante
# nuovo non deve ricordarsene. `send()` resta grezzo per chi costruisce il proprio HTML.
import html
lines = [f"<b>{html.escape(str(title), quote=False)}</b>{ver}"]
for k, v in (data or {}).items():
lines.append(f" {k}: {v}")
lines.append(f" {html.escape(str(k), quote=False)}: {html.escape(str(v), quote=False)}")
return send("\n".join(lines), tentativi=tentativi)
+22 -3
View File
@@ -36,6 +36,10 @@ _DEFAULTS = {
"venue_cap_misurato": "2026-08-31",
"depeg_warn": 0.99,
"depeg_crit": 0.95,
# bande del cuscino USDC (frazioni del cuscino richiesto), issue #7 — vedi `_nota_cuscino`
"cuscino_preavviso_frac": 0.10,
"cuscino_margine_frac": 0.20,
"cuscino_riacquisto_frac": 0.40,
}
@@ -123,9 +127,24 @@ def cuscino_richiesto_usd(equity_tot: float) -> tuple[float, str]:
return equity_tot * f, f"{come} su ${equity_tot:,.2f}"
def quota_ripristino(margine_frac: float) -> float:
def bande_cuscino(c: dict | None = None) -> dict:
"""PURA sulla config. Le tre frazioni del cuscino che governano cuscino_watch, con l'ordine
verificato: 0 < preavviso < margine < riacquisto. Fuori ordine la banda di isteresi non
esiste (vendita e acquisto si inseguirebbero): si solleva, non si indovina."""
c = c or cfg()
b = dict(preavviso=float(c["cuscino_preavviso_frac"]), margine=float(c["cuscino_margine_frac"]),
riacquisto=float(c["cuscino_riacquisto_frac"]))
if not (0.0 < b["preavviso"] < b["margine"] < b["riacquisto"]):
raise ValueError(f"bande del cuscino fuori ordine (serve 0 < preavviso < margine < riacquisto): {b}")
return b
def quota_ripristino(margine_frac: float | None = None) -> float:
"""PURA sulla config. Quota USDE che lascia il cuscino PIU' un margine (frazione del cuscino):
e' il bersaglio della riconversione automatica quando il cuscino e' scoperto. Il margine serve
a non tornare sul bordo alla prima ora in perdita (P6: la persistenza e' latenza)."""
e' il bersaglio UNICO di ogni conversione automatica — vendita quando il cuscino e' scoperto,
acquisto quando lo slack eccede la banda. Il margine serve a non tornare sul bordo alla prima
ora in perdita (P6: la persistenza e' latenza). Default: `cuscino_margine_frac` di config."""
if margine_frac is None:
margine_frac = bande_cuscino()["margine"]
f, _ = frazione_cuscino()
return max(0.0, 1.0 - f * (1.0 + margine_frac))