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:
@@ -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.
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user