From c250c04371f10efe0614f52cae2800e5bcee963d Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 30 Aug 2026 23:48:52 +0000 Subject: [PATCH] =?UTF-8?q?usde:=20l'USDC=20NON=20frutta=20sul=20nostro=20?= =?UTF-8?q?conto=20=E2=80=94=20misurato,=20e=20=C2=A76=20di=20stasera=20er?= =?UTF-8?q?a=20sbagliata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Domanda dell'operatore: "verifica se l'USDC frutta davvero sul nostro conto". Sembrava dover aspettare: il gateway non espone il Transaction Log (get_transaction_log, get_settlement_history, get_deposits, get_transfers, get_interest_history: tutti 404 — debito #11) e l'unica serie storica e' trades.db.equity, oraria ma arrotondata a 2 decimali e sporcata dal P&L non realizzato (+-$2-8/ora a posizioni aperte), dove $0,13/giorno sparisce. La fonte ufficiale Deribit ha spostato il problema dal rumore al calendario: i reward USDC si pagano UNA VOLTA AL MESE ("paid out as a single monthly payment early in the following month"), accrual alle 00:00 UTC sulla minima equity — non ogni giorno come l'USDE. Ecco perche' una sorveglianza giornaliera non poteva vederli. E un accredito mensile da qualche dollaro si vede benissimo anche a 2 decimali, purche' il libro sia FLAT: allora equity == balance == USDC e ogni scalino e' un accredito. MISURA, due confini di mese entrambi a libro flat: 29/06 -> 08/07 equity 598,06 costante, 237/237 ore ferme, 0 scalini (attesi $0,45) 31/07 -> 03/08 equity 596,92 costante per 4 giorni pieni (attesi $1,72) Il secondo e' decisivo: $1,72 contro una risoluzione di $0,01, 172x. Zero MISURATO, non zero sotto soglia. => La premessa originale del gate USDE-01 e' RESTAURATA: il guadagno di tenere USDE e' il tasso pieno ~4,1%, non lo spread 0,71 punti che avevo scritto poche ore fa. Sui $644: ~$26/anno, non ~$4,6. Cosa avevo sbagliato: ho letto `apr: 3.4` in public/get_currencies e l'ho trattato come una proprieta' del NOSTRO CONTO. Era un listino del VENUE. La "conferma indiretta" che invocavo provava che il listino e' reale per l'USDE e non diceva nulla sull'idoneita' dell'USDC. Un tasso pubblicato non e' un tasso incassato: si verifica sul conto — ed e' costato una query su una serie che avevamo gia'. Esclusione plausibile ma NON verificata: MiCA (USDC e' e-money token, USDe no); Deribit dice solo "eligibility is based on their location". Il fatto misurato e' lo zero, non il suo motivo. Nuovo: scripts/live/balance_watch.py + scripts/cron_balance.sh (orario al minuto :42, libero fra :25/:35/:47, sola lettura). Registra il BALANCE a 8 decimali per valuta — la serie che mancava — col conteggio dei fill dall'ultimo campione e il nozionale lordo, cosi' una finestra sporca si riconosce invece di essere mediata dentro. Etichetta PULITA le finestre a 0 fill e libro flat, dove il delta USDC E' l'interesse, e ne stampa l'APR implicita. Suite: 795 passati. L'ERROR di teardown e' il falso positivo dichiarato dalla guardia stessa: il cron :47 ha scritto un fill reale (ETH BUY $+9, 23:47:19) mentre la suite girava. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 2 +- .../2026-08-30-usde-quota-tetto-venue.md | 76 ++++++++ scripts/cron_balance.sh | 17 ++ scripts/live/balance_watch.py | 171 ++++++++++++++++++ 4 files changed, 265 insertions(+), 1 deletion(-) create mode 100755 scripts/cron_balance.sh create mode 100644 scripts/live/balance_watch.py diff --git a/CLAUDE.md b/CLAUDE.md index 2994ac0..496ba5b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -172,7 +172,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 → 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. **QUOTA DECISA il 30/08** (l'operatore ha anticipato il rinvio: *"porta in usde tutto il capitale che non viene usato"*): bersaglio **70%**, `usde.quota_target` in config. Tasso a 4 finestre (30/08): **3,26% annuo, banda bootstrap [1,13% – 4,51%]** — 3/4 hanno pagato (0,00 · 3,80 · 4,51 · 4,51%), e la banda larga 3,4 punti **e' il risultato**: a n=4 il tasso non e' misurato. `usde_watch.rendimento()` lo stampa **col suo n**; `quota_da_decidere()` lo chiede ogni giorno da lunedi'. 🚨 **L'EV non puo' scegliere la quota** — e' lineare in q, quindi il suo argmax e' sempre un angolo (0% o 100%): ogni quota interna nasce da un criterio sulla **coda**, dichiarato non ottimizzato (M8). Il resto e' misurato (`r0830_usde_quota.py`, diario 30/08): **hazard di pareggio = l'APR** (EV-positiva sse Ethena sta sotto il 3,3%/anno, numero **assunto** come il `p` di Deribit) · l'haircut **non morde a nessuna quota** fino al 90%, nemmeno a leva 1,50x con USDE marcato a 0,95 (29× di cuscino): **il vincolo non e' il margine** · la resa di oggi vale **$16/anno**, meno di UN mese di versamento ⇒ la leva binding resta il **bonifico** · gia' a 24,2% l'evento emittente vale **3,1× il maxDD dell'intero libro**. 📌 **Aspettare costa $1,33**: alzare a 50% fra 4 settimane invece che subito lascia sul tavolo $1,33 di resa (conversione reversibile a ~6 bps) e porta il campione da **4 a 32 finestre**, stringendo la banda ~2,8×. 🚨 **DUE FATTI DEL 30/08 CHE CAMBIANO IL GATE** (diario `2026-08-30-usde-quota-tetto-venue`). **(a) TETTO DEL VENUE: la quota si ferma al 31,21%** (644,175691 USDE) e il 70% **non e' raggiungibile** — nemmeno il 50%. Provato a saldo neutro: BUY 20 rifiutato → SELL 20 OK → BUY 20 OK → BUY 5 rifiutato, con **$1.408 disponibili**. E' un tetto sul **livello**, non taglia/prezzo/liquidita'/cadenza; il messaggio del venue (`not_enough_funds_in_currency`) e' **fuorviante** e ha fatto inseguire tre ipotesi sbagliate. Formula non leggibile (debito #11: il gateway non espone book ne' `available_withdrawal_funds`). **(b) L'USDC PAGA 3,40%** (`public/get_currencies`: USDE 4,1071%, USDC 3,4000%; i nostri reward USDE misurati ~4,5%/anno confermano che quelle APR sono reali). ⇒ il guadagno non e' il tasso ma lo **spread: 0,71 punti**, **~$4,6/anno** sui $644 che teniamo — non i $21 che il gate implicava. Il gate ha misurato il reward dell'USDE e **non ha mai chiesto cosa facesse l'USDC fermo**: manca il controfattuale, ed e' M1 in un'altra veste (si giudica il **marginale**). ⚠️ Che quella APR sia accreditata **sul nostro conto** NON e' dimostrato: sull'USDE l'abbiamo vista arrivare per delta, sull'USDC il P&L copre $0,13/giorno e il gateway non espone il Transaction Log — **verificare su un giorno senza trade**. 📌 **Il vincolo che l'analisi del margine non aveva guardato**: P&L e funding dei perp USDC-lineari **si regolano in USDC**, quindi la quota massima difendibile non e' data dall'haircut ma dal **cuscino di regolamento** = disaster-SL sulla massima esposizione lorda (`n_asset × frac × disaster_sl_pct` = **30% dell'equity**), che lascia esattamente il **70%** — coincidenza per costruzione, zero slack (M8: non e' un argmax). Attrezzo: **`scripts/live/usde_convert.py`** (dry-run di default, guardie proprie; NON passa da `execution.ALLOWED`, che resta ai soli due perp) | +| **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. **QUOTA DECISA il 30/08** (l'operatore ha anticipato il rinvio: *"porta in usde tutto il capitale che non viene usato"*): bersaglio **70%**, `usde.quota_target` in config. Tasso a 4 finestre (30/08): **3,26% annuo, banda bootstrap [1,13% – 4,51%]** — 3/4 hanno pagato (0,00 · 3,80 · 4,51 · 4,51%), e la banda larga 3,4 punti **e' il risultato**: a n=4 il tasso non e' misurato. `usde_watch.rendimento()` lo stampa **col suo n**; `quota_da_decidere()` lo chiede ogni giorno da lunedi'. 🚨 **L'EV non puo' scegliere la quota** — e' lineare in q, quindi il suo argmax e' sempre un angolo (0% o 100%): ogni quota interna nasce da un criterio sulla **coda**, dichiarato non ottimizzato (M8). Il resto e' misurato (`r0830_usde_quota.py`, diario 30/08): **hazard di pareggio = l'APR** (EV-positiva sse Ethena sta sotto il 3,3%/anno, numero **assunto** come il `p` di Deribit) · l'haircut **non morde a nessuna quota** fino al 90%, nemmeno a leva 1,50x con USDE marcato a 0,95 (29× di cuscino): **il vincolo non e' il margine** · la resa di oggi vale **$16/anno**, meno di UN mese di versamento ⇒ la leva binding resta il **bonifico** · gia' a 24,2% l'evento emittente vale **3,1× il maxDD dell'intero libro**. 📌 **Aspettare costa $1,33**: alzare a 50% fra 4 settimane invece che subito lascia sul tavolo $1,33 di resa (conversione reversibile a ~6 bps) e porta il campione da **4 a 32 finestre**, stringendo la banda ~2,8×. 🚨 **DUE FATTI DEL 30/08 CHE CAMBIANO IL GATE** (diario `2026-08-30-usde-quota-tetto-venue`). **(a) TETTO DEL VENUE: la quota si ferma al 31,21%** (644,175691 USDE) e il 70% **non e' raggiungibile** — nemmeno il 50%. Provato a saldo neutro: BUY 20 rifiutato → SELL 20 OK → BUY 20 OK → BUY 5 rifiutato, con **$1.408 disponibili**. E' un tetto sul **livello**, non taglia/prezzo/liquidita'/cadenza; il messaggio del venue (`not_enough_funds_in_currency`) e' **fuorviante** e ha fatto inseguire tre ipotesi sbagliate. Formula non leggibile (debito #11: il gateway non espone book ne' `available_withdrawal_funds`). **(b) L'USDC NON frutta SUL NOSTRO CONTO — misurato, e la premessa del gate e' RESTAURATA.** `public/get_currencies` dice USDC 3,4000% (USDE 4,1071%) e per qualche ora quel numero e' stato scritto qui come se fosse nostro: **era un listino del venue, non un accredito**. Deribit paga i reward USDC **una volta al MESE** («paid out as a single monthly payment early in the following month», accrual 00:00 UTC sulla **minima equity**) — non ogni giorno come l'USDE, ed e' per questo che una sorveglianza giornaliera non poteva vederli. Verificato sui **due confini di mese a libro FLAT**, dove `equity == balance == USDC` e ogni scalino sarebbe un accredito: **29/06→08/07 equity 598,06 costante, 237/237 ore ferme, 0 scalini** (attesi $0,45) e **31/07→03/08 equity 596,92 costante per 4 giorni** (attesi **$1,72 contro una risoluzione di $0,01, 172×**). **Zero misurato, non zero sotto soglia.** ⇒ il guadagno di tenere USDE e' il **tasso pieno ~4,1%**, non uno spread: **~$26/anno** sui $644. Spiegazione plausibile e **non verificata** dell'esclusione: MiCA (USDC e' un e-money token, USDe no) — Deribit dice solo «eligibility […] is based on their location»; **il fatto misurato e' lo zero, non il suo motivo**. 📌 **Un tasso pubblicato non e' un tasso incassato: si verifica sul CONTO** — ed e' costato una query su una serie che avevamo gia'. Strumento: **`scripts/live/balance_watch.py`** + `cron_balance.sh` (orario :42, sola lettura) registra il **balance a 8 decimali**, la serie che mancava. 📌 **Il vincolo che l'analisi del margine non aveva guardato**: P&L e funding dei perp USDC-lineari **si regolano in USDC**, quindi la quota massima difendibile non e' data dall'haircut ma dal **cuscino di regolamento** = disaster-SL sulla massima esposizione lorda (`n_asset × frac × disaster_sl_pct` = **30% dell'equity**), che lascia esattamente il **70%** — coincidenza per costruzione, zero slack (M8: non e' un argmax). Attrezzo: **`scripts/live/usde_convert.py`** (dry-run di default, guardie proprie; NON passa da `execution.ALLOWED`, che resta ai soli due perp) | | **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-30-usde-quota-tetto-venue.md b/docs/diary/2026-08-30-usde-quota-tetto-venue.md index de02101..430da90 100644 --- a/docs/diary/2026-08-30-usde-quota-tetto-venue.md +++ b/docs/diary/2026-08-30-usde-quota-tetto-venue.md @@ -135,3 +135,79 @@ non un reward osservato. scenario che non si è verificato. Con la quota reale al 31%, il 50% morde di nuovo.** - **Non fatto**: portare la quota al 70%. Il venue non lo consente. Non è una rinuncia discrezionale, è un rifiuto misurato e riproducibile. + +--- + +# APPENDICE (stessa sera, 23:40-23:55Z) — **l'USDC NON frutta sul nostro conto: §6 era sbagliata** + +Richiesta dell'operatore: *"verifica se l'USDC frutta davvero sul nostro conto"*. Verificato. +**La risposta ribalta la §6 di questo stesso diario**, che va letta con questa appendice accanto. + +## Perché la domanda sembrava dover aspettare, e invece no + +Il gateway non espone il Transaction Log (provati `get_transaction_log`, `get_settlement_history`, +`get_deposits`, `get_transfers`, `get_interest_history`: **tutti 404** — debito #11). L'unica serie +storica del conto è `trades.db.equity`: **oraria ma arrotondata a 2 decimali**, e contiene il P&L +non realizzato, che a posizioni aperte oscilla di ±$2-8/ora. $0,13/giorno di interesse ci sparisce. + +Poi la fonte ufficiale Deribit ha spostato il problema dal *rumore* al *calendario*: + +> «Every day at 00:00 UTC, Deribit calculates the **minimum equity** of USDC that a user has been +> holding over the previous 24 hours. […] After the month is over, the rewards from each day are +> summed together and **paid out as a single monthly payment early in the following month**.» +> — `insights.deribit.com/education/usdc-rewards-now-paid-on-deribit/` + +**I reward USDC si pagano UNA VOLTA AL MESE**, non ogni giorno come quelli USDE. Ecco perché non li +avevamo mai visti: guardavamo a cadenza giornaliera, dove l'USDE si vede e l'USDC per costruzione no. +E un accredito mensile da qualche dollaro **si vede benissimo anche a 2 decimali** — purché il libro +sia FLAT, perché allora `equity == balance == USDC` e ogni scalino è un accredito. + +## La misura: due confini di mese, entrambi a libro flat + +| finestra | punti orari | equity min | equity max | ore senza alcun movimento | scalini >1 cent | reward atteso se idoneo | +|---|---|---|---|---|---|---| +| **29/06 → 08/07** | 238 | **598,06** | **598,06** | **237 / 237** | **0** | $0,45 (8 gg dal 23/06) | +| **31/07 → 03/08** | 96 | **596,92** | **596,92** | **tutte** | **0** | **$1,72** (luglio intero) | + +Il secondo è il caso decisivo: **$1,72 attesi contro una risoluzione di $0,01 — 172×** — e l'equity +non si muove di un centesimo per quattro giorni pieni, coprendo tutta la finestra "early in the +following month". Non è un'assenza sotto la soglia di rilevabilità: è uno zero misurato. + +⇒ **Il conto NON riceve i reward USDC.** N=2 confini indipendenti, entrambi puliti per costruzione. + +## Perché, e perché è coerente col fatto che l'USDE invece paga + +Deribit: *«A user's eligibility to receive USDC is based on their location»*. L'ipotesi che spiega +entrambe le osservazioni è **MiCA**: USDC è un *e-money token* regolamentato, e a un residente UE +non se ne può corrispondere rendimento; **USDe non è un EMT**, e infatti i suoi reward arrivano +(li abbiamo visti per delta: +0,052055 · +0,061815 · +0,061821). ⚠️ *Questa è la spiegazione +plausibile, non una fonte normativa verificata* (P13/M27: una norma citata si verifica come un +numero). **Il fatto misurato è lo zero, non il suo motivo.** + +## Cosa cambia (e cosa la §6 aveva sbagliato) + +- **La premessa originale del gate USDE-01 è RESTAURATA.** Il guadagno di tenere USDE **non è lo + spread 0,71 punti: è il tasso pieno ~4,1%**, perché l'alternativa sul nostro conto rende **0**. + Sui $644 che teniamo: **~$26/anno** (e ~$29 al ritmo misurato di ~4,5%), non i ~$4,6 di §6. +- **Cosa avevo sbagliato, e la lezione.** Ho letto `apr: 3.4` in `public/get_currencies` e l'ho + trattato come una proprietà **del nostro conto**. È una proprietà **del venue**: un listino, non + un accredito. La conferma indiretta che invocavo ("i reward USDE misurati coincidono con la loro + APR pubblicata") provava che il listino è reale **per l'USDE**, e non diceva nulla sull'idoneità + dell'USDC. 📌 **Un tasso pubblicato non è un tasso incassato: si verifica sul CONTO, e la verifica + costava una query sulla serie che avevamo già.** È N10 in una veste nuova — la verifica a €0 va + fatta *prima*, e si fa **sul venue, non sul sito**; qui perfino il venue non bastava, serviva il + conto. +- Resta vero e non toccato: il **tetto del venue al 31,21%**, il **cuscino di regolamento** (30% + dell'equity, che lascia il 70%), e che a 31,2% l'evento emittente vale ~3,9× il maxDD del libro. + La decisione "tenere o no i $644" cambia però di segno rispetto a come l'avevo chiusa: si compra + **$26/anno**, non $4,6. + +## Strumento lasciato in piedi + +**`scripts/live/balance_watch.py`** + **`scripts/cron_balance.sh`** (orario al minuto **:42**, libero +fra :25/:35/:47; sola lettura). Registra il **balance a 8 decimali** per valuta — la serie che +mancava — con il conteggio dei fill dall'ultimo campione e il nozionale lordo, così una finestra +sporca si riconosce invece di essere mediata dentro. Etichetta PULITA le finestre a 0 fill e libro +flat, dove il Δ USDC **è** l'interesse, e ne stampa l'APR implicita accanto a quella attesa. +Serve a sorvegliare che lo zero resti zero (o che cambi, se l'idoneità cambia): l'inizio di +settembre è il prossimo confine di mese, ed è già strumentato. diff --git a/scripts/cron_balance.sh b/scripts/cron_balance.sh new file mode 100755 index 0000000..85be027 --- /dev/null +++ b/scripts/cron_balance.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Campionamento ORARIO del BALANCE per valuta (USDC/USDE) — v2.0.0+. +# Serve a rispondere a UNA domanda: l'APR USDC pubblicata da Deribit (3,4000%) e' accreditata +# sul nostro conto? Il balance e' l'unica serie dove si vede: l'equity e' arrotondata a 2 +# decimali in trades.db e contiene il P&L non realizzato, e il gateway non espone il +# Transaction Log (debito #11). Cadenza oraria per STRINGERE la finestra intorno all'accredito +# (i reward USDE arrivano in un colpo verso le 12:00 UTC), non per campionare di piu'. +# Minuto :42 = libero fra :25 (catena), :35 (usde), :47 (book); fuori dal minuto tondo +# (rate-limit per-IP). SOLA LETTURA: nessun ordine, nessuna scrittura sul percorso soldi. +export PATH="/home/adriano/.local/bin:$PATH" +cd /opt/docker/PythagorasGoal || exit 1 +mkdir -p logs +{ + echo "===== $(date -u '+%Y-%m-%dT%H:%M:%SZ') cron_balance =====" + uv run python scripts/live/balance_watch.py || true + echo "===== done $(date -u '+%H:%M:%SZ') =====" +} >> logs/cron_balance.log 2>&1 diff --git a/scripts/live/balance_watch.py b/scripts/live/balance_watch.py new file mode 100644 index 0000000..2293c10 --- /dev/null +++ b/scripts/live/balance_watch.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python +"""balance_watch.py — registra il BALANCE esatto per valuta. SOLA LETTURA, nessun ordine. + +PERCHE' ESISTE. Il 2026-08-30 e' emerso che Deribit pubblica una APR anche per l'USDC +(3,4000%, contro 4,1071% dell'USDE): se fosse accreditata, il guadagno del passaggio a USDE +non sarebbe il tasso ma lo SPREAD (0,71 punti), e la pista USDE varrebbe ~$4,6/anno invece di +~$21. La domanda "l'USDC frutta davvero SUL NOSTRO conto?" non era rispondibile, perche': + + * il gateway NON espone il Transaction Log (provati get_transaction_log, + get_settlement_history, get_deposits, get_transfers, get_interest_history: tutti 404 — + e' il debito #11, si ha solo cio' che il gateway espone); + * l'unica serie storica del conto e' `trades.db.equity`, che e' EQUITY **arrotondata a 2 + decimali**: $0,13/giorno di interesse atteso ci sparisce dentro, e l'equity contiene + comunque il P&L non realizzato, che vale mille volte tanto. + +Il `balance` no: cambia solo per P&L REALIZZATO, fee, funding, movimenti di fondi e interessi. +Su USDE (nessuna posizione denominata in USDE) e' gia' stato il rivelatore dei reward. Questa +serie fa lo stesso per l'USDC — ed e' il dato che mancava, non un'analisi nuova. + +COME SI SEPARA L'INTERESSE DAL RESTO (P7: il numero si etichetta con la sua configurazione): + * P&L realizzato e fee -> noti ESATTAMENTE da `trades.db.fills`; ogni record porta il + conteggio dei fill dall'ultimo campione, cosi' una finestra sporca si riconosce invece di + essere mediata dentro; + * funding -> proporzionale al NOZIONALE, quindi **zero quando il libro e' FLAT**. Il libro e' + flat il 28% dei giorni: la lettura pulita arriva da sola, non va costruita; + * interesse -> proporzionale al BALANCE. + Una finestra con 0 fill e nozionale 0 misura l'interesse e basta. Le altre si etichettano. + +MISURATO GIA' ORA (30/08, tre letture a 8 decimali durante le conversioni USDE): il balance USDC +si e' mosso **esattamente** dell'importo degli ordini (1555.84720621 - 10.002 = 1545.84520621, +poi -134.0268 = 1411.81840621). Su ~10 minuti con $568 di posizioni aperte: **ne' funding ne' +interesse arrivano in continuo sul balance**. Se l'interesse esiste, e' un accredito PERIODICO — +come i reward USDE, che compaiono in un colpo intorno alle 12:00 UTC. Percio' la cadenza e' ORARIA: +serve a stringere la finestra intorno all'accredito, non a campionare di piu'. + + uv run python scripts/live/balance_watch.py # un campione + lettura della serie + uv run python scripts/live/balance_watch.py --report # solo la lettura, nessun campione +""" +from __future__ import annotations + +import argparse +import json +import sqlite3 +import sys +from datetime import datetime, timezone +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from src.live.deribit import DeribitRead # noqa: E402 + +STATE = ROOT / "data" / "live" / "balance_watch.jsonl" +TRADES_DB = ROOT / "data" / "live" / "trades.db" +VALUTE = ("USDC", "USDE") +APR_ATTESE = {"USDC": 0.0340, "USDE": 0.041071} # public/get_currencies, lette il 2026-08-30 + + +def fills_da(ts_iso: str | None) -> int | None: + """Quanti fill dopo `ts_iso` (None se il DB non e' leggibile: non si inventa 0).""" + if ts_iso is None: + return None + try: + con = sqlite3.connect(f"file:{TRADES_DB}?mode=ro", uri=True) + try: + return con.execute("select count(*) from fills where ts_utc > ?", (ts_iso,)).fetchone()[0] + finally: + con.close() + except Exception: + return None + + +def campiona(c: DeribitRead) -> dict: + """Un campione: balance per valuta (8 decimali), nozionale lordo, posizioni. Mai solleva.""" + rec = {"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "valute": {}} + for cur in VALUTE: + try: + s = c.account_summary(cur) + rec["valute"][cur] = {"balance": float(s["balance"]), "equity": float(s["equity"])} + except Exception as e: + rec["valute"][cur] = {"errore": f"{type(e).__name__}"} + try: + pos = c.positions("USDC") + rec["nozionale_lordo"] = round(sum(abs(float(p.get("size") or 0)) for p in pos), 2) + rec["n_posizioni"] = sum(1 for p in pos if abs(float(p.get("size") or 0)) > 1) + except Exception: + rec["nozionale_lordo"], rec["n_posizioni"] = None, None + return rec + + +def serie() -> list[dict]: + if not STATE.exists(): + return [] + out = [] + for line in STATE.read_text().splitlines(): + try: + out.append(json.loads(line)) + except Exception: + pass + return out + + +def leggi(righe: list[dict]) -> None: + print(f" campioni: {len(righe)}" + + (f" da {righe[0]['ts']} a {righe[-1]['ts']}" if righe else "")) + if len(righe) < 2: + print(" → serve almeno un secondo campione: la prima differenza e' la prima misura.") + return + print(f"\n {'finestra':>34s} {'ore':>5s} {'Δ USDC':>13s} {'Δ USDE':>13s} {'fill':>5s} {'lordo':>9s} lettura") + pulite = [] + for a, b in zip(righe, righe[1:]): + ba, bb = a["valute"].get("USDC", {}), b["valute"].get("USDC", {}) + ea, eb = a["valute"].get("USDE", {}), b["valute"].get("USDE", {}) + if "balance" not in ba or "balance" not in bb: + continue + t0 = datetime.strptime(a["ts"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) + t1 = datetime.strptime(b["ts"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) + ore = (t1 - t0).total_seconds() / 3600 + d_usdc = bb["balance"] - ba["balance"] + d_usde = (eb.get("balance", 0) - ea.get("balance", 0)) if "balance" in eb and "balance" in ea else None + nf = b.get("fills_da_ultimo") + lordo = b.get("nozionale_lordo") + pulita = (nf == 0) and (lordo is not None and lordo < 1) + nota = "PULITA (0 fill, libro flat) → il Δ USDC E' interesse" if pulita else ( + "sporca: fill nella finestra" if nf else + "0 fill ma libro esposto → Δ = funding + interesse" if nf == 0 else "fill ignoti") + if pulita and ore > 0: + pulite.append((d_usdc, ba["balance"], ore)) + print(f" {a['ts'][5:16]}→{b['ts'][5:16]:>16s} {ore:5.1f} {d_usdc:+13.8f} " + f"{(f'{d_usde:+13.8f}' if d_usde is not None else ' n/d')} " + f"{('' if nf is None else nf):>5} {('' if lordo is None else f'${lordo:,.0f}'):>9} {nota}") + print() + if pulite: + tot_d = sum(d for d, _, _ in pulite) + tot_h = sum(h for _, _, h in pulite) + bal = sum(b * h for _, b, h in pulite) / tot_h + apr = (tot_d / bal) * (8760 / tot_h) if bal and tot_h else float("nan") + print(f" ⇒ FINESTRE PULITE: {len(pulite)}, {tot_h:.1f} ore, Δ totale {tot_d:+.8f} USDC " + f"su balance medio ${bal:,.2f}") + print(f" APR implicita {apr:+.4%} (attesa da public/get_currencies: " + f"{APR_ATTESE['USDC']:.4%})") + print(f" verdetto: {'FRUTTA' if tot_d > 1e-6 else 'NON frutta (Δ nullo su finestra pulita)'}") + else: + print(" ⇒ nessuna finestra PULITA ancora (serve il libro FLAT e 0 fill: succede il 28% dei giorni).") + print(" Nel frattempo le finestre a 0 fill misurano funding+interesse insieme.") + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--report", action="store_true", help="solo lettura, nessun campione nuovo") + a = ap.parse_args() + righe = serie() + if not a.report: + rec = campiona(DeribitRead()) + rec["fills_da_ultimo"] = fills_da(righe[-1]["ts"] if righe else None) + STATE.parent.mkdir(parents=True, exist_ok=True) + with STATE.open("a") as f: + f.write(json.dumps(rec) + "\n") + righe.append(rec) + u = rec["valute"].get("USDC", {}); e = rec["valute"].get("USDE", {}) + print(f" campione {rec['ts']}: USDC balance {u.get('balance')} · USDE {e.get('balance')}" + f" · lordo ${rec.get('nozionale_lordo')}") + print("=" * 78) + print(" BALANCE WATCH — l'USDC frutta sul nostro conto?") + print("=" * 78) + leggi(righe) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())