debito 14: il report stampa il TWR (+10,61%), non il bonifico (+243%)

`trades_db.py --report` calcolava e1/e0-1 sulla serie grezza di equity: il 96,3% del
numero era il versamento di $1.399,39 del 25/08. La riparazione (journal.movimenti_capitale)
esisteva e non aveva attraversato il confine fra i due lettori della stessa serie (P1).

- src/live/journal.py: `rendimento_twr` — funzione unica, spezza la serie sui movimenti
  CERTI e moltiplica i segmenti; gli ambigui restano dentro, dichiarati (P12); tre stati.
- scripts/live/trades_db.py: report() la chiama; stampa TWR con segmenti datati, movimenti
  elencati, trading al netto, delta $ etichettato "movimenti INCLUSI"; il % grezzo sparisce.
- test: +5 in test_journal.py (incl. riproduzione del +10,80% del diario 01/09, M23),
  +2 in test_trades_report.py sul testo stampato con connect() deviato in tmp. 832 verdi.
- docs: CLAUDE.md §5.14 chiuso, §2 e §13 aggiornati; memoria 40; diario 02/09.

Limite ereditato e dichiarato (D5): +10% di trading fra due letture consecutive tocca la
soglia del rilevatore e a mercato fermo verrebbe classificato movimento.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RziUCB336YPUUyDJ29x4Ke
This commit is contained in:
Adriano Dal Pastro
2026-09-02 14:22:50 +00:00
parent bc43218a12
commit 2f701b8469
7 changed files with 282 additions and 21 deletions
+16 -18
View File
@@ -115,7 +115,7 @@ sono all'ancora fortunata; le stime oneste sono la **mediana della banda d'ancor
| P(≥50 €/g), canale funded, 36 mesi | 42% | **2,6%** [1,5 4,7], P(zero) 40,3% | | P(≥50 €/g), canale funded, 36 mesi | 42% | **2,6%** [1,5 4,7], P(zero) 40,3% |
| XSR01 | Sharpe 1,82 (e' una **terza** lente, divisore fisso 50) | **1,79** alla scoperta / **1,56-1,63** a oggi — lente dei gate, sole barre chiuse. Citare sempre la coppia (lente, ultima barra chiusa) | | XSR01 | Sharpe 1,82 (e' una **terza** lente, divisore fisso 50) | **1,79** alla scoperta / **1,56-1,63** a oggi — lente dei gate, sole barre chiuse. Citare sempre la coppia (lente, ultima barra chiusa) |
| soffitto direzionale BTC/ETH | ~1,3 | **~1,15** col funding dentro | | soffitto direzionale BTC/ETH | ~1,3 | **~1,15** col funding dentro |
| performance del libro LIVE | «+243%» di equity (e' **96,3% un bonifico**) | **TWR +10,80%** spezzato sul versamento — +11,61% fino al 25/08, 0,73% dopo (01/09 16:47Z) | | performance del libro LIVE | «+243%» di equity (e' **96,3% un bonifico**) | **TWR +10,61%** spezzato sul versamento — +11,61% fino al 25/08, 0,89% dopo (02/09 14:02Z). Dal 02/09 lo stampa `trades_db.py --report` (debito 14 chiuso) |
📌 **Il libro a k=1 rende MENO dell'S&P 500** (15,19% contro 17,40%, stessa finestra): il vantaggio 📌 **Il libro a k=1 rende MENO dell'S&P 500** (15,19% contro 17,40%, stessa finestra): il vantaggio
sta nello **Sharpe** (1,35 vs 0,89), e **senza leva non si converte in rendimento**. A iso-rischio sta nello **Sharpe** (1,35 vs 0,89), e **senza leva non si converte in rendimento**. A iso-rischio
@@ -329,21 +329,19 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per
banda dichiarata [1,81-2,12]. QQQ e IWM arriveranno allo stesso muro **fra 2,5 e 3,7 anni**. banda dichiarata [1,81-2,12]. QQQ e IWM arriveranno allo stesso muro **fra 2,5 e 3,7 anni**.
Riparazione (ancorare la fase al calendario) rinviata con la decisione su GTAA01: cambierebbe Riparazione (ancorare la fase al calendario) rinviata con la decisione su GTAA01: cambierebbe
**tutti** i numeri registrati dello sleeve. **tutti** i numeri registrati dello sleeve.
14. ⚠️ **`trades_db.py --report` stampa un rendimento che e' per il 96,3% un bonifico** — e la 14. **RIPARATO (2026-09-02).** `trades_db.py --report` stampava `e1/e0-1` sulla serie grezza —
riparazione ESISTE gia' nel repo, non ha attraversato il confine (misurato 2026-09-01, diario «**+243%**», per il **96,3% un bonifico** (il versamento di $1.399,39 del 25/08 11:47Z; misurato
`2026-09-01-stato-trades.md`). La riga e' `$598,06 -> $2.051,84 (+1.453,78, **+243,08%**)`, il 01/09, diario `2026-09-01-stato-trades.md`). La riparazione esisteva gia' nel giornale
stampata accanto a `netto +40,77`: la serie di 1.657 letture orarie contiene **un solo salto**, (`journal.movimenti_capitale`) e **non aveva attraversato il confine fra i due lettori della
il **versamento di $1.399,39** del 25/08 11:47Z. Il classificatore ufficiale del progetto lo stessa serie** — variante di P1. Ora **`journal.rendimento_twr`** e' la funzione che entrambi
conferma (`journal.movimenti_capitale`: `certi 1399.39 · ambigui 0.0`, classe `movimento`). chiamano: spezza la serie sui movimenti CERTI (gli ambigui restano dentro, dichiarati) e il
Al netto: **TWR +10,80%**, equity +$54,39 in 69 giorni — e il giornale, che lo scorporo lo fa, report stampa **TWR +10,61% = +11,61% × 0,89%**, `trading da arming +$50,95`, i movimenti
concorda ($+62,00 al 31/08, 7,61 di marcatura fino a oggi). ⚠️ **`journal.py:121` porta il elencati, e il delta $ grezzo etichettato «movimenti INCLUSI» — il `%` grezzo **non compare piu'**.
difetto scritto nel docstring col suo caso d'origine** (la voce del 25/08 che dichiarava Test: 5 in `test_journal.py` (fra cui la riproduzione del +10,80% del diario 01/09, M23) + 2 in
«+$1.414,57» di giornata); `trades_db.py:83` calcola `100*(e1/e0-1)` sulla serie grezza e non `test_trades_report.py` sul testo stampato, con `connect()` deviato in tmp. Diario
chiama `movimenti_capitale()`, che e' a un import di distanza. **Variante di P1**: non un `2026-09-02-debito-14-report-twr.md`. ⚠️ **Limite ereditato e dichiarato (D5):** un +10% di
sorvegliante che ridichiara il bersaglio, ma una **riparazione che non si e' propagata al trading fra due letture consecutive tocca la soglia del rilevatore e, a mercato fermo, verrebbe
secondo lettore della stessa serie**. Danno sui soldi **nessuno** (sola lettura); danno di classificato «movimento» — e' lo stesso limite del rilevatore live, non un difetto nuovo.
citazione **si'** — ed e' l'unico numero fuorviante che il progetto produce su richiesta di un
comando pubblicato in §13. Riparazione non fatta: sta su uno script che legge il libro vivo.
--- ---
@@ -558,11 +556,11 @@ uv run python scripts/analysis/rebuild_history.py --asset BTC ETH # storico da
uv run python scripts/analysis/certify_feed.py [--local] # certifica i feed uv run python scripts/analysis/certify_feed.py [--local] # certifica i feed
uv run python scripts/portfolio/run_portfolio.py # report del portafoglio (ricerca) uv run python scripts/portfolio/run_portfolio.py # report del portafoglio (ricerca)
uv run python scripts/live/paper_portfolio.py # avanza il paper (forward-only) uv run python scripts/live/paper_portfolio.py # avanza il paper (forward-only)
uv run python scripts/live/trades_db.py --report # stato libro di bordo + P&L uv run python scripts/live/trades_db.py --report # stato libro di bordo + P&L (TWR, non e1/e0)
uv run python scripts/live/trades_db.py --reconcile # incrocio delle 3 fonti sui fill uv run python scripts/live/trades_db.py --reconcile # incrocio delle 3 fonti sui fill
uv run python scripts/live/journal.py # voce del giorno (numeri + lettura) uv run python scripts/live/journal.py # voce del giorno (numeri + lettura)
uv run python scripts/live/analista.py --secco # analisi del giorno, senza salvare uv run python scripts/live/analista.py --secco # analisi del giorno, senza salvare
uv run pytest # test (775, tutti verdi dal 26/08) uv run pytest # test (832, tutti verdi al 02/09)
``` ```
```python ```python
@@ -0,0 +1,53 @@
# 2026-09-02 — Debito 14: il report stampa il TWR, non il bonifico
*Scritto il 2026-09-02 fra le 14:16Z e le 14:30Z. Numeri dalla serie `equity` di `trades.db`
(1.678 letture) e dal report rilanciato dopo la riparazione.*
## Cosa c'era
`trades_db.py --report` calcolava `100*(e1/e0-1)` sulla serie grezza di equity e lo stampava come
performance: **+243%**. La serie contiene un solo salto, il versamento di $1.399,39 del 25/08
11:47Z: il 96,3% di quel numero era un bonifico. Il classificatore che lo scorpora esisteva già
(`journal.movimenti_capitale`, dal 25/08) e non era mai arrivato al secondo lettore della stessa
serie. Trovato e registrato il 01/09 (`2026-09-01-stato-trades.md`), non riparato perché lo script
legge il libro vivo.
## Cosa ho fatto
| pezzo | dove | cosa |
|---|---|---|
| funzione unica | `src/live/journal.py``rendimento_twr(con, fino_ts)` | chiama `movimenti_capitale`, spezza la serie all'equity PRIMA e DOPO ogni movimento **certo**, moltiplica i segmenti. Gli **ambigui non spezzano** (P12): restano nel rendimento, dichiarati. Tre stati: numero, o `None` con `motivo` |
| il report | `scripts/live/trades_db.py``report()` | chiama `rendimento_twr` (P1: non rifà il conto). Stampa TWR con i segmenti datati, i movimenti elencati con la classe, `trading da arming`, e il delta $ grezzo etichettato «movimenti di capitale INCLUSI». Il `%` grezzo non compare più |
| test | `tests/test_journal.py` (+5) · `tests/test_trades_report.py` (+2) | versamento spezzato e grezzo no · senza movimenti = rendimento semplice · ambiguo non spezza · serie vuota → `None` con motivo · **riproduzione del +10,80% del diario 01/09** (M23) · il testo stampato contiene TWR e non il grezzo · `connect()` nudo deviato in tmp (conftest), verificato nel test |
## Il report, prima e dopo
```
prima: equity : $598.06 -> $2,051.84 (+1453.78, +243.08%)
dopo: equity : $598.06 -> $2,048.40 (+1,450.34 di equity, movimenti di capitale INCLUSI) | picco $2,073.59 | 1678 letture
movimenti capitale : +1,399.39 certi | +0.00 ambiguo/i (restano nel P&L, dichiarati)
2026-08-25T11:47 +1,399.39 [movimento]
trading da arming : +50.95 (equity al netto dei movimenti certi)
TWR : +10.61% = +11.61% [2026-06-23 -> 2026-08-25] x -0.89% [2026-08-25 -> 2026-09-02]
```
Il giornale del giorno concorda: «cumulato dall'arming $+1.450,34 — di cui $+1.399,39 versati ->
trading **$+50,95**». Stessa base, stesso versamento, stessa funzione.
## Una cosa imparata riparando
Quattro test sono nati rossi con scenari a **+10% di trading** fra due letture: la soglia del
rilevatore (`book.EQUITY_JUMP_ALERT`) è esattamente 0,10, e a mercato piatto un +10% supera «2× il
massimo che il mercato poteva fare» (che è zero) — viene classificato **movimento**. Non è un difetto
della riparazione: è il limite già dichiarato del rilevatore live (D5, nel docstring di
`movimenti_capitale`). Nella serie vera non morde — fra due letture orarie consecutive il trading non
fa +10% — ma un test che lo tocca lo rende visibile. I test ora usano +5%, e il limite è scritto
nel test, nel CLAUDE.md e qui.
## Cosa NON ho fatto
- Il giornale (`pnl_giorno`, pagina markdown) **non stampa il TWR**: stampa già lo scorporo in
dollari e la sua pagina ha una forma testata. Aggiungerlo è una riga, ma non era il debito.
- Non ho scomposto lo scarto (~$25) fra round-trip + non realizzato e il trading al netto: era già
fuori scope il 01/09 e lo resta.
+14
View File
@@ -692,6 +692,20 @@ e i vincoli di deploy (PRIIPs/UCITS/broker).
📌 **IL DB** (`data/live/trades.db`, dentro il perimetro del backup): `fills` col contesto del 📌 **IL DB** (`data/live/trades.db`, dentro il perimetro del backup): `fills` col contesto del
segnale a quel giro (tp_frac, skh_sign, entry, target, posizione, equity), `roundtrips` segnale a quel giro (tp_frac, skh_sign, entry, target, posizione, equity), `roundtrips`
**derivati e ricalcolati da zero**, `equity` oraria, `journal`. Sync **orario** in `cron_book`. **derivati e ricalcolati da zero**, `equity` oraria, `journal`. Sync **orario** in `cron_book`.
📌 **IL RENDIMENTO DEL LIBRO E' UN TWR, NON `e1/e0-1`** (debito 14, trovato il 01/09 e chiuso il
02/09/2026). `--report` stampava «+243%» su una serie che conteneva **un solo salto**, il
versamento di $1.399,39 del 25/08: il 96,3% del numero era un bonifico. La riparazione
(`movimenti_capitale`, che classifica ogni salto oltre `EQUITY_JUMP_ALERT` come *movimento* se
supera 2× il massimo che il mercato misurato poteva fare a tetto di leva, altrimenti *ambiguo*)
viveva gia' nel giornale e **non era arrivata al secondo lettore della stessa serie** — una
variante di P1 che non e' un sorvegliante che ridichiara, ma una riparazione che non si propaga.
Ora `journal.rendimento_twr` e' la funzione unica: spezza la serie all'equity PRIMA e DOPO ogni
movimento certo e moltiplica i segmenti (gli ambigui non spezzano: restano nel rendimento,
dichiarati accanto — P12). Il report stampa TWR, segmenti con le date, movimenti elencati,
trading al netto, e il delta $ grezzo solo etichettato «movimenti INCLUSI». Verifica M23: la
macchina riproduce il +10,80% del diario 01/09 sui suoi quattro punti. **Limite ereditato:** un
+10% di trading fra due letture orarie consecutive, a mercato fermo, sarebbe classificato
movimento — e' il limite dichiarato del rilevatore live (D5), non uno nuovo.
**Le tre fonti si INCROCIANO e non si sovrascrivono:** log (ora vera) x jsonl (i fill) x venue **Le tre fonti si INCROCIANO e non si sovrascrivono:** log (ora vera) x jsonl (i fill) x venue
(autorevole ma **TRONCA** — 1 trade su BTC, 0 su ETH). `reconcile()` riporta le divergenze e (autorevole ma **TRONCA** — 1 trade su BTC, 0 su ETH). `reconcile()` riporta le divergenze e
**non ripara niente da solo**: fra due fonti che non concordano, una riparazione silenziosa e' **non ripara niente da solo**: fra due fonti che non concordano, una riparazione silenziosa e'
+20 -3
View File
@@ -1,7 +1,7 @@
"""Sincronizza `data/live/trades.db` — il libro di bordo dei trade, allineato col tempo. """Sincronizza `data/live/trades.db` — il libro di bordo dei trade, allineato col tempo.
uv run python scripts/live/trades_db.py --sync # idempotente, da cron uv run python scripts/live/trades_db.py --sync # idempotente, da cron
uv run python scripts/live/trades_db.py --report # stato + P&L a video uv run python scripts/live/trades_db.py --report # stato + P&L a video (TWR, non e1/e0)
uv run python scripts/live/trades_db.py --reconcile # incrocio delle tre fonti uv run python scripts/live/trades_db.py --reconcile # incrocio delle tre fonti
SOLA LETTURA sul venue e sui log: non manda ordini, non tocca il libro. SOLA LETTURA sul venue e sui log: non manda ordini, non tocca il libro.
@@ -78,10 +78,27 @@ def report() -> None:
print(f"\n fill registrati : {n} dal {p['a'][:16]} al {p['b'][:16]}") print(f"\n fill registrati : {n} dal {p['a'][:16]} al {p['b'][:16]}")
eq = con.execute("SELECT ts_utc, equity FROM equity ORDER BY ts_utc").fetchall() eq = con.execute("SELECT ts_utc, equity FROM equity ORDER BY ts_utc").fetchall()
if eq: if eq:
# Il rendimento NON e' `e1/e0-1` sulla serie grezza: quel numero era per il 96,3% un
# bonifico (debito #14, 2026-09-01). Lo scorporo e' quello del giornale — si CHIAMA,
# non si rifa' (P1): `rendimento_twr` spezza la serie sui movimenti di capitale certi.
from src.live.journal import rendimento_twr
e0, e1 = eq[0]["equity"], eq[-1]["equity"] e0, e1 = eq[0]["equity"], eq[-1]["equity"]
picco = max(r["equity"] for r in eq) picco = max(r["equity"] for r in eq)
print(f" equity : ${e0:,.2f} -> ${e1:,.2f} ({e1 - e0:+.2f}, {100*(e1/e0-1):+.2f}%)" r = rendimento_twr(con, eq[-1]["ts_utc"])
f" | picco ${picco:,.2f} | {len(eq)} letture") print(f" equity : ${e0:,.2f} -> ${e1:,.2f} ({e1 - e0:+,.2f} di equity, "
f"movimenti di capitale INCLUSI) | picco ${picco:,.2f} | {len(eq)} letture")
if r["eventi"]:
print(f" movimenti capitale : {r['certi']:+,.2f} certi | {r['ambigui']:+,.2f} ambiguo/i"
f" (restano nel P&L, dichiarati)")
for ev in r["eventi"]:
print(f" {ev['ts_dopo'][:16]} {ev['delta']:+,.2f} [{ev['classe']}]")
print(f" trading da arming : {r['trading']:+,.2f} (equity al netto dei movimenti certi)")
if r["twr"] is None:
print(f" TWR : n/d ({r['motivo']})")
else:
segs = " x ".join(f"{100*s['ret']:+.2f}% [{s['da'][:10]} -> {s['a'][:10]}]"
for s in r["segmenti"])
print(f" TWR : {100*r['twr']:+.2f}% = {segs}")
rt = con.execute("SELECT * FROM roundtrips ORDER BY ts_out").fetchall() rt = con.execute("SELECT * FROM roundtrips ORDER BY ts_out").fetchall()
lordo = sum(r["pnl_lordo"] for r in rt) lordo = sum(r["pnl_lordo"] for r in rt)
fee_rt = sum(r["fee_quota"] for r in rt) fee_rt = sum(r["fee_quota"] for r in rt)
+44
View File
@@ -178,6 +178,50 @@ def movimenti_capitale(con, fino_ts: str) -> dict:
soglia=soglia, leva_tetto=leva_tetto) soglia=soglia, leva_tetto=leva_tetto)
def rendimento_twr(con, fino_ts: str) -> dict:
"""Rendimento pesato per il tempo (TWR) della serie di equity, spezzata sui movimenti CERTI.
PERCHE' ESISTE (debito #14, trovato il 2026-09-01). `e1/e0 - 1` sulla serie grezza era il
numero che `trades_db.py --report` stampava come performance: «+243%», per il 96,3% un
bonifico ($1.399 il 25/08 su $667). La riparazione `movimenti_capitale` viveva gia' in
questo modulo e NON aveva attraversato il confine fra i due lettori della stessa serie:
variante di P1. Da oggi il secondo lettore CHIAMA il primo invece di rifare il conto.
COME. Ogni evento di classe `movimento` chiude un segmento all'equity PRIMA del salto e ne
apre uno all'equity DOPO; il TWR e' il prodotto dei rendimenti di segmento, meno uno. Gli
eventi `ambiguo` NON spezzano niente (P12: restano nel rendimento, dichiarati accanto).
TRE STATI: `twr` e' un numero, oppure None con `motivo` (serie vuota, base zero).
"""
rows = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc <= ? ORDER BY ts_utc",
(fino_ts,)).fetchall()
if not rows:
return dict(twr=None, motivo="nessuna lettura di equity", segmenti=[], eventi=[],
certi=0.0, ambigui=0.0, delta_equity=None, trading=None, letture=0)
mv = movimenti_capitale(con, fino_ts)
certi = [e for e in mv["eventi"] if e["classe"] == "movimento"]
e0, e1 = float(rows[0]["equity"]), float(rows[-1]["equity"])
segmenti, da_ts, da_eq = [], rows[0]["ts_utc"], e0
for ev in certi: # gia' in ordine di tempo
if da_eq:
segmenti.append(dict(da=da_ts, a=ev["ts_prima"], e_da=da_eq, e_a=ev["prima"],
ret=ev["prima"] / da_eq - 1.0))
da_ts, da_eq = ev["ts_dopo"], ev["dopo"]
if da_eq:
segmenti.append(dict(da=da_ts, a=rows[-1]["ts_utc"], e_da=da_eq, e_a=e1,
ret=e1 / da_eq - 1.0))
if not segmenti or e0 <= 0:
return dict(twr=None, motivo="base di equity nulla", segmenti=segmenti, eventi=mv["eventi"],
certi=mv["certi"], ambigui=mv["ambigui"], delta_equity=e1 - e0,
trading=e1 - e0 - mv["certi"], letture=len(rows))
twr = 1.0
for sg in segmenti:
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))
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.
+67
View File
@@ -501,3 +501,70 @@ def test_ogni_soglia_dice_COSA_decidere_non_solo_che_e_arrivata():
"""P4: un'allerta risponde a due domande. Una riga che dice solo «sei a $15k» non serve.""" """P4: un'allerta risponde a due domande. Una riga che dice solo «sei a $15k» non serve."""
for _, testo in J.SOGLIE_CAPITALE: for _, testo in J.SOGLIE_CAPITALE:
assert len(testo) > 80 and "**" in testo, "la soglia non dice quale decisione sblocca" assert len(testo) > 80 and "**" in testo, "la soglia non dice quale decisione sblocca"
# ------------------------------------------------- TWR: il rendimento che non e' un bonifico
# Debito #14 (2026-09-01): `trades_db --report` stampava `e1/e0-1` sulla serie grezza, «+243%»
# per il 96,3% un versamento. La riparazione (`movimenti_capitale`) viveva qui e non aveva
# attraversato il confine fra i due lettori. `rendimento_twr` e' la funzione che ENTRAMBI chiamano.
def test_twr_spezza_sul_versamento_e_il_grezzo_no(db, monkeypatch):
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
# +5% di trading: SOTTO la soglia del rilevatore (0,10) — un +10% con mercato piatto
# sarebbe esso stesso un "movimento", ed e' il limite dichiarato (D5) del classificatore.
_serie_equity(db, [("2026-08-24T10:47:01+00:00", 600.0),
("2026-08-25T10:47:01+00:00", 630.0), # +5% di trading
("2026-08-25T11:47:01+00:00", 2030.0), # +1400: versamento
("2026-08-26T10:47:01+00:00", 2009.7)]) # -1% di trading
r = J.rendimento_twr(db, "2026-08-26T23:59:59+00:00")
assert r["twr"] == pytest.approx(1.05 * 0.99 - 1.0) # +3,95%
assert r["certi"] == pytest.approx(1400.0) and r["ambigui"] == 0.0
assert [round(s["ret"], 4) for s in r["segmenti"]] == [0.05, -0.01]
assert r["trading"] == pytest.approx(2009.7 - 600.0 - 1400.0) # +9,7
grezzo = 2009.7 / 600.0 - 1.0 # +235%: il numero sbagliato
assert abs(r["twr"] - grezzo) > 2.0
def test_twr_senza_movimenti_e_il_rendimento_semplice(db, monkeypatch):
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
_serie_equity(db, [("2026-08-24T10:47:01+00:00", 600.0),
("2026-08-25T10:47:01+00:00", 630.0)])
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
assert r["twr"] == pytest.approx(0.05) and len(r["segmenti"]) == 1
assert r["trading"] == pytest.approx(30.0) and r["eventi"] == []
def test_twr_un_ambiguo_non_spezza_e_resta_dichiarato(db, monkeypatch):
# stesso caso del test sul crash a tutta leva: -14% con il mercato a -15% -> ambiguo
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)])
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
assert r["twr"] == pytest.approx(-0.14) and len(r["segmenti"]) == 1
assert r["certi"] == 0.0 and r["ambigui"] == pytest.approx(-84.0)
assert r["trading"] == pytest.approx(-84.0) # niente scorporo
def test_twr_serie_vuota_e_None_con_motivo_non_zero(db):
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
assert r["twr"] is None and r["motivo"] and r["trading"] is None
def test_twr_riproduce_il_numero_del_diario_01_09(db, monkeypatch):
# M23: prima di pubblicare un numero nuovo, far riprodurre alla macchina quello vecchio.
# Diario 2026-09-01-stato-trades: 598,06 -> 667,49 | +1.399,39 | 2.066,88 -> 2.051,84 = +10,80%
ix = pd.date_range("2026-06-20", "2026-09-02", freq="1h", tz="UTC")
monkeypatch.setattr(J, "_px_1h", lambda a: pd.Series(100.0, index=ix))
# una lettura intermedia: nella serie vera ci sono ~1.500 letture orarie fra le due date,
# e nessun salto di trading fra due consecutive tocca la soglia del rilevatore
_serie_equity(db, [("2026-06-23T22:00:00+00:00", 598.06),
("2026-07-20T10:47:01+00:00", 630.00),
("2026-08-25T10:47:01+00:00", 667.49),
("2026-08-25T11:47:01+00:00", 2066.88),
("2026-09-01T16:47:01+00:00", 2051.84)])
r = J.rendimento_twr(db, "2026-09-01T23:59:59+00:00")
assert round(100 * r["twr"], 2) == 10.80
assert r["certi"] == pytest.approx(1399.39)
+68
View File
@@ -0,0 +1,68 @@
"""`trades_db.py --report` non stampa piu' un bonifico come rendimento (debito #14, 2026-09-02).
Il numero che stampava era `e1/e0-1` sulla serie grezza: «+243%» per il 96,3% un versamento.
Il report ora CHIAMA `journal.rendimento_twr` (P1: non rifa' il conto). `T.connect()` nudo
finisce in tmp grazie alla fixture autouse di conftest: qui non si tocca il libro vivo.
"""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
import pandas as pd
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from src.live import journal as J # noqa: E402
from src.live import tradesdb as T # noqa: E402
def _carica_script():
spec = importlib.util.spec_from_file_location("trades_db_script", ROOT / "scripts/live/trades_db.py")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
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 _prepara(con, equity):
con.execute("INSERT INTO fills (fill_id, ts_utc, ts_source, asset, side, qty, price, fee) "
"VALUES ('f1','2026-08-24T10:47:15+00:00','test','BTC','buy',0.001,60000.0,0.01)")
for ts, eq in equity:
con.execute("INSERT INTO equity (ts_utc, equity, src) VALUES (?,?,?)", (ts, eq, "test"))
con.commit()
def test_il_report_stampa_il_TWR_e_non_il_grezzo(monkeypatch, capsys):
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
con = T.connect() # -> tmp (conftest), NON data/live/trades.db
assert "data/live" not in str(con.execute("PRAGMA database_list").fetchone()[2])
_prepara(con, [("2026-08-24T10:47:01+00:00", 600.0),
("2026-08-25T10:47:01+00:00", 630.0), # +5%: sotto soglia rilevatore
("2026-08-25T11:47:01+00:00", 2030.0), # +1400: versamento
("2026-08-26T10:47:01+00:00", 2009.7)]) # -1%
con.close()
_carica_script().report()
out = capsys.readouterr().out
assert "TWR : +3.95%" in out
assert "+234.95%" not in out and "+235" not in out # il grezzo non compare piu'
assert "movimenti capitale : +1,400.00 certi" in out
assert "trading da arming : +9.70" in out
assert "movimenti di capitale INCLUSI" in out # il delta $ resta, etichettato
def test_il_report_senza_movimenti_dice_lo_stesso_numero_di_prima(monkeypatch, capsys):
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
con = T.connect()
_prepara(con, [("2026-08-24T10:47:01+00:00", 600.0), ("2026-08-25T10:47:01+00:00", 630.0)])
con.close()
_carica_script().report()
out = capsys.readouterr().out
assert "TWR : +5.00%" in out and "movimenti capitale" not in out