revisione 02/09, seconda tornata: il classificatore dei movimenti era cieco ~23 ore al giorno

Il feed 1h certificato si ferma alle 00:00: per le letture successive `asof` dava la stessa
barra a t0 e t1, mercato "fermo" = 0, e qualunque calo >=10% del giorno sarebbe stato un
"movimento" scorporato come prelievo (giornale dal 25/08, report da oggi). Con feed assente
tutto era "ambiguo" e il report stampava e1/e0-1 sotto l'etichetta TWR.

- journal.movimenti_capitale: stato "mercato non misurabile" (feed fermo/assente/barra
  mancante) => ambiguo con la ragione; leva_tetto = min(frac x n x scala, LEVA_LORDA_MAX) (P1).
- journal.rendimento_twr: salto non classificabile => twr E trading None con motivo.
- journal.pnl_giorno CHIAMA rendimento_twr (prima rifaceva cum - certi: "entrambi la chiamano"
  era falso); la pagina stampa il TWR; analista qualifica il cumulato "di cui versati".
- trades_db --report: la classe di ogni salto con la sua misura (mercato max a leva piena).
- book_execute docstring: banda dello stop -33,5%/-26,5% (era invertita), "non costa ordini"
  -> micro-ordini di ri-taglia (22/48, C2), latenza <=1h solo con feed fresca, conteggi di
  r0823 al posto di "raddoppia" (2 contro 1 e' rotolante vs pavimento sulla riga 4h; la
  peggiore misurata e' 24h).
- test_book_cadenza: LTF_MIN importato da skyhook; tutte le righe attive della crontab (una
  sola); tre stati dello skip (col progetto ma senza cron_book => ROSSO); slot di release
  testato con venue_probe (il :07 come controllo positivo); parser regolare, passo > 0.
- r0823_sl_anchor: guardia sul :47 (era sul vecchio :07), prosa al passato; cron_chain.sh idem.
- CLAUDE.md: §5.7 frase invertita corretta, §5.14 limite vero (non l'artefatto della fixture),
  §2 ora della LETTURA (13:47Z), nuovo debito §5.15 (USDE fuori dal bound di mercato).
- diari: tempi "scritto" corretti coi commit; sezione "Seconda tornata".

Test +11 (858 verdi).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDJsH3iDSaBns3ccpPBwiu
This commit is contained in:
Adriano Dal Pastro
2026-09-02 15:33:13 +00:00
parent 835e0c8666
commit 1279ea605a
13 changed files with 328 additions and 90 deletions
+51 -12
View File
@@ -148,30 +148,48 @@ def movimenti_capitale(con, fino_ts: str) -> dict:
return dict(eventi=[], certi=0.0, ambigui=0.0, soglia=soglia)
try:
cfg = json.loads((PROJECT_ROOT / "config" / "live.json").read_text())
leva_tetto = float(cfg["max_notional_per_asset_frac"]) * len(ASSETS)
# la leva lorda massima che il libro puo' tenere: frac x n_asset x scala, mai oltre il
# tetto di CODICE (P1: derivata da `book`, non ridichiarata — la chiave di scala del
# 01/09 entra qui da sola)
scala = float(cfg.get("book_scale_k") or 1.0)
leva_tetto = min(float(cfg["max_notional_per_asset_frac"]) * len(ASSETS) * scala,
float(_book.LEVA_LORDA_MAX))
except Exception:
leva_tetto = 1.0 # il tetto di progetto: bound conservativo, usato solo qui
leva_tetto = float(_book.LEVA_LORDA_MAX) # config illeggibile: il tetto di codice
px = {a: _px_1h(a) for a in ASSETS}
eventi = []
for prev, cur in candidati:
t0, t1 = pd.Timestamp(prev["ts_utc"]), pd.Timestamp(cur["ts_utc"])
delta = float(cur["equity"] - prev["equity"])
pct = delta / prev["equity"]
mosse = []
# Il mercato e' MISURABILE solo se il feed copre entrambe le letture. Il feed 1h
# certificato si ferma alle 00:00 (rebuild alle 00:30): per il resto del giorno
# `asof` restituirebbe la STESSA barra per t0 e t1, max_mkt 0, e qualunque calo >=10%
# diventerebbe un "movimento" — un crash stampato come prelievo (revisione 02/09).
# Non misurabile => `ambiguo` con la ragione (P5: "non vedo" non e' "mercato fermo").
mosse, non_mis = [], []
for a in ASSETS:
s = px.get(a)
if s is None or s.empty:
continue
non_mis.append(f"{a}: feed 1h non leggibile"); continue
fine = s.index[-1] + pd.Timedelta(hours=1) # l'ultima barra copre un'ora
if t0 < s.index[0] or t1 > fine:
non_mis.append(f"{a}: feed 1h fermo a {s.index[-1]:%Y-%m-%dT%H:%M}Z"); continue
p0, p1 = s.asof(t0), s.asof(t1)
if p0 and p1 and not (pd.isna(p0) or pd.isna(p1)):
mosse.append(abs(float(p1) / float(p0) - 1.0))
max_mkt = max(mosse) * leva_tetto if mosse else None
classe = "movimento" if (max_mkt is not None and abs(pct) > 2.0 * max_mkt) else "ambiguo"
if pd.isna(p0) or pd.isna(p1) or not p0 or not p1:
non_mis.append(f"{a}: barra assente"); continue
mosse.append(abs(float(p1) / float(p0) - 1.0))
if non_mis:
max_mkt, classe, nota = None, "ambiguo", "mercato non misurabile: " + "; ".join(non_mis)
else:
max_mkt = max(mosse) * leva_tetto
classe = "movimento" if abs(pct) > 2.0 * max_mkt else "ambiguo"
nota = None
eventi.append(dict(ts_prima=prev["ts_utc"], ts_dopo=cur["ts_utc"],
prima=float(prev["equity"]), dopo=float(cur["equity"]),
delta=delta, pct=float(pct),
mercato_max_pct=(None if max_mkt is None else float(max_mkt)),
classe=classe))
mercato_nota=nota, classe=classe))
return dict(eventi=eventi,
certi=float(sum(e["delta"] for e in eventi if e["classe"] == "movimento")),
ambigui=float(sum(e["delta"] for e in eventi if e["classe"] == "ambiguo")),
@@ -200,7 +218,10 @@ def rendimento_twr(con, fino_ts: str) -> dict:
Non si stima e non si corregge (P12): si dichiara. Un intervallo di sola marcatura fra due
movimenti consecutivi, o prima del primo, ha lunghezza zero e NON produce un segmento.
TRE STATI: `twr` e `trading` sono numeri, oppure None con `motivo`. Base di equity zero =>
TRE STATI: `twr` e `trading` sono numeri, oppure None con `motivo`. Mercato NON misurabile
per almeno un salto (feed 1h fermo, o assente) => entrambi None: non si conosce la classe
del salto, quindi non si conosce il rendimento — e `e1/e0-1` non torna sotto un'etichetta
piu' autorevole. Base di equity zero =>
entrambi None: il salto 0 -> X del primo versamento e' INVISIBILE a `movimenti_capitale`
(una base zero non ha percentuale), quindi `certi` lo perderebbe e `trading` varrebbe
l'intero conto — un numero etichettato "al netto dei versamenti" che li contiene tutti.
@@ -217,6 +238,16 @@ def rendimento_twr(con, fino_ts: str) -> dict:
motivo="base di equity nulla: il primo versamento non e' classificabile",
segmenti=[], eventi=mv["eventi"], certi=mv["certi"], ambigui=mv["ambigui"],
delta_equity=e1 - e0, letture=len(rows))
non_mis = [e for e in mv["eventi"] if e.get("mercato_nota")]
if non_mis:
# un salto oltre soglia che NON si e' potuto confrontare col mercato non e' ne' un
# movimento ne' un rendimento: il TWR non si stampa (altrimenti `e1/e0-1` tornerebbe
# sotto l'etichetta "TWR", il numero che il debito 14 e' nato per uccidere)
return dict(twr=None, trading=None,
motivo=(f"{len(non_mis)} salto/i non classificabile/i — "
+ non_mis[0]["mercato_nota"]),
segmenti=[], eventi=mv["eventi"], certi=mv["certi"], ambigui=mv["ambigui"],
delta_equity=e1 - e0, letture=len(rows))
certi = [e for e in mv["eventi"] if e["classe"] == "movimento"]
segmenti, da_ts, da_eq = [], rows[0]["ts_utc"], e0
for ev in certi: # gia' in ordine di tempo
@@ -253,7 +284,10 @@ def pnl_giorno(con, giorno: date) -> dict:
e_arm = con.execute("SELECT equity FROM equity ORDER BY ts_utc LIMIT 1").fetchone()
delta = (e_fin - e_ini) if (e_ini is not None and e_fin is not None) else None
cum = (e_fin - e_arm["equity"]) if (e_fin is not None and e_arm) else None
mv = movimenti_capitale(con, f"{g}T23:59:59+00:00")
# il cumulato dall'arming, i movimenti e il trading al netto vengono da rendimento_twr:
# UNA funzione per i due lettori (report e giornale), non due aritmetiche (revisione 02/09)
r_twr = rendimento_twr(con, f"{g}T23:59:59+00:00")
mv = dict(eventi=r_twr["eventi"], certi=r_twr["certi"], ambigui=r_twr["ambigui"])
ev_g = [e for e in mv["eventi"] if e["ts_dopo"][:10] == g]
mov_g = sum(e["delta"] for e in ev_g if e["classe"] == "movimento")
amb_g = sum(e["delta"] for e in ev_g if e["classe"] == "ambiguo")
@@ -267,7 +301,8 @@ def pnl_giorno(con, giorno: date) -> dict:
movimenti_giorno=ev_g, mov_giorno=float(mov_g), mov_giorno_ambiguo=float(amb_g),
trading_giorno=(delta - mov_g) if delta is not None else None,
movimenti_arming=mv["eventi"], mov_arming=mv["certi"], mov_arming_ambiguo=mv["ambigui"],
trading_da_arming=(cum - mv["certi"]) if cum is not None else None)
trading_da_arming=(r_twr["trading"] if cum is not None else None),
twr_da_arming=(r_twr["twr"] if cum is not None else None), twr_motivo=r_twr["motivo"])
def salute(con, giorno: date) -> dict:
@@ -639,6 +674,10 @@ def rendi_markdown(con, voce: dict) -> str:
if p.get("mov_arming"):
riga_c += (f" — di cui ${p['mov_arming']:+,.2f} versati/prelevati -> trading "
f"**{_n(p.get('trading_da_arming'),'${:+,.2f}')}**")
if p.get("twr_da_arming") is not None:
riga_c += f" · TWR **{100 * p['twr_da_arming']:+.2f}%**"
elif p.get("twr_motivo"):
riga_c += f" · TWR n/d ({p['twr_motivo']})"
if p.get("mov_arming_ambiguo"):
riga_c += f" — ⚠️ ${p['mov_arming_ambiguo']:+,.2f} ambigui non attribuiti"
L += ["", "## P&L", "",