From 993d17556a12ec57c422ab458d0de8b879090349 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 29 Aug 2026 06:45:47 +0000 Subject: [PATCH] =?UTF-8?q?usde:=20gate=20CHIUSO=20IDONEO,=20quota=20rinvi?= =?UTF-8?q?ata=20a=20lunedi'=20=E2=80=94=20l'APR=20si=20legge=20col=20suo?= =?UTF-8?q?=20n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Il 2026-08-28 12:35Z `usde_watch` ha rilevato il primo reward (+0.052055 USDE su 500), e la regola pre-registrata il 26/08 PRIMA dell'esito ha chiuso il gate USDE-01 su IDONEO. Si apre la decisione di QUOTA, che e' dell'operatore. DECISIONE DELL'OPERATORE (29/08): la quota si decide lunedi' 31/08, su tre-quattro finestre invece che su una. Il motivo e' un numero: sul solo pagamento il tasso implicito e' 3,80% annuo, su DUE finestre — il 27/08 aveva pagato ZERO — e' 1,90%. Fra i due c'e' tutta la decisione, e il campione e' UN pagamento. `rendimento()`: APR sulle finestre OSSERVATE, col denominatore sul TEMPO VERO e non sul numero di finestre pagate — cosi' una finestra che non paga ABBASSA la stima invece di sparire (P5: quel silenzio e' uno zero). Le letture con trade nel mezzo si escludono, non si riparano in silenzio (P12). L'APR si stampa sempre col suo `n`, e sotto 4 finestre la riga dice «un pagamento non e' un tasso». Sulle letture vere: 1,97% annuo su 1,9 giorni, 1/2 finestre pagate. `quota_da_decidere()`: vera SOLO se IDONEO E il rinvio e' scaduto. Da lunedi' il watch manda il 📌 OGNI giorno — quota attuale, tetto di allerta, APR osservato — finche' non si decide (N9: una decisione rinviata senza promemoria e' rinviata per sempre). Stesso schema della soglia $15k di GTAA01, con la data al posto del capitale. Si smette togliendo `DECISIONE_QUOTA_DAL`. 6 test nuovi, incluso il controllo positivo — «la finestra sola darebbe il doppio» — che e' esattamente la ragione per cui l'APR non si cita senza il suo n. CONFERMATO il fix IB di ieri: il giro delle 00:30 ha 0 righe `orders request timed out` (erano 2 per giro, 63 su 63) e tutti e sei gli ETF scaricati. E una conferma involontaria del debito §5.13: SPY e' passato da 1996-09-04 a 1996-09-05 — la finestra rotolante ha perso un altro giorno in una notte, come misurato. Diario: docs/diary/2026-08-29-usde-idoneo-e-ib.md Suite: 795 passati, 0 falliti. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 6 +- docs/diary/2026-08-29-usde-idoneo-e-ib.md | 67 ++++++++++++++++++++++ docs/journal/2026-08-28.md | 58 +++++++++++++++++++ scripts/live/usde_watch.py | 70 +++++++++++++++++++++++ tests/test_usde_watch.py | 64 +++++++++++++++++++++ 5 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 docs/diary/2026-08-29-usde-idoneo-e-ib.md create mode 100644 docs/journal/2026-08-28.md diff --git a/CLAUDE.md b/CLAUDE.md index 4442d92..5c47afe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,8 +68,8 @@ lorda massima 1,0x**, e nessun livello di capitale cambia il profilo di rischio 2 giorni (blocca) · `skh_feed_max_age_min` 30 (allerta, **non** blocca) · alert Telegram. 📌 **Dal 2026-08-26 l'equity del book e' il TOTALE cross-collateral** (USDC + USDE valutato -all'indice pubblico `usde_usdc`): il conto tiene **500 USDE** (test di eligibilita' reward, verdetto -29/08 — GATE USDE-01 in §4). Autorita' dei parametri: sezione `usde` di `config/live.json` +all'indice pubblico `usde_usdc`): il conto tiene **500 USDE** (**IDONEO** dal 28/08, primo reward ++0,052055 USDE; quota rinviata a lunedi' 31/08 — GATE USDE-01 in §4). Autorita' dei parametri: sezione `usde` di `config/live.json` (haircut 10%, tetto ALLERTA quota 50%, soglie depeg 0,99/0,95 coi criteri in `_nota_usde`), letta da `src/live/usde.py` — shadow (sizing orario) e `usde_watch` (sorveglianza 12:35 UTC, cron proprio, sotto `monitor_health`) derivano entrambi da li'. ⚠️ Il difetto d'origine: `shadow._equity` @@ -171,7 +171,7 @@ sull'hold-out; cambiarne le soglie guardando l'esito, pure. | gate | data | criterio | stato | |---|---|---|---| -| **USDE-01 (eligibilita' reward)** | 2026-08-29 | ≥1 reward entro il 29/08 (rilevati da `usde_watch` per delta di equity USDE al netto dei trade spot) → IDONEO e si apre la decisione di QUOTA (dell'operatore; tetto allerta 50%, mai 100% — R1 emittente non recuperabile); zero → si riconverte e la pista si chiude | ✅ pre-registrato il 26/08 PRIMA dell'esito (diario `2026-08-26-usde-analisi`); 500 USDE in conto dalle 13:04Z, baseline watch 14:17Z, verdetto applicato dal cron 12:35 | +| **USDE-01 (eligibilita' reward)** | 2026-08-29 | ≥1 reward entro il 29/08 → IDONEO e si apre la decisione di QUOTA (dell'operatore; tetto allerta 50%, mai 100% — R1 emittente non recuperabile); zero → si riconverte e la pista si chiude | ✅ **CHIUSO IDONEO** il 2026-08-28 12:35Z: primo reward **+0,052055 USDE** rilevato per delta al netto dei trade, coi criteri pre-registrati il 26/08 PRIMA dell'esito. 📌 **La QUOTA e' RINVIATA a lunedi' 31/08 su decisione dell'operatore** (29/08): su UNA finestra il tasso implicito e' **3,80%** annuo, su DUE e' **1,90%** — il 27/08 aveva pagato **zero** — e la differenza e' tutta la decisione. `usde_watch.rendimento()` stampa l'APR **col suo n** e da lunedi' `quota_da_decidere()` lo chiede ogni giorno finche' non si decide | | **STATARB** | 2026-09-27 | soglie 24/07 (non toccate) + diagnostica statica sempre-short | ✅ monitor riparato e rigenerato il 26/08: la serie vera da' Sharpe **−1,61** a oggi → coi criteri invariati punta al **RITIRO**. Si legge il 27/09, non prima | | **XSR01** | 2026-10-23 | Sharpe ≥1,0 **E** haircut ≤40% — dal 26/08 il numero decisivo e' `r0826_xsr_haircut.py` a **pavimento $10**, citato con la frazione di ordini eseguiti — poi `weights_tilt_null` e **capitale ≥$20k** | ✅ **riscritto il 26/08 su decisione dell'operatore** (dichiarato prima dell'esito, stringe: diario `2026-08-26-xsr01-gate-riscritto`): capitale allineato a "100% Deribit fino a $20k"; serie forward rigenerata (31 barre vere, Sharpe −2,72). Sotto la lente RENDITA il sleeve resta **eguagliato da un conto remunerato** (📌 sotto) | | **DVOLSPREAD** | kill 2026-10-24 se Sh<−0,50; decisione 2027-01-24 | (a) Sharpe>0 (b) marginale ADDS+robust_oos+insample_edge (c) **DSR ≥0,95** (d) `weights_tilt_null`; **veto** se barre attive <80% → si estende, non si decide | ✅ serie rigenerata il 26/08 (−4,41 a oggi, era −14,73 sulla serie rotta); il veto barre-attive ora conta barre VERE e la guardia PREMATURO sorveglia il monitor | diff --git a/docs/diary/2026-08-29-usde-idoneo-e-ib.md b/docs/diary/2026-08-29-usde-idoneo-e-ib.md new file mode 100644 index 0000000..afe3ec2 --- /dev/null +++ b/docs/diary/2026-08-29-usde-idoneo-e-ib.md @@ -0,0 +1,67 @@ +# 2026-08-29 — USDE idoneo, la quota rinviata a lunedì, e il fix IB confermato + +*Scritto il 2026-08-29. Numeri letti dalle serie citate.* + +## 1. GATE USDE-01: CHIUSO **IDONEO** + +Il 2026-08-28 alle 12:35Z `usde_watch` ha rilevato il primo reward: + +``` +equity USDE : 500.0521 @ 1.0001 (indice) -> $500.05 +delta : +0.052055 USDE dall'ultima lettura +verdetto : IDONEO — reward rilevato il 2026-08-28T12:35:01Z +``` + +La regola era pre-registrata il 26/08, **prima** dell'esito: ≥1 reward entro il 29/08 → IDONEO, +e si apre la decisione di QUOTA, che è dell'operatore. Il gate ha fatto quello che doveva. + +## 2. Il tasso, e perché la quota è stata rinviata + +| lettura | tasso implicito | +|---|---| +| il solo pagamento (0,052055 su 500) | **3,80% annuo** | +| su **due** finestre — il 27/08 ha pagato **zero** | **1,90% annuo** | +| annunciato da Deribit (ricerca 26/08) | 9% | + +**Decisione dell'operatore (29/08): la quota si decide lunedì 31/08**, su tre-quattro finestre +invece che su una. Il motivo è nella tabella: fra 3,80% e 1,90% c'è tutta la decisione, e il +campione è **un pagamento**. Il costo dell'attesa è zero — i 500 USDE rendono comunque. + +📌 Perché il numero non venga letto male: `usde_watch.rendimento()` calcola l'APR sulle finestre +**osservate**, col denominatore sul **tempo vero** e non sul numero di finestre pagate — così una +finestra che non paga **abbassa** la stima invece di sparire (P5: quel silenzio è uno zero). Le +letture con trade nel mezzo si escludono (P12). L'APR si stampa sempre **col suo `n`**, e sotto +4 finestre la riga dice *«un pagamento non è un tasso»*. Sulle letture vere di oggi: +**1,97% annuo su 1,9 giorni, 1/2 finestre pagate**. + +📌 Perché la decisione non si dimentichi (N9): `quota_da_decidere()` è vera **solo** se il conto è +IDONEO **e** il rinvio è scaduto. Da lunedì il watch manda il 📌 **ogni giorno** — con quota +attuale, tetto di allerta e APR osservato accanto — finché non si decide. Si smette togliendo +`DECISIONE_QUOTA_DAL`. È lo stesso schema della soglia $15k di GTAA01, con la data al posto +del capitale. + +⚠️ Nota di contorno che vale una lezione: il 📌 dell'IDONEO era partito su Telegram il 28/08 alle +12:35Z, **in mezzo ai messaggi-spazzatura dei test** delle 14:47 e 14:52 (difetto riparato lo +stesso giorno). L'unico messaggio che contava è arrivato nel giorno in cui il canale era sporco. +È P9 in forma concreta. + +## 3. Il fix IB è confermato + +Il giro del cron delle 00:30 di stanotte: **zero** righe `open orders request timed out` (erano +2 per giro, 63 giri su 63) e tutti e sei gli ETF scaricati — `SPY n=7541`, `QQQ n=6912`, +`IWM n=6602`, `TLT n=2658`, `GLD n=5476`, `HYG n=4877`, aggiornati al 28/08. Il `readonly=True` +resta. + +📌 E una conferma involontaria del debito §5.13: **SPY è passato da `1996-09-04` a `1996-09-05`**. +La finestra rotolante ha perso un altro giorno di borsa in una notte, esattamente come misurato +il 28/08 — il difetto si legge a occhio nudo nel log del cron. + +## Cosa cambia + +- `scripts/live/usde_watch.py`: `rendimento()` (APR sulle finestre osservate, col suo n) · + `quota_da_decidere()` · `DECISIONE_QUOTA_DAL = "2026-08-31"` · il 📌 giornaliero da lunedì. +- `tests/test_usde_watch.py`: 6 test nuovi, incluso il controllo positivo (*la finestra sola + darebbe il doppio*) che è la ragione per cui l'APR si stampa col suo `n`. +- `CLAUDE.md`: §1 e §4 aggiornati — gate CHIUSO IDONEO, quota rinviata a lunedì. + +**Suite: 795 passati, 0 falliti.** diff --git a/docs/journal/2026-08-28.md b/docs/journal/2026-08-28.md new file mode 100644 index 0000000..1669946 --- /dev/null +++ b/docs/journal/2026-08-28.md @@ -0,0 +1,58 @@ +# Giornale di bordo — 2026-08-28 + +*Scritto 2026-08-29T00:36:42+00:00. Numeri misurati; nessuna interpretazione automatica.* + +## Mercato + +| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL | +|---|---|---|---|---|---|---|---| +| **BTC** | $77,850.00 | -3.01% | -0.64% | +21.77% | 44.1% | ↑ ↑ ↑ (3/3 su) | 38.3 (25° pctl 1a) | +| **ETH** | $2,443.35 | -2.72% | -2.93% | +27.98% | 68.9% | ↑ ↑ ↑ (3/3 su) | 50.9 (16° pctl 1a) | + +## Libro + +Equity **$2,052.36** · nozionale lordo $571 · leva lorda 0.28x · barra dati 2026-08-28 · 24 giri + +| | TP01 | SKH01 | target | posizione | azione | +|---|---|---|---|---|---| +| **BTC** | +0.464 | flat | $+357 | $+357 | HOLD (a target) | +| **ETH** | +0.278 | flat | $+214 | $+214 | HOLD (a target) | + +## P&L + +- giorno: **$-17.87** di equity (24 letture) +- realizzato: +0.00 netto su 0 round-trip chiusi · 2 fill · fee 0.0054 +- cumulato dall'arming: **$+1,454.30** di equity — di cui $+1,399.39 versati/prelevati -> trading **$+54.91** + +## Salute + +- giri di `book_execute`: 24/24 +- eta' feed SKH all'ultimo giro: 0 min + +## Lettura + +*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* + +- `[stato]` A mercato su BTC $+357 (TP01 +0.464, SKH01 flat) e ETH $+214 (TP01 +0.278, SKH01 flat). +- `[leva]` Leva lorda **0.28x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.72x. +- `[pnl]` Equity $-17.87 con 2 fill e 0 round-trip chiusi (realizzato $+0.00 netto). +- `[drawdown]` 📌 Equity **-1.02%** sotto il picco ($2,073.59). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[vol]` BTC: implicita 38.3 contro realizzata 30g 44.1 (sotto); DVOL al 25° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[vol]` ETH: implicita 50.9 contro realizzata 30g 68.9 (sotto); DVOL al 16° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). +- `[evidenza]` 📌 Campione a oggi: **66 giorni, 23 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi. + +## Analisi (agente) + +*Scritta da `claude-opus-5` il 2026-08-29T00:37:39+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.* + +Prima giornata in rosso dal versamento: dopo +8.86 e +4.78, l'equity fa $-17.87 con zero round-trip chiusi. Il libro non ha deciso niente — BTC $+357 e ETH $+214 restano a target, dove erano ieri ($+361 e $+212) — quindi la perdita e' marcatura di mercato sul giorno a -3.01% BTC e -2.72% ETH, non esecuzione; e con SKH01 flat su entrambi e' esposizione interamente TP01. I 2 fill con fee 0.0054 sono aggiustamenti: se siano ritocchi al target o altro, non e' distinguibile qui. Salute pulita (24/24 giri, feed a 0 min), quindi non spiega nulla della giornata. + +Cio' che e' cambiato davvero e' la scala. Dal 25/08 l'equity e' $2,052.36, e il movimento di un solo giorno ($-17.87) e' dello stesso ordine dei $+54.91 di trading accumulati dall'arming in 66 giorni. Il P&L giornaliero e' ora piu' rumoroso della cosa che dovrebbe misurare, mentre i round-trip restano 23. + +Due letture reggono ugualmente sugli stessi numeri: TSMOM 3/3 su entrambi con 30 giorni a +21.77% e +27.98% dice trend intatto e ritracciamento; i 7 giorni (-0.64% e -2.93%) dicono che la spinta si era gia' fermata e oggi se ne vede la parte visibile. Con questi dati non si sceglie. + +Merita un'occhiata umana il picco $2,073.59: non compare fra le chiusure giornaliere, e il -1.02% e' misurato contro quella lettura oraria. + +## Nota + +*(vuota — campo dell'operatore)* diff --git a/scripts/live/usde_watch.py b/scripts/live/usde_watch.py index 3948b67..edcf06e 100755 --- a/scripts/live/usde_watch.py +++ b/scripts/live/usde_watch.py @@ -58,6 +58,14 @@ EPS_REWARD = 1e-3 # USDE: sotto e' rumore di lettura; il reward atteso a $500 FILL_TS = "2026-08-26T13:04:32Z" # conversione eseguita: 500 USDE @ 1.0003 VERDETTO_DOPO = "2026-08-29T12:30:00Z" # ultima finestra reward del 29/08 conclusa +# La QUOTA e' dell'operatore, e il 2026-08-29 e' stata RINVIATA a lunedi' 31/08 per decidere su +# piu' di una finestra invece che su una (il 27/08 aveva pagato ZERO, il 28/08 +0.052 USDE: fra +# le due letture ci sono 3,80% e 1,90% annuo, cioe' tutta la differenza che conta). N9: una +# decisione rinviata senza un promemoria e' rinviata per sempre — da lunedi' il watch lo dice +# ogni giorno finche' la riga non viene tolta. +DECISIONE_QUOTA_DAL = "2026-08-31" # lunedi': si decide la quota +EPS_GIORNI = 0.5 # sotto mezza giornata l'APR non si annualizza + def _parse(ts: str) -> datetime: return datetime.fromisoformat(ts.replace("Z", "+00:00")) @@ -102,6 +110,46 @@ def verdetto(records: list[dict], now: datetime) -> tuple[str, str]: return "IN_ATTESA", f"finestre reward ~12:00 UTC, verdetto dopo {VERDETTO_DOPO}" +def rendimento(records: list[dict]) -> dict: + """PURA. Il rendimento sulle finestre OSSERVATE, col loro NUMERO accanto. + + E' il numero che serve alla decisione di quota, e l'unico modo onesto di darlo e' con `n` + attaccato: un pagamento non e' un tasso. Il denominatore e' il TEMPO VERO fra la prima e + l'ultima lettura misurabile, non il conteggio delle finestre — cosi' una finestra che non + paga abbassa la stima invece di sparire (P5: il silenzio in una serie di ritorni si legge + come zero, e qui e' proprio uno zero). + + Si escludono le letture con trade nel mezzo (`con_trade`): quel delta non e' solo reward, e + una riparazione silenziosa sarebbe un'invenzione (P12). + """ + usabili = [r for r in records + if r.get("reward_stimato") is not None and not r.get("con_trade")] + pagate = [r for r in usabili if r.get("reward_rilevato")] + if not usabili: + return dict(apr=None, finestre=0, pagate=0, giorni=0.0, tot=0.0, + nota="nessuna finestra misurabile") + tot = float(sum(r["reward_stimato"] for r in usabili)) + eq = [float(r["eq_usde"]) for r in usabili if r.get("eq_usde")] + eq_med = sum(eq) / len(eq) if eq else 0.0 + # tempo vero coperto: dalla lettura PRIMA della prima misurabile, all'ultima + i0 = records.index(usabili[0]) + t0 = records[max(0, i0 - 1)]["ts"] + giorni = (usabili[-1]["ts"] - t0) / 86_400_000.0 + if giorni < EPS_GIORNI or eq_med <= 0: + return dict(apr=None, finestre=len(usabili), pagate=len(pagate), giorni=giorni, tot=tot, + nota=f"solo {giorni:.2f} giorni osservati: non si annualizza") + apr = tot / eq_med / giorni * 365.0 + return dict(apr=apr, finestre=len(usabili), pagate=len(pagate), giorni=giorni, tot=tot, + nota=f"{len(pagate)}/{len(usabili)} finestre hanno pagato") + + +def quota_da_decidere(now: datetime, verdetto_corrente: str) -> bool: + """PURA. La decisione di quota e' dovuta? Solo se il conto e' IDONEO e il rinvio e' scaduto.""" + if verdetto_corrente != "IDONEO": + return False + return now.date().isoformat() >= DECISIONE_QUOTA_DAL + + def condizioni(rec: dict | None, c: dict) -> set[str]: """PURA. Condizioni di allerta attive su un record.""" if not rec: @@ -229,6 +277,20 @@ def main() -> int: notify("⚠️ USDE: conto NON idoneo", {"dettaglio": v_motivo}) inviati.append(f"VERDETTO->{v}") + # La QUOTA e' dell'operatore ed e' stata rinviata a lunedi': da quel giorno lo si dice OGNI + # giorno, non solo alla transizione. Ripetersi qui e' voluto — non e' un allarme che chiede + # un'azione impossibile (P14), e' una domanda che da lunedi' si puo' finalmente rispondere, + # con accanto il numero per rispondere. Si smette togliendo `DECISIONE_QUOTA_DAL`. + rend = rendimento(records + [rec]) + if quota_da_decidere(now, v): + apr = "n/d" if rend["apr"] is None else f"{rend['apr']*100:.2f}% annuo" + notify("📌 USDE: la decisione di QUOTA e' dovuta", { + "quota attuale": f"{quota:.1%}" if quota is not None else "n/d", + "tetto di allerta": f"{c['quota_max_frac']:.0%} (mai 100%: R1 emittente non recuperabile)", + "rendimento osservato": f"{apr} — {rend['nota']}, {rend['giorni']:.1f} giorni", + "nota": "un pagamento non e' un tasso: leggere l'APR con il suo n"}) + inviati.append("QUOTA_DA_DECIDERE") + cambiato = bool(inviati) or v != v_prev if not quiet or cambiato: print("=" * 78) @@ -249,6 +311,14 @@ def main() -> int: + (" [con trade nel mezzo]" if an["con_trade"] else "")) print(f" delta : {an['delta']:+.6f} USDE dall'ultima lettura -> {attr}") print(f" verdetto : {v} — {v_motivo}") + if rend["finestre"]: + apr = "n/d" if rend["apr"] is None else f"{rend['apr']*100:.2f}% annuo" + print(f" rendimento : {apr} su {rend['giorni']:.1f} giorni — {rend['nota']}" + f" (tot {rend['tot']:+.6f} USDE)") + if rend["finestre"] < 4: + print(f" ⚠️ n={rend['finestre']}: un pagamento non e' un tasso") + if v == "IDONEO" and not quota_da_decidere(now, v): + print(f" quota : decisione RINVIATA a {DECISIONE_QUOTA_DAL} (operatore)") if inviati: print(f" allarmi : {', '.join(inviati)}") return 0 diff --git a/tests/test_usde_watch.py b/tests/test_usde_watch.py index f401d34..945eac2 100644 --- a/tests/test_usde_watch.py +++ b/tests/test_usde_watch.py @@ -141,3 +141,67 @@ def test_leggi_jsonl_roundtrip(tmp_path): + json.dumps({"ts": 2, "eq_usde": 500.05}) + "\n") rows = W.leggi(p) assert len(rows) == 2 and rows[-1]["eq_usde"] == 500.05 + + +# ============================================================================================= +# IL RENDIMENTO OSSERVATO, E LA DECISIONE DI QUOTA RINVIATA +# ============================================================================================= +# Il 2026-08-29 il gate USDE-01 si e' chiuso IDONEO (primo reward il 28/08, +0.052055 USDE) e la +# decisione di QUOTA e' stata rinviata dall'operatore a lunedi' 31/08: su UNA finestra il tasso +# implicito e' 3,80% annuo, su DUE e' 1,90% — cioe' tutta la differenza che conta. + +def _lettura(ts_ms, eq, reward=None, con_trade=False): + return dict(ts=ts_ms, eq_usde=eq, reward_stimato=reward, + reward_rilevato=bool(reward and reward > 1e-3), con_trade=con_trade) + + +G = 86_400_000 + + +def test_una_finestra_che_NON_paga_abbassa_la_stima_invece_di_sparire(): + """P5: il silenzio in una serie di ritorni si legge come zero — e qui e' proprio uno zero. + Il denominatore e' il TEMPO, non il numero di finestre pagate.""" + recs = [_lettura(0, 500.0), _lettura(G, 500.0, 0.0), _lettura(2 * G, 500.052055, 0.052055)] + r = W.rendimento(recs) + assert r["finestre"] == 2 and r["pagate"] == 1 + # 0.052055 su 500 in 2 giorni -> ~1.90%, non il 3.80% della sola finestra che ha pagato + assert 0.018 < r["apr"] < 0.021, r + + +def test_la_finestra_sola_darebbe_il_doppio(): + """Il controllo positivo: se si guardasse solo il pagamento, il numero raddoppierebbe. + E' la ragione per cui l'APR si stampa sempre con il suo n.""" + r = W.rendimento([_lettura(G, 500.0), _lettura(2 * G, 500.052055, 0.052055)]) + assert 0.037 < r["apr"] < 0.040, r + + +def test_le_letture_con_trade_nel_mezzo_si_escludono(): + """P12: quel delta non e' solo reward, e ripararlo in silenzio sarebbe un'invenzione.""" + recs = [_lettura(0, 500.0), _lettura(G, 600.0, 100.0, con_trade=True), _lettura(2 * G, 600.05, 0.05)] + r = W.rendimento(recs) + assert r["finestre"] == 1 and r["tot"] == 0.05 + + +def test_troppo_poco_tempo_non_si_annualizza(): + """Un'ora di osservazione moltiplicata per 365 e' un numero, non una misura.""" + r = W.rendimento([_lettura(0, 500.0), _lettura(3_600_000, 500.05, 0.05)]) + assert r["apr"] is None and "non si annualizza" in r["nota"] + + +def test_nessuna_finestra_misurabile_non_e_zero(): + r = W.rendimento([_lettura(0, 500.0)]) + assert r["apr"] is None and r["finestre"] == 0 + + +def test_la_quota_si_decide_da_lunedi_e_solo_se_IDONEO(): + from datetime import datetime, timezone + + def q(giorno, v): + return W.quota_da_decidere(datetime.fromisoformat(giorno + "T12:35:00+00:00"), v) + + assert W.DECISIONE_QUOTA_DAL == "2026-08-31" + assert q("2026-08-30", "IDONEO") is False, "il rinvio dell'operatore non e' rispettato" + assert q("2026-08-31", "IDONEO") is True, "lunedi' la domanda non viene posta" + assert q("2026-09-30", "IDONEO") is True, "smette di chiedere: la decisione si dimentica" + assert q("2026-08-31", "IN_ATTESA") is False, "chiede la quota su un conto non idoneo" + assert q("2026-08-31", "NON_IDONEO") is False