usde al 68,7%: il tetto del venue del 30-31/08 non c'era piu'; il versamento di prova del 25/08 dichiarato, TWR +7,83%
Su autorizzazione dell'operatore («usa piu' USDE», «cerca % massima raggiungibile»): - usde_convert al 31,8% (774 USDE), poi sonda a saldo crescente r0906_usde_tetto_sonda.py (passi 100->20->4->1, doppio rifiuto prima di scendere, conferma a saldo neutro): da 31,8% a 68,7% in 39 ordini @ 1,0005, ZERO rifiuti. Fermata dal cuscino di regolamento (70%, derivato da config), non dal venue. «Il 70% non e' raggiungibile ne' ora ne' mai» (31/08) e' falsificato: il tetto e' sparito, non scalato. Non spiegato, puo' tornare. - config: venue_cap_frac -> null (misura in venue_cap_misurato), quota_max_frac 0,50 -> 0,85 (il valore scelto dall'operatore il 30/08 per questo scenario). - debito §5.17: nessuno sorveglia il cuscino USDC (slack $58; una perdita del libro lo consuma da sola). Versamento di prova di EUR 25 il 25/08 08:00Z (dichiarato dall'operatore): +3,3% su $647, sotto la soglia del 10% del rilevatore, contato per 12 giorni come trading. - data/live/movimenti_dichiarati.jsonl (append-only, nel backup) letto da journal.movimenti_dichiarati; movimenti_capitale lo fonde coi rilevati (fonte dichiarato, importo dell'operatore, dopo = prima + importo; dichiarato+rilevato se coincide; fuori letture o riga rotta -> avvisi). Report e nota di giornale lo dicono. - trading da arming +87,35 -> +62,45; TWR +11,98% -> +7,83%. CLAUDE.md §2 aggiornato. - tests: +6 in test_journal (conftest isola il file). Suite 916 verdi. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KSnordG9FT4q8M4MVm85GQ
This commit is contained in:
+80
-8
@@ -25,6 +25,12 @@ from src.live import tradesdb as T
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
JOURNAL_DIR = PROJECT_ROOT / "docs" / "journal"
|
||||
# Movimenti di capitale DICHIARATI dall'operatore (append-only, dentro il perimetro di backup —
|
||||
# P11). Servono per i movimenti che il rilevatore non puo' vedere: sotto `EQUITY_JUMP_ALERT`
|
||||
# (il versamento di prova da ~$25 del 2026-08-25 08:00Z, +3,3% su $647, restato nel P&L per
|
||||
# 12 giorni) o dentro un'ora di mercato non misurabile. Una riga per movimento:
|
||||
# {"ts_utc": "...", "delta_usd": 24.9, "banda_usd": 0.5, "nota": "...", "dichiarato_il": "..."}
|
||||
MOVIMENTI_DICHIARATI = PROJECT_ROOT / "data" / "live" / "movimenti_dichiarati.jsonl"
|
||||
ASSETS = ("BTC", "ETH")
|
||||
# Gli orizzonti del segnale TP01 in produzione (trend_portfolio: TSMOM 30/90/180 giorni).
|
||||
ORIZZONTI = (30, 90, 180)
|
||||
@@ -118,6 +124,33 @@ def _px_1h(asset: str) -> pd.Series | None:
|
||||
return None
|
||||
|
||||
|
||||
def movimenti_dichiarati(path: Path | None = None) -> tuple[list[dict], list[str]]:
|
||||
"""Righe di `MOVIMENTI_DICHIARATI` -> (movimenti validi, errori di riga). Mai solleva.
|
||||
Una riga rotta non azzera le altre e NON sparisce: torna in `errori` (P3 — si registra nel
|
||||
punto in cui si ingoia)."""
|
||||
path = MOVIMENTI_DICHIARATI if path is None else path
|
||||
if not path.exists():
|
||||
return [], []
|
||||
validi, errori = [], []
|
||||
for n, riga in enumerate(path.read_text().splitlines(), 1):
|
||||
if not riga.strip():
|
||||
continue
|
||||
try:
|
||||
d = json.loads(riga)
|
||||
ts = pd.Timestamp(d["ts_utc"])
|
||||
if ts.tzinfo is None:
|
||||
raise ValueError("ts_utc senza fuso")
|
||||
delta = float(d["delta_usd"])
|
||||
if delta == 0:
|
||||
raise ValueError("delta_usd zero")
|
||||
validi.append(dict(ts=ts, delta=delta, banda=float(d.get("banda_usd") or 0.0),
|
||||
nota=str(d.get("nota") or ""), dichiarato_il=d.get("dichiarato_il")))
|
||||
except Exception as e: # noqa: BLE001 — la riga rotta si riporta
|
||||
errori.append(f"{path.name}:{n}: {type(e).__name__}: {e}")
|
||||
validi.sort(key=lambda d: d["ts"])
|
||||
return validi, errori
|
||||
|
||||
|
||||
def movimenti_capitale(con, fino_ts: str, rows=None) -> dict:
|
||||
"""Salti di equity fra letture consecutive: versamenti e prelievi, da scorporare dal P&L.
|
||||
|
||||
@@ -137,6 +170,13 @@ def movimenti_capitale(con, fino_ts: str, rows=None) -> dict:
|
||||
|
||||
⚠️ LIMITE DICHIARATO (D5): un movimento sotto soglia (es. $150 su un conto da $2.000)
|
||||
non si distingue dal mercato e RESTA nel P&L. E' lo stesso limite del rilevatore live.
|
||||
Il rimedio NON e' abbassare la soglia (ogni ora di mercato diventerebbe un candidato): e'
|
||||
la DICHIARAZIONE dell'operatore in `MOVIMENTI_DICHIARATI` — l'unica fonte che sa l'importo.
|
||||
Un movimento dichiarato e' `movimento` con `fonte="dichiarato"`, l'importo e' quello
|
||||
dichiarato (non il salto di equity), e `dopo` = `prima` + importo: il P&L di mercato di
|
||||
quell'ora resta nel rendimento invece di uscire con il movimento. Se coincide con un salto
|
||||
rilevato, l'importo dichiarato VINCE sul salto (fonte "dichiarato+rilevato"); un movimento
|
||||
dichiarato fuori dalle letture, o dopo `fino_ts`, non entra e finisce in `avvisi`.
|
||||
"""
|
||||
from src.live import book as _book # lazy: prende la soglia viva, anche monkeypatchata
|
||||
# `rows` si puo' passare gia' letto: due SELECT sulla stessa tabella a un'ora del cron
|
||||
@@ -147,8 +187,9 @@ def movimenti_capitale(con, fino_ts: str, rows=None) -> dict:
|
||||
soglia = float(_book.EQUITY_JUMP_ALERT)
|
||||
candidati = [(a, b) for a, b in zip(rows, rows[1:])
|
||||
if a["equity"] and abs(b["equity"] - a["equity"]) / a["equity"] >= soglia]
|
||||
if not candidati:
|
||||
return dict(eventi=[], certi=0.0, ambigui=0.0, soglia=soglia)
|
||||
dichiarati, avvisi = movimenti_dichiarati()
|
||||
if not candidati and not dichiarati:
|
||||
return dict(eventi=[], certi=0.0, ambigui=0.0, soglia=soglia, avvisi=avvisi)
|
||||
try:
|
||||
cfg = json.loads((PROJECT_ROOT / "config" / "live.json").read_text())
|
||||
# la leva lorda massima che il libro puo' tenere: frac x n_asset x scala, mai oltre il
|
||||
@@ -159,7 +200,7 @@ def movimenti_capitale(con, fino_ts: str, rows=None) -> dict:
|
||||
float(_book.LEVA_LORDA_MAX))
|
||||
except Exception:
|
||||
leva_tetto = float(_book.LEVA_LORDA_MAX) # config illeggibile: il tetto di codice
|
||||
px = {a: _px_1h(a) for a in ASSETS}
|
||||
px = {a: _px_1h(a) for a in ASSETS} if candidati else {}
|
||||
eventi = []
|
||||
for prev, cur in candidati:
|
||||
t0, t1 = pd.Timestamp(prev["ts_utc"]), pd.Timestamp(cur["ts_utc"])
|
||||
@@ -192,11 +233,38 @@ def movimenti_capitale(con, fino_ts: str, rows=None) -> dict:
|
||||
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)),
|
||||
mercato_nota=nota, classe=classe))
|
||||
mercato_nota=nota, classe=classe, fonte="rilevato"))
|
||||
# --- dichiarati dall'operatore: l'importo lo sa solo lui ------------------------------
|
||||
ts_rows = [pd.Timestamp(r["ts_utc"]) for r in rows]
|
||||
for d in dichiarati:
|
||||
if not rows or d["ts"] <= ts_rows[0] or d["ts"] > ts_rows[-1]:
|
||||
avvisi.append(f"movimento dichiarato {d['ts']:%Y-%m-%dT%H:%M}Z ${d['delta']:+,.2f} "
|
||||
f"fuori dalle letture [{rows[0]['ts_utc'][:16] if rows else '-'} .. "
|
||||
f"{rows[-1]['ts_utc'][:16] if rows else '-'}]: non applicato")
|
||||
continue
|
||||
i = next(k for k in range(1, len(rows)) if ts_rows[k - 1] < d["ts"] <= ts_rows[k])
|
||||
prev, cur = rows[i - 1], rows[i]
|
||||
nota_d = (f"dichiarato dall'operatore"
|
||||
+ (f" il {d['dichiarato_il']}" if d.get("dichiarato_il") else "")
|
||||
+ (f": {d['nota']}" if d["nota"] else "")
|
||||
+ (f" (banda ±${d['banda']:,.2f})" if d["banda"] else ""))
|
||||
gia = next((e for e in eventi if e["ts_dopo"] == cur["ts_utc"]), None)
|
||||
if gia is not None: # rilevato E dichiarato: l'importo e' quello dichiarato
|
||||
gia.update(delta=d["delta"], pct=d["delta"] / float(prev["equity"]) if prev["equity"] else None,
|
||||
classe="movimento", fonte="dichiarato+rilevato",
|
||||
dopo=float(prev["equity"]) + d["delta"], dichiarazione=nota_d)
|
||||
continue
|
||||
eventi.append(dict(ts_prima=prev["ts_utc"], ts_dopo=cur["ts_utc"],
|
||||
prima=float(prev["equity"]), dopo=float(prev["equity"]) + d["delta"],
|
||||
delta=d["delta"],
|
||||
pct=(d["delta"] / float(prev["equity"]) if prev["equity"] else None),
|
||||
mercato_max_pct=None, mercato_nota=None, classe="movimento",
|
||||
fonte="dichiarato", dichiarazione=nota_d))
|
||||
eventi.sort(key=lambda e: e["ts_dopo"])
|
||||
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")),
|
||||
soglia=soglia, leva_tetto=leva_tetto)
|
||||
soglia=soglia, leva_tetto=leva_tetto, avvisi=avvisi)
|
||||
|
||||
|
||||
def rendimento_twr(con, fino_ts: str) -> dict:
|
||||
@@ -241,7 +309,7 @@ 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")]
|
||||
non_mis = [e for e in mv["eventi"] if e.get("mercato_nota") and e["classe"] != "movimento"]
|
||||
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
|
||||
@@ -266,7 +334,7 @@ def rendimento_twr(con, fino_ts: str) -> dict:
|
||||
twr *= 1.0 + sg["ret"]
|
||||
return dict(twr=twr - 1.0, motivo=None, segmenti=segmenti, eventi=mv["eventi"],
|
||||
certi=mv["certi"], ambigui=mv["ambigui"], delta_equity=e1 - e0,
|
||||
trading=e1 - e0 - mv["certi"], letture=len(rows))
|
||||
trading=e1 - e0 - mv["certi"], letture=len(rows), avvisi=mv.get("avvisi") or [])
|
||||
|
||||
|
||||
def pnl_giorno(con, giorno: date) -> dict:
|
||||
@@ -413,7 +481,11 @@ def lettura(con, voce: dict) -> list[dict]:
|
||||
# --- movimenti di capitale: prima del P&L, perche' lo ridefiniscono ----------------------
|
||||
for e in p.get("movimenti_giorno") or []:
|
||||
h0, h1 = e["ts_prima"][11:16], e["ts_dopo"][11:16]
|
||||
if e["classe"] == "movimento":
|
||||
if e["classe"] == "movimento" and e.get("fonte", "rilevato") != "rilevato":
|
||||
dire("movimento", f"**Movimento di capitale ${e['delta']:+,.2f}** fra le {h0} e le "
|
||||
f"{h1} UTC, {e.get('dichiarazione') or 'dichiarato'}. "
|
||||
"Scorporato dal P&L di trading qui sotto.", "nota")
|
||||
elif e["classe"] == "movimento":
|
||||
dire("movimento", f"**Movimento di capitale ${e['delta']:+,.2f}** fra le {h0} e le "
|
||||
f"{h1} UTC: salto {100*e['pct']:+.1f}% contro un massimo spiegabile "
|
||||
f"dal mercato di ±{100*e['mercato_max_pct']:.1f}% al tetto di leva. "
|
||||
|
||||
Reference in New Issue
Block a user