revisione 02/09: il rotolante si CONTROLLA ogni ora (non "si ri-ancora"), TWR con limiti dichiarati e tre stati

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RziUCB336YPUUyDJ29x4Ke
This commit is contained in:
Adriano Dal Pastro
2026-09-02 15:17:05 +00:00
parent 861cc7fc27
commit 835e0c8666
10 changed files with 137 additions and 20 deletions
+6 -4
View File
@@ -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 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 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 **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 **CADENZA: ORARIA** con la ragione (giro idempotente: latenza SKH01 ≤1h; il rotolante e'
rotolante ogni ora) e il divieto esplicito. **`tests/test_book_cadenza.py` (10 test)** tiene **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** 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 (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 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'**. 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: 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 `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 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. 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/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 (842, tutti verdi al 02/09) uv run pytest # test (847, tutti verdi al 02/09)
``` ```
```python ```python
@@ -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. 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à - 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. 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.
@@ -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 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 - 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. 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 |
+2 -2
View File
@@ -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, 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, 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 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 +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. 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
@@ -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 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 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 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 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 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 crontab **installata** (`crontab -l`; se non è leggibile il test è SALTATO, non verde — P5). Il
+5 -2
View File
@@ -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 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 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 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 230m; (2) il disaster-SL rotolante (-30%, l'unico on-book) viene CONTROLLATO ogni ora e
7-8 anni BTC 0 scatti / ETH 1, sulla riga 4h BTC ne fa 2 (`r0823_sl_anchor.py`). 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, 🚨 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` 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 tiene d'accordo docstring, `cron_book.sh` e crontab installata. NON il cron giornaliero: le entrate
+5 -1
View File
@@ -92,7 +92,11 @@ def report() -> None:
f" (restano nel P&L, dichiarati)") f" (restano nel P&L, dichiarati)")
for ev in r["eventi"]: for ev in r["eventi"]:
print(f" {ev['ts_dopo'][:16]} {ev['delta']:+,.2f} [{ev['classe']}]") 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: if r["twr"] is None:
print(f" TWR : n/d ({r['motivo']})") print(f" TWR : n/d ({r['motivo']})")
else: else:
+22 -9
View File
@@ -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 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). 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", rows = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc <= ? ORDER BY ts_utc",
(fino_ts,)).fetchall() (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=[], return dict(twr=None, motivo="nessuna lettura di equity", segmenti=[], eventi=[],
certi=0.0, ambigui=0.0, delta_equity=None, trading=None, letture=0) certi=0.0, ambigui=0.0, delta_equity=None, trading=None, letture=0)
mv = movimenti_capitale(con, fino_ts) 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"]) 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 segmenti, da_ts, da_eq = [], rows[0]["ts_utc"], e0
for ev in certi: # gia' in ordine di tempo 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"], 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)) ret=ev["prima"] / da_eq - 1.0))
da_ts, da_eq = ev["ts_dopo"], ev["dopo"] 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, segmenti.append(dict(da=da_ts, a=rows[-1]["ts_utc"], e_da=da_eq, e_a=e1,
ret=e1 / da_eq - 1.0)) 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 twr = 1.0
for sg in segmenti: for sg in segmenti: # nessun segmento = nessun tempo a mercato = 0
twr *= 1.0 + sg["ret"] twr *= 1.0 + sg["ret"]
return dict(twr=twr - 1.0, motivo=None, segmenti=segmenti, eventi=mv["eventi"], return dict(twr=twr - 1.0, motivo=None, segmenti=segmenti, eventi=mv["eventi"],
certi=mv["certi"], ambigui=mv["ambigui"], delta_equity=e1 - e0, certi=mv["certi"], ambigui=mv["ambigui"], delta_equity=e1 - e0,
+33 -2
View File
@@ -68,10 +68,41 @@ def _cron_installato() -> str | None:
# ---------------------------------------------------------------- regressione sul docstring # ---------------------------------------------------------------- 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() 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" assert re.search(r"CADENZA:\s*ORARIA", d), "il docstring deve dichiarare la cadenza ORARIA"
+33
View File
@@ -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") r = J.rendimento_twr(db, "2026-09-01T23:59:59+00:00")
assert round(100 * r["twr"], 2) == 10.80 assert round(100 * r["twr"], 2) == 10.80
assert r["certi"] == pytest.approx(1399.39) 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)
+14
View File
@@ -66,3 +66,17 @@ def test_il_report_senza_movimenti_dice_lo_stesso_numero_di_prima(monkeypatch, c
_carica_script().report() _carica_script().report()
out = capsys.readouterr().out out = capsys.readouterr().out
assert "TWR : +5.00%" in out and "movimenti capitale" not in 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