diff --git a/CLAUDE.md b/CLAUDE.md index 1c948e5..70bac81 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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% | | 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 | -| 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 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**. Riparazione (ancorare la fase al calendario) rinviata con la decisione su GTAA01: cambierebbe **tutti** i numeri registrati dello sleeve. -14. ⚠️ **`trades_db.py --report` stampa un rendimento che e' per il 96,3% un bonifico** — e la - riparazione ESISTE gia' nel repo, non ha attraversato il confine (misurato 2026-09-01, diario - `2026-09-01-stato-trades.md`). La riga e' `$598,06 -> $2.051,84 (+1.453,78, **+243,08%**)`, - stampata accanto a `netto +40,77`: la serie di 1.657 letture orarie contiene **un solo salto**, - il **versamento di $1.399,39** del 25/08 11:47Z. Il classificatore ufficiale del progetto lo - conferma (`journal.movimenti_capitale`: `certi 1399.39 · ambigui 0.0`, classe `movimento`). - Al netto: **TWR +10,80%**, equity +$54,39 in 69 giorni — e il giornale, che lo scorporo lo fa, - concorda ($+62,00 al 31/08, −7,61 di marcatura fino a oggi). ⚠️ **`journal.py:121` porta il - difetto scritto nel docstring col suo caso d'origine** (la voce del 25/08 che dichiarava - «+$1.414,57» di giornata); `trades_db.py:83` calcola `100*(e1/e0-1)` sulla serie grezza e non - chiama `movimenti_capitale()`, che e' a un import di distanza. **Variante di P1**: non un - sorvegliante che ridichiara il bersaglio, ma una **riparazione che non si e' propagata al - secondo lettore della stessa serie**. Danno sui soldi **nessuno** (sola lettura); danno di - 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. +14. ✅ **RIPARATO (2026-09-02).** `trades_db.py --report` stampava `e1/e0-1` sulla serie grezza — + «**+243%**», per il **96,3% un bonifico** (il versamento di $1.399,39 del 25/08 11:47Z; misurato + il 01/09, diario `2026-09-01-stato-trades.md`). La riparazione esisteva gia' nel giornale + (`journal.movimenti_capitale`) e **non aveva attraversato il confine fra i due lettori della + stessa serie** — variante di P1. Ora **`journal.rendimento_twr`** e' la funzione che entrambi + chiamano: spezza la serie sui movimenti CERTI (gli ambigui restano dentro, dichiarati) e il + report stampa **TWR +10,61% = +11,61% × −0,89%**, `trading da arming +$50,95`, i movimenti + elencati, e il delta $ grezzo etichettato «movimenti INCLUSI» — il `%` grezzo **non compare piu'**. + Test: 5 in `test_journal.py` (fra cui la riproduzione del +10,80% del diario 01/09, M23) + 2 in + `test_trades_report.py` sul testo stampato, con `connect()` deviato in tmp. Diario + `2026-09-02-debito-14-report-twr.md`. ⚠️ **Limite ereditato e dichiarato (D5):** un +10% di + trading fra due letture consecutive tocca la soglia del rilevatore e, a mercato fermo, verrebbe + classificato «movimento» — e' lo stesso limite del rilevatore live, non un difetto nuovo. --- @@ -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/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/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/journal.py # voce del giorno (numeri + lettura) 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 diff --git a/docs/diary/2026-09-02-debito-14-report-twr.md b/docs/diary/2026-09-02-debito-14-report-twr.md new file mode 100644 index 0000000..d0e3efc --- /dev/null +++ b/docs/diary/2026-09-02-debito-14-report-twr.md @@ -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. diff --git a/docs/memory/40-produzione-e-deploy.md b/docs/memory/40-produzione-e-deploy.md index 1c279b6..e410b95 100644 --- a/docs/memory/40-produzione-e-deploy.md +++ b/docs/memory/40-produzione-e-deploy.md @@ -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 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`. + 📌 **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 (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' diff --git a/scripts/live/trades_db.py b/scripts/live/trades_db.py index a896274..d384735 100644 --- a/scripts/live/trades_db.py +++ b/scripts/live/trades_db.py @@ -1,7 +1,7 @@ """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 --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 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]}") eq = con.execute("SELECT ts_utc, equity FROM equity ORDER BY ts_utc").fetchall() 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"] picco = max(r["equity"] for r in eq) - print(f" equity : ${e0:,.2f} -> ${e1:,.2f} ({e1 - e0:+.2f}, {100*(e1/e0-1):+.2f}%)" - f" | picco ${picco:,.2f} | {len(eq)} letture") + r = rendimento_twr(con, eq[-1]["ts_utc"]) + 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() lordo = sum(r["pnl_lordo"] for r in rt) fee_rt = sum(r["fee_quota"] for r in rt) diff --git a/src/live/journal.py b/src/live/journal.py index e19f4af..6f92907 100644 --- a/src/live/journal.py +++ b/src/live/journal.py @@ -178,6 +178,50 @@ def movimenti_capitale(con, fino_ts: str) -> dict: 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: """P&L del giorno. `equity` e' l'autorita' (venue); il resto e' scomposizione locale. diff --git a/tests/test_journal.py b/tests/test_journal.py index 8874ab6..52a92de 100644 --- a/tests/test_journal.py +++ b/tests/test_journal.py @@ -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.""" for _, testo in J.SOGLIE_CAPITALE: 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) diff --git a/tests/test_trades_report.py b/tests/test_trades_report.py new file mode 100644 index 0000000..b7bc9a7 --- /dev/null +++ b/tests/test_trades_report.py @@ -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