From 835e0c8666374bd792a180130a8d18304d0215a4 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Wed, 2 Sep 2026 15:17:05 +0000 Subject: [PATCH] revisione 02/09: il rotolante si CONTROLLA ogni ora (non "si ri-ancora"), TWR con limiti dichiarati e tre stati MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quattro segnalazioni della revisione sui commit di oggi, tutte verificate e riparate. 1. book_execute.py (+ CLAUDE.md §5.7, memoria 40): «il disaster-SL rotolante si ri-ancora ogni ora» era falso — ensure_disaster_sl lascia il bracket finche' lo stop e' entro il 5% e la taglia entro il 10%; il giro orario CONTROLLA, ri-ancora oltre la tolleranza (mark +5,263%/-4,762%). Lo stop siede fra -26,3% e -33,3% dal mark corrente. 2. journal.rendimento_twr: limite dichiarato (D5) — l'intervallo che contiene un movimento certo esce intero dal rendimento, il suo P&L di mercato va in `certi`; errore massimo meta' del movimento per costruzione (25/08: ~$0,5). Non si stima (P12). Segmenti a lunghezza zero non prodotti; nessun tempo a mercato -> 0,0 dichiarato numero. 3. base di equity zero: `twr` E `trading` None con motivo (il salto 0->X e' invisibile al classificatore, `trading` valeva l'intero conto); il report stampa n/d. 4. test_book_cadenza: la guardia estrae ogni prescrizione «ogni/every N unita'» e pretende 60 minuti, con controllo positivo (M15); limite P13 dichiarato. Test +5 (847 verdi). Diari 02/09 e 02/09b con la sezione "Revisione". Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01RziUCB336YPUUyDJ29x4Ke --- CLAUDE.md | 10 +++--- docs/diary/2026-09-02-debito-14-report-twr.md | 10 ++++++ .../2026-09-02b-debito-7-cadenza-docstring.md | 7 ++++ docs/memory/40-produzione-e-deploy.md | 4 +-- scripts/live/book_execute.py | 7 ++-- scripts/live/trades_db.py | 6 +++- src/live/journal.py | 31 +++++++++++----- tests/test_book_cadenza.py | 35 +++++++++++++++++-- tests/test_journal.py | 33 +++++++++++++++++ tests/test_trades_report.py | 14 ++++++++ 10 files changed, 137 insertions(+), 20 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 09775c6..fbc8746 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -286,8 +286,10 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per mentre il cron gira **ogni ora** (`47 * * * *`): era il docstring a essere sbagliato, e chi lo avesse "corretto" nel verso del cron avrebbe spostato il libro sulla riga 4h, dove BTC **raddoppia** gli scatti del disaster-SL (`r0823_sl_anchor.py`). Ora il docstring dichiara - **CADENZA: ORARIA** con la ragione (giro idempotente: latenza SKH01 ≤1h, ri-ancoraggio del - rotolante ogni ora) e il divieto esplicito. **`tests/test_book_cadenza.py` (10 test)** tiene + **CADENZA: ORARIA** con la ragione (giro idempotente: latenza SKH01 ≤1h; il rotolante e' + **controllato** ogni ora e ri-ancorato solo oltre la tolleranza di `ensure_disaster_sl`, mark + +5,263%/−4,762% o taglia >10% — «si ri-ancora ogni ora» era una frase sbagliata, corretta in + revisione il 02/09) e il divieto esplicito. **`tests/test_book_cadenza.py` (10 test)** tiene d'accordo le tre dichiarazioni — docstring, riga in `cron_book.sh`, **crontab installata** (letta con `crontab -l`; dove non e' leggibile il test si dichiara SALTATO, non verde) — e verifica che la cadenza dichiarata sia 60 min, sotto la griglia 230m e lontana dai 240 della @@ -346,7 +348,7 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per 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 + `2026-09-02-debito-14-report-twr.md`. ⚠️ **Due limiti dichiarati dalla revisione del 02/09** (D5): (a) **l'intervallo che contiene un movimento certo esce INTERO dal rendimento** — il suo P&L di mercato finisce in `certi` e fuori da `trading`, con errore massimo pari a META' del movimento riconosciuto per costruzione del classificatore (il 25/08: ~$0,5 su $1.399); non si stima, si dichiara (P12); (b) **a base di equity zero `twr` E `trading` sono None**: il salto 0→X del primo versamento e' invisibile al classificatore e `trading` varrebbe l'intero conto. ⚠️ **Limite ereditato (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. @@ -567,7 +569,7 @@ uv run python scripts/live/trades_db.py --report # stato libro d 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 (842, tutti verdi al 02/09) +uv run pytest # test (847, 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 index d0e3efc..63691a4 100644 --- a/docs/diary/2026-09-02-debito-14-report-twr.md +++ b/docs/diary/2026-09-02-debito-14-report-twr.md @@ -51,3 +51,13 @@ nel test, nel CLAUDE.md e qui. 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. + +## Revisione del codice (15:20Z) — tre cose trovate, tutte riparate + +| trovato | vero? | riparazione | +|---|---|---| +| l'intervallo che contiene un movimento certo esce intero dal rendimento: il suo P&L di mercato finisce in `certi` e fuori da `trading`, e nessun documento lo diceva | sì, per costruzione. Errore massimo = metà del movimento riconosciuto (margine del classificatore). Il 25/08: ~$263 lordi a ±0,2% → ~$0,5 | dichiarato nel docstring, nel report, in CLAUDE.md §5.14 e in memoria 40. Non si stima (P12) | +| a base di equity zero `trading` era un numero mentre `twr` era None, e sbagliato: il salto 0→X è invisibile al classificatore, `trading` valeva l'intero conto sotto l'etichetta "al netto dei versamenti" | sì. Non raggiungibile dal cron (non scrive mai 0), ma raggiungibile da un backfill | tre stati veri: `twr` e `trading` entrambi None con motivo; il report stampa n/d. Test in `test_journal.py` e `test_trades_report.py` | +| movimento nel primo intervallo o due consecutivi → segmento `+0.00% [x -> x]` | sì, cosmetico | i segmenti a lunghezza zero non si producono; nessun tempo a mercato → TWR 0,0 dichiarato numero. Test | + +Il numero pubblicato non cambia: +10,61% alle 14:02Z, +10,56% alle 14:47Z per la marcatura. diff --git a/docs/diary/2026-09-02b-debito-7-cadenza-docstring.md b/docs/diary/2026-09-02b-debito-7-cadenza-docstring.md index aca1f5f..7fa3f1f 100644 --- a/docs/diary/2026-09-02b-debito-7-cadenza-docstring.md +++ b/docs/diary/2026-09-02b-debito-7-cadenza-docstring.md @@ -43,3 +43,10 @@ minuti) BTC **raddoppia** gli scatti del disaster-SL rotolante (2 contro 1; a 24 - Non ho toccato il cron né `cron_book.sh`: la configurazione che gira era quella giusta. - Non ho aggiunto un sorvegliante a runtime sulla cadenza (contare i giri per ora nel log): il giornale già stampa «giri mancanti» ogni giorno, e il test copre la deriva di configurazione. + +## Revisione del codice (15:20Z) — due cose trovate, tutte riparate + +| trovato | vero? | riparazione | +|---|---|---| +| il docstring nuovo diceva «il disaster-SL rotolante si ri-ancora ogni ora»: falso. `ensure_disaster_sl` lascia il bracket com'è finché lo stop voluto è entro il 5% da quello piazzato e la taglia entro il 10%; il giro orario **controlla**, ri-ancora oltre la tolleranza (mark +5,263% / −4,762%). I «BTC 0 / ETH 1» di `r0823` sono misurati **con** questa isteresi | sì, verificato in `execution.py:233-235`. Propagato anche a CLAUDE.md §5.7 e memoria 40 | riscritto in tutti e tre i posti: «controllato ogni ora, ri-ancorato solo oltre la tolleranza; lo stop siede fra −26,3% e −33,3% dal mark corrente». Frase sbagliata su un meccanismo di sicurezza vivo: era la segnalazione più importante | +| la guardia del test cercava due letterali («ogni ~230»): «ogni quattro ore» sarebbe passato | sì | la guardia estrae **ogni** prescrizione «ogni/every N unità» e pretende 60 minuti; controllo positivo (M15) su cinque frasi. Limite dichiarato (P13): una prosa che prescrive senza «ogni» passa | diff --git a/docs/memory/40-produzione-e-deploy.md b/docs/memory/40-produzione-e-deploy.md index 477ba35..364c9c7 100644 --- a/docs/memory/40-produzione-e-deploy.md +++ b/docs/memory/40-produzione-e-deploy.md @@ -703,7 +703,7 @@ e i vincoli di deploy (PRIIPs/UCITS/broker). 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 + macchina riproduce il +10,80% del diario 01/09 sui suoi quattro punti. **Due limiti trovati in revisione (02/09):** l'intervallo che contiene un movimento certo esce INTERO dal rendimento (il suo P&L di mercato va in `certi`; errore massimo META' del movimento, per costruzione; il 25/08 ~$0,5) e a base di equity zero `twr` e `trading` sono entrambi None, perche' il salto 0→X del primo versamento e' invisibile al classificatore. I segmenti a lunghezza zero (movimento nel primo intervallo, o due consecutivi) non si stampano. **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 @@ -923,7 +923,7 @@ rotolante è funzione della **cadenza del cron** (1h: BTC 0 / ETH 1 in 7-8 anni con 2 in 30 giorni), quindi chi avesse "corretto" il cron verso il docstring avrebbe spostato il libro sulla riga peggiore. La riparazione è il docstring stesso — cadenza ORARIA, la ragione (giro idempotente: girare più fitto della griglia non costa ordini, e compra latenza ≤1h per gli -ingressi/uscite software di SKH01 e un ri-ancoraggio orario del rotolante) e il divieto scritto — +ingressi/uscite software di SKH01 e un **controllo** orario del rotolante — che si ri-ancora solo oltre la tolleranza di `ensure_disaster_sl`, mark +5,263%/−4,762% o taglia >10%: la prima stesura diceva «si ri-ancora ogni ora» ed era sbagliata, corretta in revisione lo stesso giorno) e il divieto scritto — più `tests/test_book_cadenza.py`, che **deriva** (P1) le tre dichiarazioni e le confronta: la riga citata nel docstring, quella dichiarata nell'intestazione di `cron_book.sh` (`47 * * * *`) e la crontab **installata** (`crontab -l`; se non è leggibile il test è SALTATO, non verde — P5). Il diff --git a/scripts/live/book_execute.py b/scripts/live/book_execute.py index 1326297..2a69782 100644 --- a/scripts/live/book_execute.py +++ b/scripts/live/book_execute.py @@ -14,8 +14,11 @@ data/live/book_executions.jsonl. decide su griglia 230m, ma il giro e' IDEMPOTENTE (riconcilia al target netto corrente: se non cambia nulla -> HOLD), quindi girare piu' fitto della griglia non costa ordini e compra due cose misurate: (1) gli ingressi/uscite SOFTWARE di SKH01 arrivano con latenza <=1h invece che fino a -230m; (2) il disaster-SL rotolante (-30%, l'unico on-book) si ri-ancora ogni ora — a cadenza 1h in -7-8 anni BTC 0 scatti / ETH 1, sulla riga 4h BTC ne fa 2 (`r0823_sl_anchor.py`). +230m; (2) il disaster-SL rotolante (-30%, l'unico on-book) viene CONTROLLATO ogni ora e +ri-ancorato solo oltre la tolleranza di `ensure_disaster_sl` (stop a >5% da quello piazzato, cioe' +mark +5,263%/-4,762%, o taglia >10%) — con questa isteresi a cadenza 1h in 7-8 anni BTC 0 scatti / +ETH 1, sulla riga 4h BTC ne fa 2 (`r0823_sl_anchor.py`). Lo stop siede quindi fra -26,3% e -33,3% +dal mark corrente, non a -30% dall'ultima ora: e' rotolante, non un massimo di perdita (CLAUDE.md §1). 🚨 NON "correggere" la cadenza a ~230 minuti: fino al 2026-09-02 questo docstring lo prescriveva, ed era il docstring a essere sbagliato, non il cron (debito §5.7). `tests/test_book_cadenza.py` tiene d'accordo docstring, `cron_book.sh` e crontab installata. NON il cron giornaliero: le entrate diff --git a/scripts/live/trades_db.py b/scripts/live/trades_db.py index d384735..d7daa11 100644 --- a/scripts/live/trades_db.py +++ b/scripts/live/trades_db.py @@ -92,7 +92,11 @@ def report() -> None: 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["trading"] is None: + print(f" trading da arming : n/d ({r['motivo']})") + else: + print(f" trading da arming : {r['trading']:+,.2f} (equity al netto dei movimenti " + f"certi; l'ora del movimento esce intera, vedi journal.rendimento_twr)") if r["twr"] is None: print(f" TWR : n/d ({r['motivo']})") else: diff --git a/src/live/journal.py b/src/live/journal.py index 6f92907..7886edd 100644 --- a/src/live/journal.py +++ b/src/live/journal.py @@ -191,7 +191,19 @@ def rendimento_twr(con, fino_ts: str) -> dict: 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). + ⚠️ LIMITE DICHIARATO (D5). L'intervallo che CONTIENE un movimento certo esce INTERO dal + rendimento: il suo P&L di mercato (marcatura delle posizioni aperte, funding; in un buco di + lettura — fino a 10h nella serie vera — l'intero buco) finisce in `certi`, quindi anche fuori + da `trading`. E' limitato per costruzione dal margine del classificatore (|salto| > + 2·max_mkt·leva·equity), percio' l'errore massimo vale max_mkt·leva·equity: fino a META' del + movimento riconosciuto. Il 25/08: libro a ~$263 lordi, mercato a ±0,2% -> ~$0,5 su $1.399,39. + Non si stima e non si corregge (P12): si dichiara. Un intervallo di sola marcatura fra due + movimenti consecutivi, o prima del primo, ha lunghezza zero e NON produce un segmento. + + TRE STATI: `twr` e `trading` sono numeri, oppure None con `motivo`. Base di equity zero => + entrambi None: il salto 0 -> X del primo versamento e' INVISIBILE a `movimenti_capitale` + (una base zero non ha percentuale), quindi `certi` lo perderebbe e `trading` varrebbe + l'intero conto — un numero etichettato "al netto dei versamenti" che li contiene tutti. """ rows = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc <= ? ORDER BY ts_utc", (fino_ts,)).fetchall() @@ -199,23 +211,24 @@ def rendimento_twr(con, fino_ts: str) -> dict: 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"]) + if e0 <= 0: + return dict(twr=None, trading=None, + motivo="base di equity nulla: il primo versamento non e' classificabile", + segmenti=[], eventi=mv["eventi"], certi=mv["certi"], ambigui=mv["ambigui"], + delta_equity=e1 - e0, letture=len(rows)) + certi = [e for e in mv["eventi"] if e["classe"] == "movimento"] segmenti, da_ts, da_eq = [], rows[0]["ts_utc"], e0 for ev in certi: # gia' in ordine di tempo - if da_eq: + if da_eq and ev["ts_prima"] != da_ts: # lunghezza zero = niente segmento 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: + if da_eq and rows[-1]["ts_utc"] != da_ts: 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: + for sg in segmenti: # nessun segmento = nessun tempo a mercato = 0 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, diff --git a/tests/test_book_cadenza.py b/tests/test_book_cadenza.py index 6c38883..bd85a52 100644 --- a/tests/test_book_cadenza.py +++ b/tests/test_book_cadenza.py @@ -68,10 +68,41 @@ def _cron_installato() -> str | None: # ---------------------------------------------------------------- regressione sul docstring +# La guardia non cerca due letterali («ogni ~230»): estrae OGNI prescrizione di cadenza scritta +# come «ogni/every N unita'» e pretende che valga 60 minuti. ⚠️ Limite dichiarato (P13): una +# guardia sui numeri non copre il ragionamento — una prosa che prescrive una cadenza senza la +# parola «ogni» passa. Il controllo positivo qui sotto dice cosa la guardia VEDE. -def test_il_docstring_non_prescrive_piu_i_230_minuti(): +_NUMERI = {"un": 1, "una": 1, "due": 2, "tre": 3, "quattro": 4, "sei": 6, "otto": 8, "dodici": 12} +_RX_CADENZA = re.compile( + r"\b(?:ogni|every)\s*~?\s*(\d+|un'?|una|due|tre|quattro|sei|otto|dodici)?\s*'?\s*" + r"(minuti|minuto|minutes?|min|m|ore|ora|hours?|h)\b", re.I) + + +def prescrizioni_minuti(testo: str) -> list[int]: + """Ogni «ogni N unita'» del testo, in minuti. «ogni ora» -> 60, «ogni ~230 minuti» -> 230.""" + out = [] + for m in _RX_CADENZA.finditer(testo): + n_raw, unita = m.group(1), m.group(2).lower() + n = 1 if n_raw is None else (int(n_raw) if n_raw.isdigit() else _NUMERI[n_raw.rstrip("'")]) + out.append(n * (60 if unita.startswith(("or", "h")) else 1)) + return out + + +def test_la_guardia_vede_le_prescrizioni_che_deve_vedere(): + # controllo positivo (M15): i modi in cui il docstring potrebbe ri-prescrivere la riga 4h + assert prescrizioni_minuti("va lanciato ogni ~230 minuti con la feed") == [230] + assert prescrizioni_minuti("il giro va lanciato ogni quattro ore") == [240] + assert prescrizioni_minuti("every 4 hours, then every 230 min") == [240, 230] + assert prescrizioni_minuti("controllato ogni ora; ogni 60 minuti") == [60, 60] + assert prescrizioni_minuti("fino a 230m di latenza") == [] # non e' una prescrizione + + +def test_il_docstring_prescrive_solo_la_cadenza_oraria(): d = _docstring() - assert "ogni ~230" not in d and "ogni 230" not in d + p = prescrizioni_minuti(d) + assert p, "il docstring deve contenere almeno una prescrizione di cadenza" + assert all(x == 60 for x in p), f"prescrizioni non orarie nel docstring: {p}" assert re.search(r"CADENZA:\s*ORARIA", d), "il docstring deve dichiarare la cadenza ORARIA" diff --git a/tests/test_journal.py b/tests/test_journal.py index 52a92de..e19159b 100644 --- a/tests/test_journal.py +++ b/tests/test_journal.py @@ -568,3 +568,36 @@ def test_twr_riproduce_il_numero_del_diario_01_09(db, monkeypatch): 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) + + +def test_twr_a_base_zero_e_None_anche_sul_trading(db, monkeypatch): + # revisione 02/09: a base zero `movimenti_capitale` non vede il salto 0 -> X del primo + # versamento, quindi `trading = e1-e0-certi` varrebbe l'intero conto. Tre stati, non due. + monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto()) + _serie_equity(db, [("2026-08-24T09:47:01+00:00", 0.0), + ("2026-08-24T10:47:01+00:00", 598.06), + ("2026-08-25T10:47:01+00:00", 610.0)]) + r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00") + assert r["twr"] is None and r["trading"] is None and "base" in r["motivo"] + + +def test_twr_due_movimenti_consecutivi_non_producono_segmenti_a_lunghezza_zero(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-24T11:47:01+00:00", 2000.0), # movimento nel PRIMO intervallo + ("2026-08-24T12:47:01+00:00", 3400.0), # movimento consecutivo + ("2026-08-25T10:47:01+00:00", 3434.0)]) # +1% di trading + r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00") + assert len(r["segmenti"]) == 1 and r["segmenti"][0]["ret"] == pytest.approx(0.01) + assert r["twr"] == pytest.approx(0.01) + assert all(s["da"] != s["a"] for s in r["segmenti"]) + assert r["certi"] == pytest.approx(1400.0 + 1400.0) + + +def test_twr_senza_tempo_a_mercato_e_zero_non_None(db, monkeypatch): + # due letture, la seconda e' un versamento: nessun segmento, rendimento 0, dichiarato numero + monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto()) + _serie_equity(db, [("2026-08-24T10:47:01+00:00", 600.0), + ("2026-08-24T11:47:01+00:00", 2000.0)]) + r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00") + assert r["twr"] == 0.0 and r["segmenti"] == [] and r["trading"] == pytest.approx(0.0) diff --git a/tests/test_trades_report.py b/tests/test_trades_report.py index b7bc9a7..f84b110 100644 --- a/tests/test_trades_report.py +++ b/tests/test_trades_report.py @@ -66,3 +66,17 @@ def test_il_report_senza_movimenti_dice_lo_stesso_numero_di_prima(monkeypatch, c _carica_script().report() out = capsys.readouterr().out assert "TWR : +5.00%" in out and "movimenti capitale" not in out + + +def test_il_report_a_base_zero_dice_n_d_e_non_l_intero_conto(monkeypatch, capsys): + monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto()) + con = T.connect() + _prepara(con, [("2026-08-24T09:47:01+00:00", 0.0), + ("2026-08-24T10:47:01+00:00", 598.06), + ("2026-08-25T10:47:01+00:00", 610.0)]) + con.close() + _carica_script().report() + out = capsys.readouterr().out + assert "trading da arming : n/d" in out and "TWR : n/d" in out + assert "trading da arming : +610.00" not in out # l'intero conto NON e' "trading" + assert "(+610.00 di equity, movimenti di capitale INCLUSI)" in out # il fatto resta, etichettato