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
+6 -5
View File
@@ -1,6 +1,6 @@
# Giornale di bordo — 2026-08-25 # Giornale di bordo — 2026-08-25
*Scritto 2026-08-26T00:37:44+00:00. Numeri misurati; nessuna interpretazione automatica.* *Scritto 2026-08-26T07:05:51+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato ## Mercato
@@ -20,9 +20,9 @@ Equity **$2,056.59** · nozionale lordo $557 · leva lorda 0.27x · barra dati 2
## P&L ## P&L
- giorno: **$+1,414.57** di equity (24 letture) - giorno: **$+1,414.57** di equity (24 letture) — di cui **$+1,399.39 movimenti di capitale** -> trading **$+15.18**
- realizzato: +3.00 netto su 6 round-trip chiusi · 7 fill · fee 0.3452 - realizzato: +3.00 netto su 6 round-trip chiusi · 7 fill · fee 0.3452
- cumulato dall'arming: **$+1,458.53** - cumulato dall'arming: **$+1,458.53** di equity — di cui $+1,399.39 versati/prelevati -> trading **$+59.14**
## Salute ## Salute
@@ -33,10 +33,11 @@ Equity **$2,056.59** · nozionale lordo $557 · leva lorda 0.27x · barra dati 2
*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* *Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.*
- `[movimento]` 📌 **Movimento di capitale $+1,399.39** fra le 10:47 e le 11:47 UTC: salto +209.6% contro un massimo spiegabile dal mercato di ±0.2% al tetto di leva. Scorporato dal P&L di trading qui sotto.
- `[stato]` A mercato su BTC $+345 (TP01 +0.450, SKH01 flat) e ETH $+212 (TP01 +0.274, SKH01 flat). - `[stato]` A mercato su BTC $+345 (TP01 +0.450, SKH01 flat) e ETH $+212 (TP01 +0.274, SKH01 flat).
- `[leva]` Leva lorda **0.27x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.73x. - `[leva]` Leva lorda **0.27x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.73x.
- `[pnl]` Equity $+1414.57 con 7 fill e 6 round-trip chiusi (realizzato $+3.00 netto). - `[pnl]` P&L di trading $+15.18 (equity $+1414.57 al lordo del movimento di capitale) con 7 fill e 6 round-trip chiusi (realizzato $+3.00 netto).
- `[concentrazione]` 📌 **Il 100% di tutto il P&L cumulato ($+1,458.53) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento. - `[concentrazione]` 📌 **Il 102% di tutto il P&L cumulato ($+59.14) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento.
- `[drawdown]` 📌 Equity **-0.51%** sotto il picco ($2,067.23). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. - `[drawdown]` 📌 Equity **-0.51%** sotto il picco ($2,067.23). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 43.3 contro realizzata 30g 43.5 (sotto); DVOL al 58° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). - `[vol]` BTC: implicita 43.3 contro realizzata 30g 43.5 (sotto); DVOL al 58° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 58.1 contro realizzata 30g 69.1 (sotto); DVOL al 48° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). - `[vol]` ETH: implicita 58.1 contro realizzata 30g 69.1 (sotto); DVOL al 48° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
+22
View File
@@ -790,6 +790,28 @@ SORGENTE** di `cron_daily.sh` — ogni chiamata a `journal.py` deve avere `--gio
perche' `journal.py` **senza argomenti scrive OGGI**. La guardia **non vieta** di rigenerare perche' `journal.py` **senza argomenti scrive OGGI**. La guardia **non vieta** di rigenerare
spesso: vieta di scrivere la pagina **una volta e lasciarla li'**. spesso: vieta di scrivere la pagina **una volta e lasciarla li'**.
🚨 **RIPARATO (2026-08-26): il P&L di giornale contava i VERSAMENTI come profitto.**
Il «giorno» era un delta di equity fra letture: la voce del 25/08 dichiarava **+$1.414,57 di
P&L** quando $1.399,39 erano il deposito USDC dell'operatore, e il «cumulato dall'arming»
avrebbe mentito per sempre (e al prossimo versamento da $3.000 avrebbe stampato «+$3.000 di
giorno»). Riparazione in `src/live/journal.py::movimenti_capitale` con tre proprieta':
(1) la **soglia e' importata** da `book.EQUITY_JUMP_ALERT`, non ridichiarata (P1 — il rilevatore
di movimenti del progetto e' quello); (2) un salto e' `movimento` solo se supera di >2x il
massimo che il **mercato misurato** (feed certificato, tetto di leva da config) avrebbe potuto
produrre nell'intervallo fra le due letture; (3) altrimenti e' **`ambiguo`: dichiarato e NON
scorporato** (P12 — un crash vero a tutta leva non deve trasformarsi in "prelievo").
Scansione dell'intera storia reale: **1 evento, esattamente il deposito** (+209,6% contro un
massimo di mercato di ±0,22%), zero falsi positivi. Anche la regola `concentrazione` ora
lavora sul cumulato di **trading** (il 25/08 leggeva "il 100% del P&L viene dagli ultimi 7
giorni" su un P&L che era il bonifico). Due scelte di design da non perdere: le regole sui
movimenti stanno **sopra** l'early-return "nessun giro di book" (un versamento in un giorno
senza giri esiste lo stesso: viene dal DB equity, non dal log del libro), e il **limite e'
dichiarato** (D5): un movimento sotto soglia — es. $150 su un conto da $2.000 — non si
distingue dal mercato e resta nel P&L, come nel rilevatore live. 7 prove nuove in
`test_journal.py` (31 → 37), incluso il controllo M15 al contrario: un crash 14% a tutta
leva resta AMBIGUO. Voce del 25/08 **rigenerata**: giorno +$1.414,57 di equity → trading
**+$15,18**; cumulato +$1.458,53 → trading **+$59,14**.
⚠️ **Trovato girando la suite completa, NON riparato — e' un'altra cosa:** ⚠️ **Trovato girando la suite completa, NON riparato — e' un'altra cosa:**
`tests/test_wave_0726.py::test_t1_canonical_riproduce_il_backtest_ufficiale` **fallisce**, e `tests/test_wave_0726.py::test_t1_canonical_riproduce_il_backtest_ufficiale` **fallisce**, e
falliva gia' prima di questa modifica (verificato con `git stash`: stesso fallimento sull'albero falliva gia' prima di questa modifica (verificato con `git stash`: stesso fallimento sull'albero
+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()}) 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: 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() g = giorno.isoformat()
eq = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc LIKE ? ORDER BY ts_utc", eq = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc LIKE ? ORDER BY ts_utc",
(f"{g}%",)).fetchall() (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() 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() 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() 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( return dict(
equity_inizio=e_ini, equity_fine=e_fin, equity_inizio=e_ini, equity_fine=e_fin, delta_equity=delta,
delta_equity=(e_fin - e_ini) if (e_ini is not None and e_fin is not None) else None,
realizzato_lordo=sum(r["pnl_lordo"] for r in rt) if rt else 0.0, 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, realizzato_netto=sum(r["pnl_netto"] for r in rt) if rt else 0.0,
roundtrip_chiusi=len(rt), fill=len(fills), roundtrip_chiusi=len(rt), fill=len(fills),
fee=sum(f["fee"] for f in fills) if fills else 0.0, 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, cumulato_da_arming=cum, letture_equity=len(eq),
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: def salute(con, giorno: date) -> dict:
@@ -201,7 +284,25 @@ def lettura(con, voce: dict) -> list[dict]:
def dire(rid, testo, livello="info"): def dire(rid, testo, livello="info"):
out.append(dict(id=rid, testo=testo, livello=livello)) 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 ----------------------------------------------------------- # --- 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: if "errore" in lb:
dire("stato", f"Nessun giro di `book_execute`: {lb['errore']}. " dire("stato", f"Nessun giro di `book_execute`: {lb['errore']}. "
"Non e' «il libro era fermo», e' «non e' stato misurato».", "attenzione") "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 ------------------------------------------------------ # --- P&L del giorno, in proporzione ------------------------------------------------------
dq, fee = p.get("delta_equity"), p.get("fee", 0.0) dq, fee = p.get("delta_equity"), p.get("fee", 0.0)
if dq is not None: dq_t = p.get("trading_giorno")
if p["fill"] and fee > 0 and abs(dq) < fee: dq_eff = dq_t if dq_t is not None else dq
dire("fee", f"Il movimento di equity del giorno (${dq:+.2f}) e' **piu' piccolo delle " scorporo = bool(p.get("mov_giorno"))
f"fee pagate** (${fee:.4f}): la giornata l'ha decisa il costo, non il segnale.", if dq_eff is not None:
"nota") 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: elif p["fill"] == 0:
dire("pnl", f"Equity ${dq:+.2f} **senza operare**: e' mark-to-market sulle posizioni " dire("pnl", f"Equity ${dq_eff:+.2f} **senza operare**: e' mark-to-market sulle "
"gia' aperte.") "posizioni gia' aperte."
+ (" (Al netto dei movimenti di capitale.)" if scorporo else ""))
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['roundtrip_chiusi']} round-trip chiusi (realizzato "
f"${p['realizzato_netto']:+.2f} netto).") 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() (voce["giorno"] + "T23:59:59+00:00",)).fetchall()
if len(eq) > 24: if len(eq) > 24:
e0, e1 = eq[0]["equity"], eq[-1]["equity"] 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) picco = max(r["equity"] for r in eq)
g7 = [r for r in eq if r["ts_utc"][:10] >= (date.fromisoformat(voce["giorno"]) - g7 = [r for r in eq if r["ts_utc"][:10] >= g7s]
timedelta(days=7)).isoformat()] 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 # 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. # 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) rilevante = abs(tot) >= max(5.0, 0.01 * e1)
if rilevante and g7: if rilevante and g7:
quota = (e1 - g7[0]["equity"]) / tot quota = (e1 - g7[0]["equity"] - mv7) / tot
if quota > 0.60: if quota > 0.60:
coda = ("Un risultato concentrato in una finestra non e' un tasso di rendimento: " coda = ("Un risultato concentrato in una finestra non e' un tasso di rendimento: "
"e' un evento.") "e' un evento.")
@@ -424,12 +536,24 @@ def rendi_markdown(con, voce: dict) -> str:
sk += " @ {:,.1f}".format(v["skh_entry"]) sk += " @ {:,.1f}".format(v["skh_entry"])
L.append(f"| **{a}** | {v['tp_frac']:+.3f} | {sk} | ${v['target']:+,.0f} | " L.append(f"| **{a}** | {v['tp_frac']:+.3f} | {sk} | ${v['target']:+,.0f} | "
f"${v['posizione']:+,.0f} | {v['azione']} |") 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", "", L += ["", "## P&L", "",
f"- giorno: **{_n(p.get('delta_equity'),'${:+,.2f}')}** di equity " riga_g,
f"({p['letture_equity']} letture)",
f"- realizzato: {p['realizzato_netto']:+.2f} netto su {p['roundtrip_chiusi']} round-trip " f"- realizzato: {p['realizzato_netto']:+.2f} netto su {p['roundtrip_chiusi']} round-trip "
f"chiusi · {p['fill']} fill · fee {p['fee']:.4f}", f"chiusi · {p['fill']} fill · fee {p['fee']:.4f}",
f"- cumulato dall'arming: **{_n(p.get('cumulato_da_arming'),'${:+,.2f}')}**", "", riga_c, "",
"## Salute", "", "## Salute", "",
f"- giri di `book_execute`: {s['giri_book']}/{s['atteso_giri']}" f"- giri di `book_execute`: {s['giri_book']}/{s['atteso_giri']}"
+ (" *(giorno in corso)*" if s.get("giorno_in_corso") else "") + (" *(giorno in corso)*" if s.get("giorno_in_corso") else "")
+109
View File
@@ -358,3 +358,112 @@ def test_il_cron_non_apre_piu_la_voce_del_giorno_in_corso():
assert chiamate, "il giornale non gira piu' nel cron: e' un'altra rottura, non questa" assert chiamate, "il giornale non gira piu' nel cron: e' un'altra rottura, non questa"
for r in chiamate: for r in chiamate:
assert "--giorno" in r, f"apre un giorno implicito (= oggi, parziale): {r}" assert "--giorno" in r, f"apre un giorno implicito (= oggi, parziale): {r}"
# ------------------------------------------------- movimenti di capitale nel P&L
# Il difetto (2026-08-25): la voce dichiarava «giorno: +$1.414,57» quando $1.399 erano un
# deposito USDC — il P&L era un delta di equity e ogni versamento ci finiva dentro come
# profitto, per sempre (il cumulato dall'arming non si sarebbe mai piu' corretto).
def _px_piatto(livello=100.0):
ix = pd.date_range("2026-08-20", "2026-08-27", freq="1h", tz="UTC")
return pd.Series(livello, index=ix)
def _serie_equity(con, punti):
for ts, eq in punti:
con.execute("INSERT INTO equity (ts_utc, equity, src) VALUES (?,?,?)", (ts, eq, "test"))
con.commit()
def test_un_versamento_viene_rilevato_e_scorporato(db, monkeypatch):
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
_serie_equity(db, [("2026-08-25T09:47:01+00:00", 667.0),
("2026-08-25T10:47:01+00:00", 667.5),
("2026-08-25T11:47:01+00:00", 2066.5),
("2026-08-25T12:47:01+00:00", 2064.0)])
p = J.pnl_giorno(db, date(2026, 8, 25))
ev = p["movimenti_giorno"]
assert len(ev) == 1 and ev[0]["classe"] == "movimento"
assert p["mov_giorno"] == pytest.approx(1399.0)
# il delta resta il FATTO, il trading e' il netto
assert p["delta_equity"] == pytest.approx(2064.0 - 667.0)
assert p["trading_giorno"] == pytest.approx(p["delta_equity"] - 1399.0)
assert p["trading_da_arming"] == pytest.approx(p["cumulato_da_arming"] - 1399.0)
def test_il_markdown_scorpora_e_lo_dice(db, monkeypatch):
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
_serie_equity(db, [("2026-08-25T10:47:01+00:00", 667.0),
("2026-08-25T11:47:01+00:00", 2066.0)])
voce = _voce(pnl=J.pnl_giorno(db, date(2026, 8, 25)), giorno="2026-08-25")
md = J.rendi_markdown(db, voce)
assert "movimenti di capitale" in md and "trading" in md
righe = J.lettura(db, voce)
assert "movimento" in _ids(righe)
def test_un_crash_a_tutta_leva_resta_AMBIGUO_e_non_diventa_prelievo(db, monkeypatch):
# controllo positivo al contrario (M15): il mercato fa -15% nell'ora, il libro al tetto
# di leva (1.0x) puo' spiegare il -14% di equity -> NON e' un prelievo, e' dichiarato.
ix = pd.date_range("2026-08-20", "2026-08-27", freq="1h", tz="UTC")
px = pd.Series(100.0, index=ix)
px[ix >= "2026-08-25T11:00:00+00:00"] = 85.0
monkeypatch.setattr(J, "_px_1h", lambda a: px)
_serie_equity(db, [("2026-08-25T10:47:01+00:00", 600.0),
("2026-08-25T11:47:01+00:00", 516.0)])
p = J.pnl_giorno(db, date(2026, 8, 25))
assert p["movimenti_giorno"][0]["classe"] == "ambiguo"
assert p["mov_giorno"] == 0.0 # niente scorporo
assert p["trading_giorno"] == pytest.approx(p["delta_equity"])
righe = J.lettura(db, _voce(pnl=p, giorno="2026-08-25"))
amb = [r for r in righe if r["id"] == "movimento_ambiguo"]
assert amb and amb[0]["livello"] == "attenzione"
def test_sotto_soglia_niente_eventi_e_trading_uguale_al_delta(db, monkeypatch):
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
_serie_equity(db, [("2026-08-25T10:47:01+00:00", 600.0),
("2026-08-25T11:47:01+00:00", 612.0)]) # +2%: sotto EQUITY_JUMP_ALERT
p = J.pnl_giorno(db, date(2026, 8, 25))
assert p["movimenti_giorno"] == [] and p["mov_giorno"] == 0.0
assert p["trading_giorno"] == pytest.approx(p["delta_equity"])
md = J.rendi_markdown(db, _voce(pnl=p, giorno="2026-08-25"))
assert "movimenti di capitale" not in md
def test_la_soglia_e_DERIVATA_dal_book_non_riscritta(db, monkeypatch):
# P1: se il rilevatore del book cambia soglia, il giornale la segue senza essere toccato.
from src.live import book as B
monkeypatch.setattr(B, "EQUITY_JUMP_ALERT", 0.50)
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
_serie_equity(db, [("2026-08-25T10:47:01+00:00", 600.0),
("2026-08-25T11:47:01+00:00", 780.0)]) # +30%: sotto la soglia alzata
p = J.pnl_giorno(db, date(2026, 8, 25))
assert p["movimenti_giorno"] == []
def test_mercato_non_misurabile_e_ambiguo_dichiarato_non_movimento(db, monkeypatch):
# P5: "non vedo" non e' "va tutto bene" — senza feed il salto NON si attribuisce.
monkeypatch.setattr(J, "_px_1h", lambda a: None)
_serie_equity(db, [("2026-08-25T10:47:01+00:00", 667.0),
("2026-08-25T11:47:01+00:00", 2066.0)])
p = J.pnl_giorno(db, date(2026, 8, 25))
e = p["movimenti_giorno"][0]
assert e["classe"] == "ambiguo" and e["mercato_max_pct"] is None
righe = J.lettura(db, _voce(pnl=p, giorno="2026-08-25"))
assert any(r["id"] == "movimento_ambiguo" and "non e' misurabile" in r["testo"] for r in righe)
def test_la_concentrazione_non_conta_i_versamenti_come_pnl(db, monkeypatch):
# 30 giorni piatti a 600, poi un versamento da 1400: senza scorporo la regola direbbe
# "tutto il P&L viene dagli ultimi 7 giorni" su un P&L che non esiste.
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
punti = [(f"2026-07-{g:02d}T12:00:00+00:00", 600.0) for g in range(1, 32)]
punti += [(f"2026-08-{g:02d}T12:00:00+00:00", 600.0) for g in range(1, 25)]
punti += [("2026-08-25T11:47:01+00:00", 2000.0)]
_serie_equity(db, punti)
p = J.pnl_giorno(db, date(2026, 8, 25))
righe = J.lettura(db, _voce(pnl=p, giorno="2026-08-25"))
conc = [r for r in righe if r["id"] == "concentrazione"]
assert conc == [] # il cumulato di TRADING e' 0: la regola deve tacere