journal: i versamenti non sono piu' P&L — movimenti di capitale scorporati dal trading

Il P&L di giornale era un delta di equity: la voce del 25/08 dichiarava +$1.414,57
di "giorno" quando $1.399,39 erano il deposito USDC, e il cumulato dall'arming
avrebbe mentito per sempre. Nuova `movimenti_capitale()`:
- soglia IMPORTATA da book.EQUITY_JUMP_ALERT, non ridichiarata (P1);
- "movimento" solo se il salto supera di >2x il massimo che il mercato MISURATO
  (feed certificato, tetto di leva da config) poteva produrre fra le due letture;
- altrimenti AMBIGUO: dichiarato con attenzione e NON scorporato (P12 -- un crash
  vero a tutta leva non deve diventare "prelievo"); idem a feed illeggibile (P5).
Scansione dell'intera storia: 1 evento, esattamente il deposito (+209,6% contro
mercato max 0,22%), zero falsi positivi. `concentrazione` ora lavora sul cumulato
di TRADING; le regole sui movimenti stanno sopra l'early-return "nessun giro".
Limite dichiarato (D5): un movimento sotto il 10% dell'equity resta nel P&L.
7 prove nuove (31 -> 37), incluso il controllo M15 al contrario. Voce del 25/08
rigenerata: giorno -> trading +$15,18; cumulato -> trading +$59,14.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B9UyJLHzR7EJzxR3iQ3RN1
This commit is contained in:
Adriano Dal Pastro
2026-08-26 07:35:15 +00:00
parent f10d847816
commit a51844875b
4 changed files with 281 additions and 25 deletions
+144 -20
View File
@@ -107,8 +107,83 @@ def stato_libro(con, giorno: date) -> dict:
azione=v["azione"]) for a, v in ult.stato_asset.items()})
def _px_1h(asset: str) -> pd.Series | None:
"""Chiusure orarie UTC dal feed certificato; None se non leggibile (P5: si dichiara)."""
try:
from src.data.downloader import load_data
df = load_data(asset, "1h")
s = df.set_index(pd.to_datetime(df["timestamp"], unit="ms", utc=True))["close"]
return s.sort_index()
except Exception:
return None
def movimenti_capitale(con, fino_ts: str) -> dict:
"""Salti di equity fra letture consecutive: versamenti e prelievi, da scorporare dal P&L.
PERCHE' ESISTE. Il P&L di giornale e' un delta di equity, quindi un versamento ci finisce
dentro come se fosse profitto: la voce del 2026-08-25 dichiarava «giorno: +$1.414,57» quando
$1.399 erano un deposito USDC, e il «cumulato dall'arming» avrebbe mentito per sempre.
COME CLASSIFICA (tre stati, mai due):
- candidato = salto fra due letture consecutive oltre `EQUITY_JUMP_ALERT` del book —
la soglia e' IMPORTATA, non ridichiarata (P1: il rilevatore di movimenti e' quello);
- `movimento` solo se il salto supera di >2x il massimo che il MERCATO avrebbe potuto
produrre nell'intervallo: |Δprezzo| reale dal feed certificato x il tetto di leva letto
da config. Non un'ipotesi sul mercato: il mercato misurato in quella finestra;
- `ambiguo` altrimenti, o quando il mercato non e' misurabile: si DICHIARA e non si
scorpora (P12 — una riparazione silenziosa e' un'invenzione; un crash vero a tutta
leva non deve trasformarsi in "prelievo").
⚠️ 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.
"""
from src.live import book as _book # lazy: prende la soglia viva, anche monkeypatchata
rows = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc <= ? ORDER BY ts_utc",
(fino_ts,)).fetchall()
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)
try:
cfg = json.loads((PROJECT_ROOT / "config" / "live.json").read_text())
leva_tetto = float(cfg["max_notional_per_asset_frac"]) * len(ASSETS)
except Exception:
leva_tetto = 1.0 # il tetto di progetto: bound conservativo, usato solo qui
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 = []
for a in ASSETS:
s = px.get(a)
if s is None or s.empty:
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"
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))
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)
def pnl_giorno(con, giorno: date) -> dict:
"""P&L del giorno. `equity` e' l'autorita' (venue); il resto e' scomposizione locale."""
"""P&L del giorno. `equity` e' l'autorita' (venue); il resto e' scomposizione locale.
`delta_equity` e `cumulato_da_arming` restano FATTI (l'equity si e' mossa cosi');
`trading_giorno` e `trading_da_arming` sono gli stessi numeri al netto dei movimenti
di capitale CERTI — gli ambigui restano dentro, dichiarati a parte."""
g = giorno.isoformat()
eq = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc LIKE ? ORDER BY ts_utc",
(f"{g}%",)).fetchall()
@@ -119,15 +194,23 @@ def pnl_giorno(con, giorno: date) -> dict:
rt = con.execute("SELECT * FROM roundtrips WHERE ts_out LIKE ?", (f"{g}%",)).fetchall()
fills = con.execute("SELECT * FROM fills WHERE ts_utc LIKE ?", (f"{g}%",)).fetchall()
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")
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")
return dict(
equity_inizio=e_ini, equity_fine=e_fin,
delta_equity=(e_fin - e_ini) if (e_ini is not None and e_fin is not None) else None,
equity_inizio=e_ini, equity_fine=e_fin, delta_equity=delta,
realizzato_lordo=sum(r["pnl_lordo"] for r in rt) if rt else 0.0,
realizzato_netto=sum(r["pnl_netto"] for r in rt) if rt else 0.0,
roundtrip_chiusi=len(rt), fill=len(fills),
fee=sum(f["fee"] for f in fills) if fills else 0.0,
cumulato_da_arming=(e_fin - e_arm["equity"]) if (e_fin is not None and e_arm) else None,
letture_equity=len(eq))
cumulato_da_arming=cum, letture_equity=len(eq),
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)
def salute(con, giorno: date) -> dict:
@@ -201,7 +284,25 @@ def lettura(con, voce: dict) -> list[dict]:
def dire(rid, testo, livello="info"):
out.append(dict(id=rid, testo=testo, livello=livello))
# --- 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":
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. "
"Scorporato dal P&L di trading qui sotto.", "nota")
else:
perche = ("il mercato non e' misurabile in quella finestra" if e["mercato_max_pct"]
is None else f"il mercato avrebbe potuto produrlo (max "
f"±{100*e['mercato_max_pct']:.1f}% al tetto di leva)")
dire("movimento_ambiguo", f"Salto di equity **${e['delta']:+,.2f}** fra le {h0} e le "
f"{h1} UTC che NON si puo' attribuire: {perche}. "
"Resta nel P&L, dichiarato — non scorporato.", "attenzione")
# --- cosa il libro STA FACENDO -----------------------------------------------------------
# (le regole sui movimenti stanno SOPRA questo return: un versamento in un giorno senza
# giri di book esiste lo stesso — viene dal DB equity, non dal log del libro)
if "errore" in lb:
dire("stato", f"Nessun giro di `book_execute`: {lb['errore']}. "
"Non e' «il libro era fermo», e' «non e' stato misurato».", "attenzione")
@@ -264,16 +365,23 @@ def lettura(con, voce: dict) -> list[dict]:
# --- P&L del giorno, in proporzione ------------------------------------------------------
dq, fee = p.get("delta_equity"), p.get("fee", 0.0)
if dq is not None:
if p["fill"] and fee > 0 and abs(dq) < fee:
dire("fee", f"Il movimento di equity del giorno (${dq:+.2f}) e' **piu' piccolo delle "
f"fee pagate** (${fee:.4f}): la giornata l'ha decisa il costo, non il segnale.",
"nota")
dq_t = p.get("trading_giorno")
dq_eff = dq_t if dq_t is not None else dq
scorporo = bool(p.get("mov_giorno"))
if dq_eff is not None:
if p["fill"] and fee > 0 and abs(dq_eff) < fee:
dire("fee", f"Il movimento di equity del giorno (${dq_eff:+.2f}"
f"{', al netto dei movimenti di capitale' if scorporo else ''}) e' "
f"**piu' piccolo delle fee pagate** (${fee:.4f}): la giornata l'ha "
"decisa il costo, non il segnale.", "nota")
elif p["fill"] == 0:
dire("pnl", f"Equity ${dq:+.2f} **senza operare**: e' mark-to-market sulle posizioni "
"gia' aperte.")
dire("pnl", f"Equity ${dq_eff:+.2f} **senza operare**: e' mark-to-market sulle "
"posizioni gia' aperte."
+ (" (Al netto dei movimenti di capitale.)" if scorporo else ""))
else:
dire("pnl", f"Equity ${dq:+.2f} con {p['fill']} fill e "
dire("pnl", (f"P&L di trading ${dq_eff:+.2f} (equity ${dq:+.2f} al lordo del "
f"movimento di capitale) " if scorporo else f"Equity ${dq_eff:+.2f} ")
+ f"con {p['fill']} fill e "
f"{p['roundtrip_chiusi']} round-trip chiusi (realizzato "
f"${p['realizzato_netto']:+.2f} netto).")
@@ -282,15 +390,19 @@ def lettura(con, voce: dict) -> list[dict]:
(voce["giorno"] + "T23:59:59+00:00",)).fetchall()
if len(eq) > 24:
e0, e1 = eq[0]["equity"], eq[-1]["equity"]
tot = e1 - e0
# il cumulato di TRADING: i movimenti di capitale certi escono, o un versamento
# diventa "concentrazione di P&L" (e' successo: 25/08, +$1.399 letti come profitto)
mv_ev = [e for e in (p.get("movimenti_arming") or []) if e["classe"] == "movimento"]
g7s = (date.fromisoformat(voce["giorno"]) - timedelta(days=7)).isoformat()
tot = e1 - e0 - sum(e["delta"] for e in mv_ev)
picco = max(r["equity"] for r in eq)
g7 = [r for r in eq if r["ts_utc"][:10] >= (date.fromisoformat(voce["giorno"]) -
timedelta(days=7)).isoformat()]
g7 = [r for r in eq if r["ts_utc"][:10] >= g7s]
mv7 = sum(e["delta"] for e in mv_ev if e["ts_dopo"][:10] >= g7s)
# Soglia di rilevanza: su un cumulato di pochi dollari la "concentrazione" e' aritmetica
# vera e informazione zero. Una regola che si accende su $2 insegna a saltare la sezione.
rilevante = abs(tot) >= max(5.0, 0.01 * e1)
if rilevante and g7:
quota = (e1 - g7[0]["equity"]) / tot
quota = (e1 - g7[0]["equity"] - mv7) / tot
if quota > 0.60:
coda = ("Un risultato concentrato in una finestra non e' un tasso di rendimento: "
"e' un evento.")
@@ -424,12 +536,24 @@ def rendi_markdown(con, voce: dict) -> str:
sk += " @ {:,.1f}".format(v["skh_entry"])
L.append(f"| **{a}** | {v['tp_frac']:+.3f} | {sk} | ${v['target']:+,.0f} | "
f"${v['posizione']:+,.0f} | {v['azione']} |")
riga_g = (f"- giorno: **{_n(p.get('delta_equity'),'${:+,.2f}')}** di equity "
f"({p['letture_equity']} letture)")
if p.get("mov_giorno"):
riga_g += (f" — di cui **${p['mov_giorno']:+,.2f} movimenti di capitale** -> trading "
f"**{_n(p.get('trading_giorno'),'${:+,.2f}')}**")
if p.get("mov_giorno_ambiguo"):
riga_g += f" — ⚠️ salto ambiguo di ${p['mov_giorno_ambiguo']:+,.2f} non attribuito"
riga_c = f"- cumulato dall'arming: **{_n(p.get('cumulato_da_arming'),'${:+,.2f}')}** di equity"
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("mov_arming_ambiguo"):
riga_c += f" — ⚠️ ${p['mov_arming_ambiguo']:+,.2f} ambigui non attribuiti"
L += ["", "## P&L", "",
f"- giorno: **{_n(p.get('delta_equity'),'${:+,.2f}')}** di equity "
f"({p['letture_equity']} letture)",
riga_g,
f"- realizzato: {p['realizzato_netto']:+.2f} netto su {p['roundtrip_chiusi']} round-trip "
f"chiusi · {p['fill']} fill · fee {p['fee']:.4f}",
f"- cumulato dall'arming: **{_n(p.get('cumulato_da_arming'),'${:+,.2f}')}**", "",
riga_c, "",
"## Salute", "",
f"- giri di `book_execute`: {s['giri_book']}/{s['atteso_giri']}"
+ (" *(giorno in corso)*" if s.get("giorno_in_corso") else "")