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:
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -20,9 +20,9 @@ Equity **$2,056.59** · nozionale lordo $557 · leva lorda 0.27x · barra dati 2
|
||||
|
||||
## 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
|
||||
- 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
|
||||
|
||||
@@ -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.*
|
||||
|
||||
- `[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).
|
||||
- `[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).
|
||||
- `[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.
|
||||
- `[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 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.
|
||||
- `[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).
|
||||
|
||||
@@ -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
|
||||
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:**
|
||||
`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
|
||||
|
||||
+144
-20
@@ -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 "")
|
||||
|
||||
@@ -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"
|
||||
for r in chiamate:
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user