cinque punti della revisione 09/09: usde_watch a 1000 trade + finestra 24h, cuscino_watch (equity USDC, riconverte da solo), PREVDAY-01 kill/veto cablati, SCALA-01 al 2027-02-28, versamento 04/09 dichiarato
Decisi dall'operatore il 2026-09-10, verificati da revisione fable (15 segnalazioni, 12 applicate). - usde_watch: TRADE_LIMIT 1000 (count max Deribit) e trade_copertura(): lista troncata o ultima lettura oltre la finestra di 24h del gateway => somma NON leggibile, reward non attribuito (P12). Riga del 07/09 corretta nel log con campo `correzione` (400 USDE erano acquisti, non reward). - cuscino_watch.py (cron :53, monitor_health): equity USDC contro cuscino derivato da usde.cuscino_richiesto_usd (formula spostata in src/live/usde.py, usde_convert la importa); OK/PREAVVISO/SCOPERTO/BLIND; sotto zero lancia usde_convert --quota quota_ripristino(0.20)=0.64 --esegui con guardie (execution_enabled, depeg_warn, 1 tentativo/6h). Primo giro: PREAVVISO, +$26. - usde_convert: il tetto del venue vale solo in ACQUISTO (bloccava la vendita). - paper_prevday: GATE PREVDAY-01 cablato (2027-06-21, kill Sharpe giornaliero < -0,50 su >=180 g attivi, veto >=80% barre ricostruibili + divergenze non crescenti con soglia materiale). Oggi: +0,95 su 81 g, 1942/1943 ricostruibili, kill NON MATURO. - CLAUDE.md: arming 20/06 (TP01) / 23/06 (BOOK); piano EUR 5.000 chiuso col versamento 04/09 ($2.414,68 dal balance, dichiarato); SCALA-01 non prima del 2027-02-28 (A2 dal 01/09); PREVDAY-01 con data, kill, veto; §5.17 riparato con i limiti dichiarati (ratchet, slack zero a 0,70). - test: 1062 (+31): test_cuscino_watch (16), test_paper_prevday_gate (8), test_usde_watch (+5). Fixes #2 Fixes #3 Fixes #4 Fixes #5 Fixes #6 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016kqvff47UBGeYfj1QeN4zE
This commit is contained in:
@@ -38,7 +38,7 @@ Documento di fondazione: `docs/diary/2026-06-19-deribit-history.md`.
|
|||||||
gia' dichiarati artefatti. Riscritto contro lo stato vero. **La lezione non e' "aggiornare il
|
gia' dichiarati artefatti. Riscritto contro lo stato vero. **La lezione non e' "aggiornare il
|
||||||
README": e' che un reset invalida anche i documenti che nessuno rilegge** — l'inventario di cosa
|
README": e' che un reset invalida anche i documenti che nessuno rilegge** — l'inventario di cosa
|
||||||
cita numeri morti va fatto il giorno del reset, non quando qualcuno ci inciampa.
|
cita numeri morti va fatto il giorno del reset, non quando qualcuno ci inciampa.
|
||||||
- **L'esecuzione e' ARMATA e LIVE** su Deribit mainnet dal 2026-06-20. Capitale reale **~$4.495** dal 04/09 (era ~$635 fino al 25/08, ~$2.060 fino al 04/09)
|
- **L'esecuzione e' ARMATA e LIVE** su Deribit mainnet: **TP01 da solo dal 2026-06-20** (commit `4650aa7`), **il BOOK TP01+SKH01 dal 2026-06-23** (commit `db738bc`, prima lettura di equity 23/06 22:00Z — e' la data d'arming che il TWR e `config/live.json` usano). Capitale reale **~$4.495** dal 04/09 (era ~$635 fino al 25/08, ~$2.060 fino al 04/09)
|
||||||
(NON i €2.000 nominali del paper trader).
|
(NON i €2.000 nominali del paper trader).
|
||||||
- Si riparte dalla ricerca di strategie NUOVE, su dati certi, con la metodologia della §8.
|
- Si riparte dalla ricerca di strategie NUOVE, su dati certi, con la metodologia della §8.
|
||||||
|
|
||||||
@@ -108,8 +108,11 @@ le quattro gia' chiuse il 31/08. L'USDC sul conto **non e' fermo** (base di sizi
|
|||||||
vero sta fuori (~€6k in XEON = lo split-cassa): le opzioni sicure valgono **+€52-62/anno** (BOT 12,5% /
|
vero sta fuori (~€6k in XEON = lo split-cassa): le opzioni sicure valgono **+€52-62/anno** (BOT 12,5% /
|
||||||
conto deposito 26%), sUSDe rende 2x ma e' **lo stesso emittente dell'USDE** e distrugge lo split.
|
conto deposito 26%), sUSDe rende 2x ma e' **lo stesso emittente dell'USDE** e distrugge lo split.
|
||||||
✅ **L'operatore ha scelto di VERSARE** (piano 27/07: **€5.000 dentro, ~€1.000 fuori** — la protezione
|
✅ **L'operatore ha scelto di VERSARE** (piano 27/07: **€5.000 dentro, ~€1.000 fuori** — la protezione
|
||||||
venue satura a qualunque quota > 0). Nessuna azione di config. **In attesa di importo e data**; al
|
venue satura a qualunque quota > 0). ✅ **VERSATO il 04/09: $2.414,68 di USDC** (dal balance, fra 13:42Z e
|
||||||
deposito va **una riga di giornale**, non ricostruita dopo. `r0901d_soldi_fermi.py`.
|
14:42Z), **dichiarato il 10/09** in `movimenti_dichiarati.jsonl` — e **l'operatore ha detto che e' l'INTERO
|
||||||
|
versamento previsto: il piano €5.000 e' chiuso qui**, non resta nulla «in attesa». Per sei giorni era stato solo
|
||||||
|
*rilevato* (+$2.410,14 dal salto di equity, col mercato dell'ora dentro): la regola di §2 dice il giorno stesso.
|
||||||
|
`r0901d_soldi_fermi.py`.
|
||||||
|
|
||||||
**Candidati in forward-monitor** (nessuno nel book): XSR01 · DVOLSPREAD · STATARB · PREVDAY.
|
**Candidati in forward-monitor** (nessuno nel book): XSR01 · DVOLSPREAD · STATARB · PREVDAY.
|
||||||
|
|
||||||
@@ -143,7 +146,7 @@ sono all'ancora fortunata; le stime oneste sono la **mediana della banda d'ancor
|
|||||||
| P(≥50 €/g), canale funded, 36 mesi | 42% | **2,6%** [1,5 – 4,7], P(zero) 40,3% |
|
| P(≥50 €/g), canale funded, 36 mesi | 42% | **2,6%** [1,5 – 4,7], P(zero) 40,3% |
|
||||||
| XSR01 | Sharpe 1,82 (e' una **terza** lente, divisore fisso 50) | **1,79** alla scoperta / **1,56-1,63** a oggi — lente dei gate, sole barre chiuse. Citare sempre la coppia (lente, ultima barra chiusa) |
|
| XSR01 | Sharpe 1,82 (e' una **terza** lente, divisore fisso 50) | **1,79** alla scoperta / **1,56-1,63** a oggi — lente dei gate, sole barre chiuse. Citare sempre la coppia (lente, ultima barra chiusa) |
|
||||||
| soffitto direzionale BTC/ETH | ~1,3 | **~1,15** col funding dentro |
|
| soffitto direzionale BTC/ETH | ~1,3 | **~1,15** col funding dentro |
|
||||||
| performance del libro LIVE | «+243%» di equity (e' **96,3% un bonifico**) · e **+11,98%** (lettura 06/09 20:47Z, PRIMA della dichiarazione del versamento di prova) | **TWR +7,83%** spezzato sui TRE movimenti — +8,14% fino al 25/08 08:00, −0,62% / −0,12% / +0,46% dopo (lettura 06/09 21:14Z); trading da arming **+$62,45**. Dal 02/09 lo stampa `trades_db.py --report` (debito 14 chiuso). 🚨 **Il 06/09 l'operatore ha dichiarato un versamento di PROVA di €25 il 25/08 alle 08:00Z**, ~$24,9: +3,3% su $647, **sotto la soglia del 10%** del rilevatore, quindi per 12 giorni e' stato contato come trading (un terzo del «trading da arming» e 4 punti di TWR). Ora sta in `data/live/movimenti_dichiarati.jsonl`, letto da `journal.movimenti_capitale` (fonte `dichiarato`, importo dell'operatore, il mercato dell'ora resta nel rendimento). **Regola: ogni versamento, anche di prova, va dichiarato li' il giorno stesso** — il rilevatore vede solo i salti ≥10% |
|
| performance del libro LIVE | «+243%» di equity (e' **96,3% un bonifico**) · e **+11,98%** (lettura 06/09 20:47Z, PRIMA della dichiarazione del versamento di prova) | **TWR +6,66%** spezzato sui TRE movimenti in QUATTRO tratti — +8,14% fino al 25/08 08:00, −0,62% / −0,12% / **−0,63%** dopo (lettura 10/09 13:04Z, dopo la dichiarazione del versamento del 04/09 a **$2.414,68** dal balance); trading da arming **+$13,82**. Era +7,83% / +$62,45 il 06/09: la coda e' la marcatura delle due long dal picco del 06/09, non trade chiusi. Dal 02/09 lo stampa `trades_db.py --report` (debito 14 chiuso). 🚨 **Il 06/09 l'operatore ha dichiarato un versamento di PROVA di €25 il 25/08 alle 08:00Z**, ~$24,9: +3,3% su $647, **sotto la soglia del 10%** del rilevatore, quindi per 12 giorni e' stato contato come trading (un terzo del «trading da arming» e 4 punti di TWR). Ora sta in `data/live/movimenti_dichiarati.jsonl`, letto da `journal.movimenti_capitale` (fonte `dichiarato`, importo dell'operatore, il mercato dell'ora resta nel rendimento). **Regola: ogni versamento, anche di prova, va dichiarato li' il giorno stesso** — il rilevatore vede solo i salti ≥10% |
|
||||||
|
|
||||||
📌 **Il libro a k=1 rende MENO dell'S&P 500** (15,19% contro 17,40%, stessa finestra): il vantaggio
|
📌 **Il libro a k=1 rende MENO dell'S&P 500** (15,19% contro 17,40%, stessa finestra): il vantaggio
|
||||||
sta nello **Sharpe** (1,35 vs 0,89), e **senza leva non si converte in rendimento**. A iso-rischio
|
sta nello **Sharpe** (1,35 vs 0,89), e **senza leva non si converte in rendimento**. A iso-rischio
|
||||||
@@ -255,8 +258,8 @@ regolamento (§1). Quanto segue e' la misura del 30-31/08, vera allora, tenuta p
|
|||||||
| **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) |
|
| **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 |
|
| **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 |
|
||||||
| **GATE PROP-01** | chiuso 22-23/08 | (a) listino 13/13 ✅ (b) ancora SKH01 23/23 ✅ (c) economica+sopravvivenza 2/2 ✅ | **chiuso 3/3** — ma il numero operativo e' sceso 42% → 7,8% → 4,4% → 2,6% |
|
| **GATE PROP-01** | chiuso 22-23/08 | (a) listino 13/13 ✅ (b) ancora SKH01 23/23 ✅ (c) economica+sopravvivenza 2/2 ✅ | **chiuso 3/3** — ma il numero operativo e' sceso 42% → 7,8% → 4,4% → 2,6% |
|
||||||
| **GATE PREVDAY-01** | scritto 23/08 | 10 condizioni | **8/10**: la cella che GIRA non e' quella che la selezione onesta sceglie, e quella viva **fallisce il DSR** (0,905) |
|
| **GATE PREVDAY-01** | decisione **2027-06-21** (scritto 23/08, `RESULTS-0822` §54); kill leggibile da ~18/12/2026 | 7 condizioni (a)-(g) tutte necessarie + **veto d'integrita'** (≥80% barre ricostruibili, divergenze non crescenti) + **kill** Sharpe forward **giornaliero** < −0,50 su ≥180 giorni attivi, senza ri-ottimizzare | **5/7** (= l'«8/10» di §54 e di `20-ondate`, contato su una base a 10 voci: stessa sostanza, base diversa): la cella che GIRA non e' quella che la selezione onesta sceglie, e quella viva **fallisce il DSR** (0,905) — (a) e (b) sono STRUTTURALI, nessun giorno di forward le cambia. ✅ **Kill e veto cablati il 10/09** in `paper_prevday.py` (issue #4; fino ad allora esistevano solo nel testo e la revisione del 09/09 lesse «senza data»): al 10/09 Sharpe giornaliero +0,95 su 81 giorni (MDE ±4,25), 1942/1943 barre ricostruibili, kill NON MATURO. Decisione dell'operatore (10/09): si tiene la data — verso i 50 €/g PREVDAY vale centesimi, non si spende altro |
|
||||||
| **GATE SCALA-01** (portare `book_scale_k` da 1,00 a 1,25) | non prima del **2026-10-01** | 9 condizioni **tutte necessarie** (`SPEC-scale-key` §6.2): A1 gradino di `SCALA_LADDER` · A2 k0 in produzione ≥180g col criterio passato ogni giorno · A3 **C1-C5 di WORST-DAY ricalcolati OGGI**, non ereditati · A4 invariante disaster-SL · A5 tetto sul prodotto · **A6 ANTI-RECENCY** (il criterio deve passare anche **escludendo gli ultimi 90 giorni**) · A7 `r0726_fee_sensitivity` rifatto · A8 la modifica e' **una riga di config + una di giornale, zero codice** · A9 T1-T11 verdi | ◐ **A8/A9 FATTI il 01/09** (chiave inerte + 18 test); **A2 non maturo** — la specifica impone ≥30 giorni a 1,00 col sorvegliante attivo *prima* di eseguire il gate: «l'unico modo di scoprire che il sorvegliante e' rotto mentre la leva e' ancora 1,00». 📌 **A6 oggi NON morde** (k ammesso ~3,5x contro un gradino di 1,25x): il vincolo che morde e' il **disaster-SL**. E' scritto ora **perche' non e' comodo per nessuno**, non quando servira' |
|
| **GATE SCALA-01** (portare `book_scale_k` da 1,00 a 1,25) | non prima del **2027-02-28** (= 180 giorni dal **01/09**, primo giorno di `scale_watch`: A2 conta da quando il criterio e' MISURATO, non dall'arming — decisione dell'operatore 10/09, issue #5. «Non prima del 2026-10-01», scritto il 01/09, era incompatibile con A2 per costruzione) | 9 condizioni **tutte necessarie** (`SPEC-scale-key` §6.2): A1 gradino di `SCALA_LADDER` · A2 k0 in produzione ≥180g col criterio passato ogni giorno · A3 **C1-C5 di WORST-DAY ricalcolati OGGI**, non ereditati · A4 invariante disaster-SL · A5 tetto sul prodotto · **A6 ANTI-RECENCY** (il criterio deve passare anche **escludendo gli ultimi 90 giorni**) · A7 `r0726_fee_sensitivity` rifatto · A8 la modifica e' **una riga di config + una di giornale, zero codice** · A9 T1-T11 verdi | ◐ **A8/A9 FATTI il 01/09** (chiave inerte + 18 test); **A2 non maturo** — la specifica impone ≥30 giorni a 1,00 col sorvegliante attivo *prima* di eseguire il gate: «l'unico modo di scoprire che il sorvegliante e' rotto mentre la leva e' ancora 1,00». 📌 **A6 oggi NON morde** (k ammesso ~3,5x contro un gradino di 1,25x): il vincolo che morde e' il **disaster-SL**. E' scritto ora **perche' non e' comodo per nessuno**, non quando servira' |
|
||||||
| **`edge_watch`** (kill del book live) | continuo | **(A)** Sharpe rolling 36m < −0,5 → si riapre `weights_tilt_null`+DSR, il book **non si spegne da solo**; **(B)** in un anno con DD buy&hold >10%, il DD di TP01 deve restare <75% di quello | stato 27/07: Sharpe 36m +1,51, protezione **8/8** |
|
| **`edge_watch`** (kill del book live) | continuo | **(A)** Sharpe rolling 36m < −0,5 → si riapre `weights_tilt_null`+DSR, il book **non si spegne da solo**; **(B)** in un anno con DD buy&hold >10%, il DD di TP01 deve restare <75% di quello | stato 27/07: Sharpe 36m +1,51, protezione **8/8** |
|
||||||
|
|
||||||
📌 **XSR01 sotto la lente RENDITA (25/08): il meccanismo vale ~0.** Mescolarne i rendimenti da' lo
|
📌 **XSR01 sotto la lente RENDITA (25/08): il meccanismo vale ~0.** Mescolarne i rendimenti da' lo
|
||||||
@@ -438,13 +441,19 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per
|
|||||||
`valida` sia la prima istruzione, che l'uso documenti i flag accettati, e che **i flag che il
|
`valida` sia la prima istruzione, che l'uso documenti i flag accettati, e che **i flag che il
|
||||||
cron usa davvero restino accettati** (P15/P16). Diario `2026-09-02c-flag-e-pulizia.md`.
|
cron usa davvero restino accettati** (P15/P16). Diario `2026-09-02c-flag-e-pulizia.md`.
|
||||||
|
|
||||||
17. ⚠️ **Nessuno sorveglia il CUSCINO USDC di regolamento** (aperto il 2026-09-06, quando la quota USDE e'
|
17. ✅ **RIPARATO (2026-09-10, issue #3).** Nessuno sorvegliava il CUSCINO USDC di regolamento (aperto il 06/09
|
||||||
salita al 68,7%). P&L e funding dei perp si regolano in USDC: una perdita del libro consuma il cuscino e
|
con la quota USDE al 68,7%): P&L e funding dei perp si regolano in USDC, una perdita del libro lo consuma e fa
|
||||||
fa salire la quota USDE da sola, senza alcun ordine. `usde_watch` allerta solo su `quota > quota_max_frac`
|
salire la quota da sola; `usde_watch` allerta solo su `quota > 0,85`, e un USDC negativo costa lo 0,05%/giorno.
|
||||||
(0,85): a 68,7% lo slack sul cuscino e' **$58**, e un saldo USDC negativo costa lo 0,05%/giorno. Il
|
Ora **`scripts/live/cuscino_watch.py`** (cron **:53**, orario, `cron_cuscino.sh`, sorvegliato da `monitor_health`)
|
||||||
sorvegliante giusto legge `USDC − cuscino_richiesto` (derivato da config come in
|
legge l'**EQUITY USDC** (decisione dell'operatore: e' quella che risponde del regolamento, il balance vede il buco
|
||||||
`usde_convert.cuscino_richiesto_usd`, P1) ogni ora e allerta sotto zero; e la riconversione USDE→USDC che
|
solo a trade chiuso), deriva il cuscino da `usde.cuscino_richiesto_usd` — **la formula si e' spostata in
|
||||||
ripristina il cuscino e' `usde_convert --quota <minore>`, che esiste ma non e' automatica.
|
`src/live/usde.py`, e `usde_convert` la importa da li'** (due lettori, una formula: P1) — e ha tre stati:
|
||||||
|
OK · **PREAVVISO** (slack < 10% del cuscino, ⚠️ alla transizione) · **SCOPERTO** (slack < 0: 🚨 e
|
||||||
|
**riconversione automatica** `usde_convert --quota usde.quota_ripristino(0,20) --esegui`, cioe' **0,64**, che lascia
|
||||||
|
il cuscino piu' un 20% — il DOPPIO del preavviso, altrimenti ogni 🚨 sarebbe seguito da un ⚠️ per costruzione). Guardie: `execution_enabled` del libro (disarmare il libro disarma anche questo), niente
|
||||||
|
vendita sotto `depeg_warn` (decide l'operatore), un tentativo ogni 6h, le guardie proprie di `usde_convert`;
|
||||||
|
marcatore «gia' detto» = esito dell'invio (§5.2). **Primo giro (13:00Z): gia' PREAVVISO, slack +$26** su $1.335
|
||||||
|
richiesti — la marcatura del libro dal 06/09 lo aveva consumato senza che nessuno lo vedesse. ⚠️ **Due limiti dichiarati** (revisione fable 10/09): (a) **a quota 0,70 lo slack e' ZERO per costruzione**, quindi PREAVVISO e' lo stato normale del conto a quota piena (allerta una volta); (b) **il ripristino e' un RATCHET verso il basso** — dopo una riconversione la quota resta a 0,64 e nessun automatismo la riporta a 0,70: ricomprare e' `usde_convert --quota 0.70` a mano, decisione dell'operatore (N9: va deciso se accettare il ratchet o autorizzare un riacquisto automatico quando lo slack torna sopra il margine). Il tetto del venue in `usde_convert.piano` ora vale **solo in acquisto**: bloccava anche la vendita, e con `venue_cap_frac` rimesso in config la riconversione sarebbe stata morta. `tests/test_cuscino_watch.py` (16).
|
||||||
|
|
||||||
18. ✅ **RIPARATO (2026-09-09 sera).** `cblib.spot_series` + `asof(ts)` guardava **un'ora avanti** (feed
|
18. ✅ **RIPARATO (2026-09-09 sera).** `cblib.spot_series` + `asof(ts)` guardava **un'ora avanti** (feed
|
||||||
1h etichettato all'apertura: verificato, chiusura 1h a T == chiusura 5m a T+55 nel 100% delle barre)
|
1h etichettato all'apertura: verificato, chiusura 1h a T == chiusura 5m a T+55 nel 100% delle barre)
|
||||||
@@ -689,7 +698,7 @@ uv run python scripts/live/trades_db.py --reconcile # incrocio dell
|
|||||||
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 python scripts/live/revisione.py --secco # taglia del materiale della revisione settimanale (niente modello)
|
uv run python scripts/live/revisione.py --secco # taglia del materiale della revisione settimanale (niente modello)
|
||||||
uv run pytest # test (1031, tutti verdi al 09/09 sera)
|
uv run pytest # test (1062, tutti verdi al 10/09)
|
||||||
```
|
```
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# I cinque punti della revisione del 09/09 — decisi e fatti
|
||||||
|
|
||||||
|
**Data:** 2026-09-10 (sessione 12:08Z → 13:10Z; i timestamp qui dentro sono letti, non ricordati)
|
||||||
|
**Origine:** la prima revisione settimanale (`docs/revisioni/2026-09-09.md`, revisore `fable`) e il
|
||||||
|
giornale del 09/09. Ogni punto e' stato spiegato all'operatore con pro e contro e **deciso da lui**
|
||||||
|
(§3-style: decisioni con l'informazione completa). Issue sul Gitea di casa #2-#6, chiuse con la
|
||||||
|
soluzione scritta (regola globale del 10/09).
|
||||||
|
|
||||||
|
## Le decisioni, in una tabella
|
||||||
|
|
||||||
|
| # | punto | decisione dell'operatore | issue |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | `usde_watch` contava 400 USDE come reward (07/09) | ripara ora + issue | #2 bug |
|
||||||
|
| 2 | cuscino USDC senza sorvegliante (§5.17) | **equity USDC, riconverte da solo** | #3 sviluppo |
|
||||||
|
| 3 | versamento 04/09 solo rilevato; piano €5.000 «in attesa» | **e' l'INTERO versamento previsto**: dichiarato, piano chiuso | #6 sviluppo |
|
||||||
|
| 4 | SCALA-01 A2: da quando contano i 180 giorni | **dal 01/09** (sorvegliante attivo) → gate non prima del **2027-02-28** | #5 bug |
|
||||||
|
| 5 | PREVDAY-01 «senza data» | **tieni 21/06/2027, cabla kill e veto, correggi §4** | #4 bug |
|
||||||
|
|
||||||
|
Sul punto 5 la domanda giusta e' stata «ricorda dove dobbiamo arrivare (50 €/g)»: PREVDAY
|
||||||
|
de-luckato al 15% vale +0,10 di Sharpe sul libro e la somma di *tutti* i lead dell'ondata di agosto
|
||||||
|
vale +0,036 €/giorno. Verso i 50 €/g il candidato vale centesimi in qualunque esito del gate:
|
||||||
|
non si spende una sessione di ricerca per anticipare (a)+(b), e non si butta l'unica gamba short
|
||||||
|
scorrelata oltre a SKH01 per risparmiare un cron. Costo scelto: mezza giornata, una volta.
|
||||||
|
|
||||||
|
## Punto 1 — il lettore dei trade spot aveva DUE limiti, non uno
|
||||||
|
|
||||||
|
Il primo era noto dalla revisione: `trade_history(limit=50)` con 54 ordini. Il secondo l'ho
|
||||||
|
trovato sondando il gateway per ripararlo: `get_user_trades_by_instrument` chiamato senza
|
||||||
|
`historical` restituisce **solo le ultime ~24 ore** — il fill BTC dell'08/09 06:47Z e' invisibile
|
||||||
|
alle 12:55Z del 10/09, quello del 09/09 21:47Z si vede. Il cron gira ogni 24h alle 12:35: sta
|
||||||
|
dentro la finestra per pochi secondi. Un giro saltato avrebbe reso invisibili i trade fra 48h e
|
||||||
|
24h fa, e il delta sarebbe stato attribuito al reward *esattamente come il 07/09*, con un
|
||||||
|
meccanismo diverso.
|
||||||
|
|
||||||
|
Ora `trade_copertura()` dichiara **non leggibile** (somma `None`, e `analizza` non attribuisce:
|
||||||
|
P12) in entrambi i casi — lista lunga quanto il limite (troncata) o ultima lettura oltre 24h+5min —
|
||||||
|
e il record porta `trades_motivo` (P4: il perche'). `TRADE_LIMIT = 1000` e' il `count` massimo di
|
||||||
|
Deribit. Controllo positivo nei test: 54 ordini si sommano tutti.
|
||||||
|
|
||||||
|
La riga del 07/09 in `data/live/usde_watch.jsonl` e' **corretta in loco** con campo `correzione`
|
||||||
|
(originale conservato dentro, copia del file in `usde_watch.jsonl.pre_fix_20260910`): 54 ordini
|
||||||
|
= 8 + 30 + 16 dal registro `usde_convert.jsonl`, 2.434 USDE, reward 0,080934. Il tasso non
|
||||||
|
cambia — la riga era gia' esclusa per `con_trade` — e `rendimento()` oggi da' **3,94% su 14
|
||||||
|
finestre, 12 pagate**.
|
||||||
|
|
||||||
|
## Punto 2 — `cuscino_watch`: la grandezza che nessuno guardava
|
||||||
|
|
||||||
|
Il cuscino richiesto e' `2 × 0,5 × 0,30 × equity_tot` = 30% dell'equity, **derivato** da
|
||||||
|
config e `book.WEIGHT`. La formula e' passata da `usde_convert.py` a `src/live/usde.py`, e
|
||||||
|
`usde_convert` la importa da li': due lettori, una formula (P1 — cinque occorrenze nel
|
||||||
|
progetto di sorveglianti puntati su un bersaglio ridichiarato).
|
||||||
|
|
||||||
|
| stato | criterio | azione |
|
||||||
|
|---|---|---|
|
||||||
|
| OK | slack ≥ 10% del cuscino | niente |
|
||||||
|
| PREAVVISO | 0 ≤ slack < 10% | ⚠️ alla transizione |
|
||||||
|
| SCOPERTO | slack < 0 | 🚨 + `usde_convert --quota 0,64 --esegui` (cuscino + 20% di margine, alzato dal 10% in revisione) |
|
||||||
|
| BLIND | conto non leggibile | ⚠️ alla transizione |
|
||||||
|
|
||||||
|
Guardie della riconversione: `execution_enabled` del libro (un solo interruttore), niente vendita
|
||||||
|
sotto `depeg_warn` (li' decide l'operatore, e l'allarme lo dice), un tentativo ogni 6h, le guardie
|
||||||
|
proprie di `usde_convert` (banda di prezzo, book leggibile, tetto HARD) ereditate lanciando lo
|
||||||
|
script vero (P15). Il marcatore «gia' detto» e' l'esito di `notify`: un invio fallito non consuma
|
||||||
|
la transizione (lezione del debito §5.2). Cron `:53` = dopo il giro del book (`:47`), fuori dal
|
||||||
|
minuto tondo. `monitor_health` lo sorveglia (max 3h).
|
||||||
|
|
||||||
|
📌 **Primo giro a secco (13:00Z): gia' PREAVVISO.** USDC equity $1.361,23 contro $1.335,26
|
||||||
|
richiesti: **slack +$25,97**. Il 09/09 il revisore aveva stimato +$41/+$59; la marcatura del
|
||||||
|
libro dal picco del 06/09 ne aveva consumati altri 15-30 senza che nessuno lo vedesse. E' il
|
||||||
|
caso d'uso del sorvegliante, arrivato prima del sorvegliante.
|
||||||
|
|
||||||
|
## Punto 3 — il versamento del 04/09, dal balance e non dall'equity
|
||||||
|
|
||||||
|
Il salto di equity 13:47→14:47Z diceva +$2.410,14 col mercato dell'ora dentro. Il **balance
|
||||||
|
USDC** di `balance_watch` (cambia solo per P&L realizzato, fee, funding e movimenti) dice
|
||||||
|
1.417,73455581 → 3.832,41432481 = **+$2.414,68**, nessun fill nella finestra, funding su $541
|
||||||
|
di nozionale ~$0,01. Dichiarato con banda $0,05. Il report ora lo mostra come dichiarato e
|
||||||
|
il TWR passa a **+6,66%** (era +7,22% stamattina, +7,83% il 06/09): la coda −0,63% dal 04/09 e'
|
||||||
|
marcatura delle due long, trading da arming **+$13,82**.
|
||||||
|
|
||||||
|
L'operatore ha detto che era **l'intero versamento previsto**: la voce «in attesa di importo e
|
||||||
|
data» di §1 e' chiusa. Resta la lezione di §2: per sei giorni il movimento e' stato solo
|
||||||
|
rilevato, la regola diceva il giorno stesso.
|
||||||
|
|
||||||
|
## Punto 4 — SCALA-01: la data era il punto 5 della SPEC scambiato per il gate
|
||||||
|
|
||||||
|
«Non prima del 2026-10-01» erano i 30 giorni di sorvegliante del punto 5 (§7 della SPEC); A2
|
||||||
|
chiede **≥180 giorni** col criterio passato ogni giorno, e il criterio lo misura solo
|
||||||
|
`scale_watch`, attivo dal 01/09. Le tre letture possibili, con pro e contro, sono nel diario di
|
||||||
|
sessione; l'operatore ha scelto **dal 01/09 → 2027-02-28**. Contare dall'arming avrebbe dato
|
||||||
|
un criterio ricostruito a posteriori, che non prova nulla sul sorvegliante; ridurre A2 a 30 giorni
|
||||||
|
avrebbe allentato un gate senza un costo d'attesa che lo giustifichi (il gradino 1,25x vale meno
|
||||||
|
di €100/mese di versamento).
|
||||||
|
|
||||||
|
Riconciliata anche la data d'arming: **20/06 = TP01 da solo** (commit `4650aa7`), **23/06 = il
|
||||||
|
BOOK** (commit `db738bc`, prima lettura di equity 23/06 22:00Z). CLAUDE.md §0 diceva solo la
|
||||||
|
prima, `config/live.json` e il TWR usano la seconda: erano due eventi, non un errore.
|
||||||
|
|
||||||
|
## Punto 5 — PREVDAY-01 aveva la data; non l'aveva la tabella
|
||||||
|
|
||||||
|
`RESULTS-0822` §54: decisione **2027-06-21**, kill, veto. La tabella §4 diceva «scritto 23/08 ·
|
||||||
|
10 condizioni», e il revisore ha letto quella. Ora la riga ha data, kill, veto e stato (5/7:
|
||||||
|
(a) e (b) sono strutturali, nessun forward le cambia).
|
||||||
|
|
||||||
|
Cablato in `paper_prevday.py` (prima esisteva solo nel testo, a differenza di `paper_dvolspread`):
|
||||||
|
|
||||||
|
| misura | valore al 10/09 |
|
||||||
|
|---|---|
|
||||||
|
| Sharpe forward **giornaliero** (lente del kill; l'oraria +1,19 non si cita, §54) | **+0,95** su 81 giorni, 81 attivi |
|
||||||
|
| kill (< −0,50 su ≥180 g attivi) | **NON MATURO**, leggibile dal ~18/12/2026 |
|
||||||
|
| barre ricostruibili dal feed di oggi | **1.942/1.943** (99,9%); l'unica divergente e' il 26/08 00:00, il giorno del fix di `advance()` |
|
||||||
|
| veto | ok |
|
||||||
|
|
||||||
|
⚠️ **Errore mio, catturato prima di committare:** la prima stesura del veto leggeva «divergenze
|
||||||
|
non crescenti» come `tasso_recente > tasso_totale`, e con UNA barra divergente negli ultimi 30
|
||||||
|
giorni (0,03/g contro 0,01/g) dichiarava VETO. Una guardia piu' stretta del contratto produce
|
||||||
|
allarmi che si impara a ignorare (P14): ora la crescita richiede **≥5 barre recenti E tasso
|
||||||
|
doppio**, dichiarato nel codice e provato nel test con i numeri della serie vera.
|
||||||
|
|
||||||
|
La ricostruzione al 99,9% contro il 95,6% di §54: la misura del 23/08 era stata fatta *prima*
|
||||||
|
del fix di `advance()` del 26/08 (le 67 barre divergenti erano «all'ora del cron», cioe' barre
|
||||||
|
parziali); oggi il feed rivisto e la serie coincidono tranne quella barra.
|
||||||
|
|
||||||
|
## Cose trovate per strada
|
||||||
|
|
||||||
|
- Due timestamp «di comodo» scritti a mano (13:05Z e 13:07Z) invece dell'ora letta (12:57:43Z e
|
||||||
|
13:04:09Z): corretti subito. E' esattamente la regola globale «la data si LEGGE» — vale anche per
|
||||||
|
chi la scrive nel campo `correzione`.
|
||||||
|
- `issue --json` va **prima** del sottocomando; il percorso nella regola globale era
|
||||||
|
`/opt/docker/AI-OS` invece di `/opt/AI-OS` (corretto la mattina, nel test dello strumento).
|
||||||
|
|
||||||
|
## Verifica
|
||||||
|
|
||||||
|
`uv run pytest`: **1060 passati** (erano 1031). Nuovi: `test_cuscino_watch.py` (14),
|
||||||
|
`test_paper_prevday_gate.py` (8), +5 in `test_usde_watch.py`. Revisione del diff affidata a
|
||||||
|
`fable` (agente fresco): esito e correzioni applicate in coda a questo diario.
|
||||||
|
|
||||||
|
## Revisione `fable` (13:10-13:18Z): 15 segnalazioni, 4 medie — tutte verificate, 12 applicate
|
||||||
|
|
||||||
|
| # | segnalazione | verifica | esito |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | `puo_riconvertire` contava anche i NON tentativi (`tentata=False`): un giro `--secco` o un prezzo illeggibile bloccava la riconversione vera per 6h | riprodotta | filtro su `tentata` + test |
|
||||||
|
| 2 | il tetto del venue in `usde_convert.piano` bloccava anche la **vendita**: con `venue_cap_frac` rimesso in config la riconversione automatica sarebbe stata morta | riprodotta (`side=sell`, `ok=False`) | vale solo in acquisto |
|
||||||
|
| 3 | `MARGINE_RIPRISTINO` 10% == `PREAVVISO_FRAC` 10%: dopo ogni 🚨 un ⚠️ per costruzione (P14) | riprodotta col floor di 1 USDE | margine **20%** → quota **0,64**, test |
|
||||||
|
| 4 | `--secco` scriveva nel registro vivo (veicolo del punto 1, e maschera un cron fermo) | riprodotta (riga 13:00:15Z) | non scrive; riga tolta |
|
||||||
|
| 5 | due bersagli di quota: `quota_target` 0,70 in config contro 0,64 del ripristino; il ripristino e' un **ratchet verso il basso** | letto | dichiarato in CLAUDE.md §5.17 e nel docstring: decisione dell'operatore (N9) |
|
||||||
|
| 7 | buco di ≤5 min dentro la tolleranza della finestra 24h | letto | dichiarato (D5) + fonte Deribit documentata |
|
||||||
|
| 8 | `giorni_attivi` legge posizioni REAL, il kill e' MODELED | letto | dichiarato nel docstring (81/81 oggi) |
|
||||||
|
| 10 | il test del cron leggeva un commento nello `.sh`, non la crontab | letto | legge `crontab -l` come `test_book_cadenza` |
|
||||||
|
| 11 | `prevday_target` patchato senza `monkeypatch` | letto | `monkeypatch` |
|
||||||
|
| 13 | «QUATTRO movimenti» in §2: sono TRE, in quattro tratti | letto | corretto |
|
||||||
|
| 14 | «5/7» contro «8/10» di §54: base diversa non dichiarata | letto | dichiarata |
|
||||||
|
| 6, 9, 12, 15 | verifiche che reggono (`sys.executable`, `cwd`, `piano()` in SCOPERTO, ricostruzione ≡ `advance`) | — | — |
|
||||||
|
|
||||||
|
Non applicato: il lock fra cron e conversione a mano (DUBBIO 2) — dichiarato come limite nel
|
||||||
|
docstring. Il DUBBIO 1 (ratchet) e' la domanda aperta per l'operatore.
|
||||||
@@ -8,6 +8,9 @@
|
|||||||
> `data/live/scale_history.jsonl`, `tests/test_book_scale.py` (T1-T11, **18 test verdi**).
|
> `data/live/scale_history.jsonl`, `tests/test_book_scale.py` (T1-T11, **18 test verdi**).
|
||||||
> 🚨 **`config/live.json` NON e' stato toccato**, come prescrive il punto 2: la chiave e' assente,
|
> 🚨 **`config/live.json` NON e' stato toccato**, come prescrive il punto 2: la chiave e' assente,
|
||||||
> vale 1,00, e T7 dimostra bit-exact che il libro e' quello di sempre (`max|diff| = 0.0`).
|
> vale 1,00, e T7 dimostra bit-exact che il libro e' quello di sempre (`max|diff| = 0.0`).
|
||||||
|
> 📌 **(10/09, issue #5) A2 conta dal 2026-09-01**, primo giorno di `scale_watch`: 180 giorni cadono il
|
||||||
|
> **2027-02-28**, ed e' la data «non prima del» del gate in CLAUDE.md §4. Il «2026-10-01» scritto il 01/09 era
|
||||||
|
> il punto 5 (30 giorni di sorvegliante) scambiato per la data del gate: incompatibile con A2 per costruzione.
|
||||||
> **Restano i punti 5-7:** ≥30 giorni a 1,00 col sorvegliante attivo, poi `GATE SCALA-01`, poi
|
> **Restano i punti 5-7:** ≥30 giorni a 1,00 col sorvegliante attivo, poi `GATE SCALA-01`, poi
|
||||||
> `r0726_fee_sensitivity` rifatto. Il punto 5 non e' burocrazia — e' l'unico modo di scoprire che il
|
> `r0726_fee_sensitivity` rifatto. Il punto 5 non e' burocrazia — e' l'unico modo di scoprire che il
|
||||||
> sorvegliante e' rotto mentre la leva e' ancora 1,00.
|
> sorvegliante e' rotto mentre la leva e' ancora 1,00.
|
||||||
|
|||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Sorveglianza ORARIA del cuscino USDC di regolamento (debito §5.17, issue #3) — v2.0.0+.
|
||||||
|
# Minuto :53 = DOPO il giro del book (:47, i cui fill si regolano in USDC) e fuori dal minuto
|
||||||
|
# tondo (rate-limit per-IP), dal :25 (catena), dal :35 (usde) e dal :42 (balance).
|
||||||
|
# PUO' INVIARE ORDINI: sotto zero di slack lancia usde_convert --esegui (vende USDE), con le
|
||||||
|
# guardie dichiarate nel docstring di scripts/live/cuscino_watch.py; lo interruttore e' lo
|
||||||
|
# stesso del libro (execution_enabled in config/live.json).
|
||||||
|
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_cuscino ====="
|
||||||
|
uv run python scripts/live/cuscino_watch.py --quiet || true
|
||||||
|
echo "===== done $(date -u '+%H:%M:%SZ') ====="
|
||||||
|
} >> logs/cron_cuscino.log 2>&1
|
||||||
Executable
+289
@@ -0,0 +1,289 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""cuscino_watch.py — sorveglianza ORARIA del cuscino USDC di regolamento. PUO' INVIARE ORDINI.
|
||||||
|
|
||||||
|
PERCHE' ESISTE (debito §5.17, issue #3). Dal 06/09 il 68,7% dell'equity e' USDE. P&L e funding dei
|
||||||
|
perp USDC-lineari si regolano in USDC: una perdita del libro consuma il cuscino e fa salire la quota
|
||||||
|
USDE DA SOLA, senza un ordine. `usde_watch` allerta solo su `quota > quota_max_frac` (0,85): a
|
||||||
|
68,7% lo slack sul cuscino era ~$41-58, e un saldo USDC negativo costa lo 0,05%/GIORNO (18,25%/anno,
|
||||||
|
4,3x la resa che l'USDE compra). Nessuno guardava la grandezza giusta.
|
||||||
|
|
||||||
|
LA MISURA (decisione dell'operatore, 10/09): l'EQUITY USDC del conto — balance piu' P&L non
|
||||||
|
realizzato — perche' e' quella che risponde del regolamento; il balance vedrebbe il buco solo a
|
||||||
|
trade chiuso. Il cuscino richiesto e' DERIVATO da config/live.json e src/live/book.py tramite
|
||||||
|
`usde.cuscino_richiesto_usd` (n_asset x frac x disaster_sl_pct x equity totale): la stessa formula
|
||||||
|
che `usde_convert` applica a ogni piano — un sorvegliante che ridichiara il proprio bersaglio non
|
||||||
|
sta controllando niente (P1, 5 occorrenze nel progetto).
|
||||||
|
|
||||||
|
TRE STATI, TRE AZIONI (P5: distinguere anche quando l'azione e' la stessa):
|
||||||
|
* OK — slack >= PREAVVISO_FRAC x cuscino: niente.
|
||||||
|
* PREAVVISO — 0 <= slack < 10% del cuscino: ⚠️ alla transizione. E' latenza comprata apposta:
|
||||||
|
un'ora in perdita non deve trovare il cuscino gia' scoperto.
|
||||||
|
* SCOPERTO — slack < 0: 🚨 alla transizione, e RICONVERSIONE AUTOMATICA (decisione
|
||||||
|
dell'operatore, 10/09): lancia `usde_convert --quota q --esegui` con
|
||||||
|
q = usde.quota_ripristino(MARGINE_RIPRISTINO), cioe' la quota che lascia il cuscino
|
||||||
|
piu' un 20% di margine, cosi' non si torna sul bordo alla prima ora in perdita.
|
||||||
|
Vende USDE contro USDC (~6 bps di spread, fee 0).
|
||||||
|
* BLIND — conto non leggibile: ⚠️ alla transizione. "Non vedo" non e' "va tutto bene".
|
||||||
|
|
||||||
|
LE GUARDIE DELLA RICONVERSIONE (tutte dichiarate qui):
|
||||||
|
* `execution_enabled` in config/live.json deve essere true — lo stesso interruttore del libro:
|
||||||
|
disarmare il libro disarma anche questo, senza un secondo posto da ricordare.
|
||||||
|
* NON si riconverte sotto `depeg_warn`: vendere USDE sotto la pari cristallizza la perdita del
|
||||||
|
collaterale; li' decide l'operatore, e l'allarme lo dice.
|
||||||
|
* al massimo UN tentativo ogni RIPRISTINO_MIN_ORE (6h): un tentativo fallito che si ripete ogni
|
||||||
|
ora e' spam, e un sorvegliante che si ripete si impara a ignorare (P14).
|
||||||
|
* `usde_convert` porta le proprie guardie (banda di prezzo, book leggibile, tetto HARD sulla
|
||||||
|
quota): questo script NON le duplica, le eredita lanciando lo script vero (P15: la regola si
|
||||||
|
prova contro il codice che la esegue).
|
||||||
|
* `--secco`: niente ordini, niente Telegram, NIENTE riga nel registro: stampa cosa FAREBBE.
|
||||||
|
* niente lock: un `usde_convert --esegui` lanciato a mano nello stesso minuto del cron (:53)
|
||||||
|
puo' vendere due volte verso bersagli diversi. Limite dichiarato (D5), non riparato.
|
||||||
|
|
||||||
|
LIMITE DICHIARATO — IL RIPRISTINO E' UN RATCHET VERSO IL BASSO. Dopo una riconversione la quota
|
||||||
|
resta a `quota_ripristino` (~0,64) e nessuno la riporta a `usde.quota_target` (0,70): ricomprare
|
||||||
|
USDE e' una decisione dell'operatore (`usde_convert --quota 0.70`), non un automatismo di questo
|
||||||
|
script. E a 0,70 lo slack e' ZERO per costruzione (§1: «coincidenza per costruzione, zero slack»),
|
||||||
|
quindi PREAVVISO e' lo stato NORMALE del conto a quota piena — allerta una volta, alla transizione.
|
||||||
|
|
||||||
|
TRASPORTO (lezione del debito §5.2): il marcatore "gia' detto" e' l'esito di `notify` scritto nel
|
||||||
|
record (`allertato`); se l'invio fallisce la transizione si ripete al giro dopo invece di andare
|
||||||
|
persa per l'episodio intero.
|
||||||
|
|
||||||
|
La serie data/live/cuscino_watch.jsonl e' dentro il perimetro di backup ed e' sorvegliata da
|
||||||
|
monitor_health (un watch fermo = nessuno guarda il cuscino, e il silenzio si legge come zero).
|
||||||
|
|
||||||
|
uv run python scripts/live/cuscino_watch.py # un giro: legge, giudica, agisce
|
||||||
|
uv run python scripts/live/cuscino_watch.py --quiet # stampa solo se cambia qualcosa
|
||||||
|
uv run python scripts/live/cuscino_watch.py --secco # nessun ordine, nessun Telegram
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
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 import usde as U # noqa: E402
|
||||||
|
from src.live.notifier import notify # noqa: E402
|
||||||
|
|
||||||
|
STATE = ROOT / "data" / "live" / "cuscino_watch.jsonl"
|
||||||
|
CONVERT = ROOT / "scripts" / "live" / "usde_convert.py"
|
||||||
|
PREAVVISO_FRAC = 0.10 # sotto il 10% di slack (in frazione del cuscino) si preavvisa
|
||||||
|
MARGINE_RIPRISTINO = 0.20 # la riconversione lascia cuscino x 1,20: DOPPIO del preavviso, cosi'
|
||||||
|
# dopo il ripristino lo stato e' OK e non PREAVVISO per costruzione
|
||||||
|
# (revisione fable 10/09: a 0,10 == PREAVVISO_FRAC il floor di
|
||||||
|
# usde_convert lasciava il conto sotto la soglia)
|
||||||
|
RIPRISTINO_MIN_ORE = 6.0 # un tentativo al massimo ogni 6 ore
|
||||||
|
TIMEOUT_CONVERT_S = 900 # usde_convert spazia gli ordini di 12s: 60 ordini = 12 min
|
||||||
|
|
||||||
|
|
||||||
|
def leggi(path: Path = STATE) -> list[dict]:
|
||||||
|
if not path.exists():
|
||||||
|
return []
|
||||||
|
out = []
|
||||||
|
for ln in path.read_text().splitlines():
|
||||||
|
ln = ln.strip()
|
||||||
|
if ln:
|
||||||
|
out.append(json.loads(ln))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def giudica(eq_usdc: float | None, equity_tot: float | None, cuscino: float | None,
|
||||||
|
preavviso_frac: float = PREAVVISO_FRAC) -> dict:
|
||||||
|
"""PURA. -> stato, slack, soglia di preavviso. `None` in ingresso = BLIND."""
|
||||||
|
if eq_usdc is None or equity_tot is None or cuscino is None:
|
||||||
|
return dict(stato="BLIND", slack=None, preavviso_usd=None)
|
||||||
|
slack = eq_usdc - cuscino
|
||||||
|
preavviso = preavviso_frac * cuscino
|
||||||
|
if slack < 0:
|
||||||
|
stato = "SCOPERTO"
|
||||||
|
elif slack < preavviso:
|
||||||
|
stato = "PREAVVISO"
|
||||||
|
else:
|
||||||
|
stato = "OK"
|
||||||
|
return dict(stato=stato, slack=round(slack, 2), preavviso_usd=round(preavviso, 2))
|
||||||
|
|
||||||
|
|
||||||
|
def transizione(prev: dict | None, stato: str) -> bool:
|
||||||
|
"""PURA. Si allerta se lo stato e' cambiato, O se l'ultima allerta per questo stato non e'
|
||||||
|
partita (`allertato` False): un invio fallito non consuma la transizione (debito §5.2)."""
|
||||||
|
if stato == "OK":
|
||||||
|
return False
|
||||||
|
if prev is None or prev.get("stato") != stato:
|
||||||
|
return True
|
||||||
|
return not bool(prev.get("allertato"))
|
||||||
|
|
||||||
|
|
||||||
|
def puo_riconvertire(prev_records: list[dict], now_ms: int, execution_enabled: bool,
|
||||||
|
px: float | None, depeg_warn: float,
|
||||||
|
min_ore: float = RIPRISTINO_MIN_ORE) -> tuple[bool, str]:
|
||||||
|
"""PURA. Le guardie della riconversione automatica. -> (si', motivo se no)."""
|
||||||
|
if not execution_enabled:
|
||||||
|
return False, "execution_enabled=false in config/live.json (il libro e' disarmato: anche questo)"
|
||||||
|
if px is None:
|
||||||
|
return False, "prezzo USDE non leggibile: non si vende al buio (P5)"
|
||||||
|
if px < depeg_warn:
|
||||||
|
return False, f"USDE {px:.4f} sotto depeg_warn {depeg_warn}: vendere ora cristallizza la perdita — decide l'operatore"
|
||||||
|
# contano solo i tentativi VERI (subprocess lanciato): un record con `tentata=False` — prezzo
|
||||||
|
# illeggibile, --secco, interruttore spento — non consuma il budget (revisione fable 10/09)
|
||||||
|
ultimo = [r for r in prev_records if (r.get("riconversione") or {}).get("tentata")]
|
||||||
|
if ultimo:
|
||||||
|
ore = (now_ms - int(ultimo[-1]["ts"])) / 3_600_000
|
||||||
|
if ore < min_ore:
|
||||||
|
return False, f"ultimo tentativo {ore:.1f}h fa (< {min_ore:.0f}h): non si insiste ogni ora"
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
|
def riconverti(quota: float, timeout_s: float = TIMEOUT_CONVERT_S) -> dict:
|
||||||
|
"""Lancia lo script VERO con le sue guardie. Ritorna esito + coda dell'output (P3)."""
|
||||||
|
cmd = [sys.executable, str(CONVERT), "--quota", f"{quota:.4f}", "--esegui"]
|
||||||
|
try:
|
||||||
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_s, cwd=str(ROOT))
|
||||||
|
coda = (r.stdout + r.stderr).strip().splitlines()[-12:]
|
||||||
|
return dict(rc=r.returncode, ok=r.returncode == 0, coda=coda, cmd=" ".join(cmd[1:]))
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return dict(rc=None, ok=False, coda=[f"timeout dopo {timeout_s:.0f}s"], cmd=" ".join(cmd[1:]))
|
||||||
|
except Exception as e: # noqa: BLE001 — si registra, non si ingoia
|
||||||
|
return dict(rc=None, ok=False, coda=[f"{type(e).__name__}: {e}"], cmd=" ".join(cmd[1:]))
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_client():
|
||||||
|
try:
|
||||||
|
from src.live.deribit import DeribitRead
|
||||||
|
return DeribitRead()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _execution_enabled() -> bool:
|
||||||
|
try:
|
||||||
|
return bool(json.loads((ROOT / "config" / "live.json").read_text()).get("execution_enabled"))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
quiet, secco = "--quiet" in sys.argv, "--secco" in sys.argv
|
||||||
|
c = U.cfg()
|
||||||
|
records = leggi()
|
||||||
|
prev = records[-1] if records else None
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
now_ms = int(now.timestamp() * 1000)
|
||||||
|
client = _safe_client()
|
||||||
|
|
||||||
|
eq_usdc = eq_usde = None
|
||||||
|
motivo_blind = None
|
||||||
|
if client is None:
|
||||||
|
motivo_blind = "gateway non raggiungibile"
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
eq_usdc = float(client.account_summary("USDC")["equity"])
|
||||||
|
except Exception as e:
|
||||||
|
motivo_blind = f"conto USDC non leggibile ({type(e).__name__})"
|
||||||
|
try:
|
||||||
|
eq_usde = float(client.account_summary("USDE").get("equity") or 0)
|
||||||
|
except Exception as e:
|
||||||
|
motivo_blind = motivo_blind or f"conto USDE non leggibile ({type(e).__name__})"
|
||||||
|
px, px_fonte = U.prezzo(client)
|
||||||
|
equity_tot = cuscino = come = None
|
||||||
|
if eq_usdc is not None and eq_usde is not None:
|
||||||
|
usd, _ = U.valuta(eq_usde, px)
|
||||||
|
equity_tot = eq_usdc + usd
|
||||||
|
cuscino, come = U.cuscino_richiesto_usd(equity_tot)
|
||||||
|
g = giudica(eq_usdc, equity_tot, cuscino)
|
||||||
|
stato = g["stato"]
|
||||||
|
|
||||||
|
rec = dict(ts=now_ms, data=now.strftime("%Y-%m-%dT%H:%M:%SZ"), stato=stato,
|
||||||
|
motivo_blind=motivo_blind, eq_usdc=eq_usdc, eq_usde=eq_usde, px=px,
|
||||||
|
px_fonte=px_fonte, equity_tot=equity_tot, cuscino=cuscino, cuscino_come=come,
|
||||||
|
slack=g["slack"], preavviso_usd=g["preavviso_usd"], allertato=None,
|
||||||
|
riconversione=None, secco=secco)
|
||||||
|
|
||||||
|
# --- riconversione automatica: solo SCOPERTO, solo se le guardie lo permettono ---
|
||||||
|
if stato == "SCOPERTO":
|
||||||
|
ok, perche = puo_riconvertire(records, now_ms, _execution_enabled(), px, c["depeg_warn"])
|
||||||
|
quota = U.quota_ripristino(MARGINE_RIPRISTINO)
|
||||||
|
if not ok:
|
||||||
|
rec["riconversione"] = dict(tentata=False, quota=quota, motivo=perche)
|
||||||
|
elif secco:
|
||||||
|
rec["riconversione"] = dict(tentata=False, quota=quota,
|
||||||
|
motivo=f"--secco: avrei lanciato usde_convert --quota {quota:.4f} --esegui")
|
||||||
|
else:
|
||||||
|
esito = riconverti(quota)
|
||||||
|
rec["riconversione"] = dict(tentata=True, quota=quota, **esito)
|
||||||
|
|
||||||
|
# --- allarmi: alla transizione, marcatore scritto con l'esito dell'invio ---
|
||||||
|
if transizione(prev, stato):
|
||||||
|
if secco:
|
||||||
|
rec["allertato"] = False
|
||||||
|
elif stato == "SCOPERTO":
|
||||||
|
r = rec["riconversione"] or {}
|
||||||
|
azione = ("riconversione " + ("ESEGUITA" if r.get("ok") else "FALLITA")
|
||||||
|
+ f" (quota -> {r.get('quota', 0):.0%})" if r.get("tentata")
|
||||||
|
else f"NON riconvertito: {r.get('motivo')}")
|
||||||
|
rec["allertato"] = notify("🚨 CUSCINO USDC SCOPERTO", {
|
||||||
|
"USDC equity": f"${eq_usdc:,.2f}", "cuscino richiesto": f"${cuscino:,.2f} ({come})",
|
||||||
|
"slack": f"${g['slack']:+,.2f}", "azione": azione,
|
||||||
|
"nota": "un USDC negativo costa 0,05%/giorno; il libro si regola in USDC"}, tentativi=3)
|
||||||
|
elif stato == "PREAVVISO":
|
||||||
|
rec["allertato"] = notify("⚠️ cuscino USDC in esaurimento", {
|
||||||
|
"USDC equity": f"${eq_usdc:,.2f}", "cuscino richiesto": f"${cuscino:,.2f}",
|
||||||
|
"slack": f"${g['slack']:+,.2f} (< {PREAVVISO_FRAC:.0%} del cuscino)",
|
||||||
|
"nota": f"sotto zero riconverte da solo a quota {U.quota_ripristino(MARGINE_RIPRISTINO):.0%}"})
|
||||||
|
elif stato == "BLIND":
|
||||||
|
rec["allertato"] = notify("⚠️ cuscino_watch BLIND", {"motivo": motivo_blind or "?",
|
||||||
|
"nota": "'non vedo' non e' 'va tutto bene' (P5)"})
|
||||||
|
elif stato == "SCOPERTO" and rec["riconversione"] and rec["riconversione"].get("tentata") and not secco:
|
||||||
|
# non e' una transizione ma e' un ordine mandato: si dice sempre
|
||||||
|
r = rec["riconversione"]
|
||||||
|
rec["allertato"] = notify("📌 cuscino USDC: riconversione " + ("eseguita" if r["ok"] else "FALLITA"),
|
||||||
|
{"quota bersaglio": f"{r['quota']:.0%}", "esito": " | ".join(r["coda"][-3:])})
|
||||||
|
|
||||||
|
# --secco e' secco ANCHE sul registro: un giro a mano non deve diventare il `prev` del cron,
|
||||||
|
# ne' far sembrare vivo un monitor fermo (revisione fable 10/09)
|
||||||
|
if not secco:
|
||||||
|
STATE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with STATE.open("a") as fh:
|
||||||
|
fh.write(json.dumps(rec) + "\n")
|
||||||
|
|
||||||
|
cambiato = prev is None or prev.get("stato") != stato or rec["riconversione"] is not None
|
||||||
|
if not quiet or cambiato:
|
||||||
|
print("=" * 78)
|
||||||
|
print(f" CUSCINO WATCH — {rec['data']} {'(SECCO)' if secco else ''}")
|
||||||
|
print("=" * 78)
|
||||||
|
if stato == "BLIND":
|
||||||
|
print(f" stato : BLIND — {motivo_blind}")
|
||||||
|
else:
|
||||||
|
print(f" conto : USDC equity ${eq_usdc:,.2f} + USDE {eq_usde:,.2f} @ {px if px else 'n/d'}"
|
||||||
|
f" ({px_fonte}) = ${equity_tot:,.2f}")
|
||||||
|
print(f" cuscino : ${cuscino:,.2f} richiesti ({come})")
|
||||||
|
print(f" slack : ${g['slack']:+,.2f} (preavviso sotto ${g['preavviso_usd']:,.2f})")
|
||||||
|
print(f" stato : {stato}")
|
||||||
|
if rec["riconversione"]:
|
||||||
|
r = rec["riconversione"]
|
||||||
|
print(f" riconvers. : {'TENTATA' if r.get('tentata') else 'NON tentata'} — quota {r['quota']:.4f}"
|
||||||
|
+ (f" — {r.get('motivo')}" if r.get("motivo") else ""))
|
||||||
|
for ln in r.get("coda", []):
|
||||||
|
print(f" {ln}")
|
||||||
|
if rec["allertato"] is not None:
|
||||||
|
print(f" allerta : {'inviata' if rec['allertato'] else 'NON inviata (si ripete al giro dopo)'}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
USO = """uso: cuscino_watch.py [--quiet] [--secco]
|
||||||
|
|
||||||
|
cuscino_watch.py — sorveglianza ORARIA del cuscino USDC di regolamento; sotto zero riconverte.
|
||||||
|
uv run python scripts/live/cuscino_watch.py # un giro: legge, giudica, agisce
|
||||||
|
uv run python scripts/live/cuscino_watch.py --quiet # stampa solo se cambia qualcosa
|
||||||
|
uv run python scripts/live/cuscino_watch.py --secco # nessun ordine, nessun Telegram
|
||||||
|
|
||||||
|
Dettaglio nel docstring in testa al file."""
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("cuscino_watch.py", USO, flag=("--quiet", "--secco"), con_valore=())
|
||||||
|
sys.exit(main())
|
||||||
@@ -53,6 +53,21 @@ MODELED_CAPITAL = 2000.0 # nominale, ribilanciamento continuo
|
|||||||
REAL_CAPITAL = 600.0 # capitale mainnet reale
|
REAL_CAPITAL = 600.0 # capitale mainnet reale
|
||||||
MIN_ORDER = 5.0 # min order Deribit -> sotto, il conto vero NON ribilancia
|
MIN_ORDER = 5.0 # min order Deribit -> sotto, il conto vero NON ribilancia
|
||||||
|
|
||||||
|
# --- GATE PRE-REGISTRATO `GATE PREVDAY-01` (2026-08-23, RESULTS-0822 §54) -----------------------
|
||||||
|
# Cablato qui il 2026-09-10 (issue #4): esisteva solo nel testo, e la revisione del 09/09 lo ha
|
||||||
|
# letto come "gate senza data". Decisione dell'operatore: tenere la data, cablare kill e veto.
|
||||||
|
GATE_DATE = "2027-06-21" # decisione piena: 7 condizioni (a)-(g), tutte necessarie
|
||||||
|
KILL_SHARPE = -0.50 # kill: Sharpe forward < -0,50 ...
|
||||||
|
KILL_MIN_ACTIVE_DAYS = 180 # ... su >= 180 giorni di barre ATTIVE (posizione != 0)
|
||||||
|
MIN_RECON_FRAC = 0.80 # veto d'integrita': >= 80% di barre ricostruibili dal feed
|
||||||
|
RECON_TOL = 1.5e-6 # net_modeled e' registrato a 6 decimali: mezzo ulp di tolleranza
|
||||||
|
RECENTI_GIORNI = 30 # "i minuti non registrati non devono crescere": ultimi 30g vs tutto
|
||||||
|
MIN_DIV_CRESCITA = 5 # ... ma UNA barra divergente su 1943 non e' una crescita (P14): la
|
||||||
|
# crescita richiede >= 5 divergenti recenti E tasso doppio del totale
|
||||||
|
# LENTE: il kill si misura sullo Sharpe GIORNALIERO (somma delle barre orarie per giorno UTC),
|
||||||
|
# la lente con cui e' misurato ogni altro sleeve del progetto — §54: sulla lente ORARIA lo stesso
|
||||||
|
# forward vale +2,04 contro +1,56 giornaliero. Al kill la famiglia NON si ri-ottimizza.
|
||||||
|
|
||||||
|
|
||||||
def build_bars() -> dict[str, pd.DataFrame]:
|
def build_bars() -> dict[str, pd.DataFrame]:
|
||||||
return {a: load(a, "1h").reset_index(drop=True) for a in ASSETS}
|
return {a: load(a, "1h").reset_index(drop=True) for a in ASSETS}
|
||||||
@@ -145,6 +160,151 @@ def advance(st: dict, dfs: dict) -> dict:
|
|||||||
return st
|
return st
|
||||||
|
|
||||||
|
|
||||||
|
def _returns_df() -> pd.DataFrame | None:
|
||||||
|
if not RETURNS_FILE.exists():
|
||||||
|
return None
|
||||||
|
r = pd.read_json(RETURNS_FILE, lines=True)
|
||||||
|
if r.empty:
|
||||||
|
return None
|
||||||
|
r["dt"] = pd.to_datetime(r["ts"], unit="ms", utc=True)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def sharpe_giornaliero(r: pd.DataFrame, col: str = "net_modeled") -> tuple[float, int]:
|
||||||
|
"""PURA. Sharpe annualizzato sui rendimenti GIORNALIERI (somma delle barre orarie per giorno
|
||||||
|
UTC) -> (sharpe, n_giorni). nan sotto 30 giorni o a varianza nulla."""
|
||||||
|
d = r.groupby(r["dt"].dt.floor("D"))[col].sum()
|
||||||
|
if len(d) < 30 or d.std() == 0:
|
||||||
|
return float("nan"), int(len(d))
|
||||||
|
return float(d.mean() / d.std() * np.sqrt(365.25)), int(len(d))
|
||||||
|
|
||||||
|
|
||||||
|
def giorni_attivi(r: pd.DataFrame) -> int:
|
||||||
|
"""PURA. Giorni UTC con almeno una barra a posizione != 0 (il kill conta le barre ATTIVE).
|
||||||
|
⚠️ le posizioni registrate sono quelle del libro REAL-$600 (advance scrive `pr`), lo Sharpe
|
||||||
|
del kill e' MODELED: divergono solo se un ingresso da 0 vale < $5 di nozionale, cosa che a
|
||||||
|
vol-target 20% non succede (10/09: 81/81 giorni). Dichiarato, non riparato (D5)."""
|
||||||
|
att = r[(r["pos_btc"] != 0) | (r["pos_eth"] != 0)]
|
||||||
|
return int(att["dt"].dt.floor("D").nunique())
|
||||||
|
|
||||||
|
|
||||||
|
def kill_check(sharpe: float, n_attivi: int, today: pd.Timestamp) -> tuple[str, str]:
|
||||||
|
"""PURA. -> (stato, motivo). KILL solo con >= 180 giorni attivi E Sharpe < -0,50 (§54):
|
||||||
|
a quell'orizzonte P(uccidere un edge vivo a Sharpe 1,2) = 9,7%."""
|
||||||
|
if n_attivi < KILL_MIN_ACTIVE_DAYS:
|
||||||
|
return "NON_MATURO", (f"{n_attivi} giorni attivi < {KILL_MIN_ACTIVE_DAYS}: il kill non e' "
|
||||||
|
f"leggibile (mancano {KILL_MIN_ACTIVE_DAYS - n_attivi} giorni)")
|
||||||
|
if not np.isfinite(sharpe):
|
||||||
|
return "NON_MISURABILE", "Sharpe forward non misurabile"
|
||||||
|
if sharpe < KILL_SHARPE:
|
||||||
|
return "KILL", f"Sharpe {sharpe:+.2f} < {KILL_SHARPE:+.2f} su {n_attivi} giorni attivi -> RITIRARE, senza ri-ottimizzare"
|
||||||
|
return "SUPERATO", f"Sharpe {sharpe:+.2f} >= {KILL_SHARPE:+.2f} su {n_attivi} giorni attivi"
|
||||||
|
|
||||||
|
|
||||||
|
def ricostruzione(r: pd.DataFrame, dfs: dict, start_ts: int) -> dict:
|
||||||
|
"""Rigioca la strategia CONGELATA sul feed di OGGI dallo `start_ts` del monitor e confronta
|
||||||
|
barra per barra `net_modeled` registrato con quello ricalcolato (ribilanciamento continuo,
|
||||||
|
la stessa aritmetica di `advance`). Una barra e' RICOSTRUIBILE se coincide entro RECON_TOL.
|
||||||
|
|
||||||
|
Cosa misura: quante barre registrate sono riproducibili dal dato certificato. Le divergenti
|
||||||
|
sono barre calcolate su un feed che il rebuild ha poi rivisto (§54: 67 su 1512, tutte
|
||||||
|
all'ora del cron) — e' la misura del veto d'integrita' (>= 80%, e i minuti persi non devono
|
||||||
|
crescere: qui, divergenti/giorno negli ultimi 30g contro l'intera finestra)."""
|
||||||
|
data = {}
|
||||||
|
for a in ASSETS:
|
||||||
|
df = dfs[a]
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
rr = np.zeros(len(c)); rr[1:] = c[1:] / c[:-1] - 1.0
|
||||||
|
data[a] = dict(ts=df["timestamp"].values.astype("int64"), r=rr, tgt=prevday_target(df))
|
||||||
|
idx = {a: {int(t): i for i, t in enumerate(data[a]["ts"])} for a in ASSETS}
|
||||||
|
# posizione iniziale come init_state: target all'ultima barra <= start_ts
|
||||||
|
pos = {}
|
||||||
|
for a in ASSETS:
|
||||||
|
i0 = max(i for t, i in idx[a].items() if t <= start_ts)
|
||||||
|
pos[a] = float(data[a]["tgt"][i0])
|
||||||
|
ricalc, presenti = [], []
|
||||||
|
for t in r["ts"].astype("int64"):
|
||||||
|
t = int(t)
|
||||||
|
if any(t not in idx[a] for a in ASSETS):
|
||||||
|
ricalc.append(np.nan); presenti.append(False); continue
|
||||||
|
net = 0.0
|
||||||
|
for a in ASSETS:
|
||||||
|
i = idx[a][t]
|
||||||
|
tgt = float(data[a]["tgt"][i])
|
||||||
|
net += WEIGHT * (pos[a] * float(data[a]["r"][i]) - FEE_SIDE * abs(tgt - pos[a]))
|
||||||
|
pos[a] = tgt
|
||||||
|
ricalc.append(net); presenti.append(True)
|
||||||
|
ricalc = np.asarray(ricalc, dtype=float)
|
||||||
|
reg = r["net_modeled"].values.astype(float)
|
||||||
|
ok = np.isfinite(ricalc) & (np.abs(ricalc - reg) <= RECON_TOL)
|
||||||
|
n = len(reg)
|
||||||
|
giorni = r["dt"].dt.floor("D")
|
||||||
|
span_g = max(1, int(giorni.nunique()))
|
||||||
|
recenti = r["dt"] >= (r["dt"].max() - pd.Timedelta(days=RECENTI_GIORNI))
|
||||||
|
div_rec = int((~ok[recenti.values]).sum())
|
||||||
|
g_rec = max(1, int(giorni[recenti].nunique()))
|
||||||
|
return dict(n=n, ricostruibili=int(ok.sum()), frac=float(ok.sum() / n) if n else float("nan"),
|
||||||
|
assenti_nel_feed=int((~np.asarray(presenti)).sum()),
|
||||||
|
div_per_giorno=float((n - ok.sum()) / span_g),
|
||||||
|
div_recenti=div_rec, div_per_giorno_recenti=float(div_rec / g_rec),
|
||||||
|
divergenti_ts=[int(t) for t in r["ts"].values[~ok]][:20])
|
||||||
|
|
||||||
|
|
||||||
|
def veto_check(frac: float, div_g_tot: float, div_g_rec: float, div_rec: int) -> tuple[bool, str]:
|
||||||
|
"""PURA. Veto d'integrita' (blocca, non decide): sotto l'80% di barre ricostruibili, o con le
|
||||||
|
divergenze che CRESCONO negli ultimi 30 giorni, la finestra si ESTENDE invece di decidere."""
|
||||||
|
motivi = []
|
||||||
|
if not np.isfinite(frac) or frac < MIN_RECON_FRAC:
|
||||||
|
motivi.append(f"barre ricostruibili {frac:.1%} < {MIN_RECON_FRAC:.0%}")
|
||||||
|
if div_rec >= MIN_DIV_CRESCITA and div_g_rec > 2.0 * div_g_tot + 1e-12:
|
||||||
|
motivi.append(f"divergenze in crescita: {div_rec} negli ultimi {RECENTI_GIORNI}g "
|
||||||
|
f"({div_g_rec:.2f}/g contro {div_g_tot:.2f}/g sull'intera finestra)")
|
||||||
|
return (not motivi), ("; ".join(motivi) if motivi else
|
||||||
|
f"ricostruibili {frac:.1%}, divergenti {div_rec} negli ultimi {RECENTI_GIORNI}g "
|
||||||
|
f"({div_g_rec:.2f}/g contro {div_g_tot:.2f}/g totale)")
|
||||||
|
|
||||||
|
|
||||||
|
def print_gate(st: dict, dfs: dict, today: pd.Timestamp | None = None) -> dict:
|
||||||
|
"""Stampa lo stato del gate pre-registrato e lo RITORNA (per i test e per chi legge il log).
|
||||||
|
Non decide niente: al kill e alla data stampa cosa la regola dice, in maiuscolo."""
|
||||||
|
r = _returns_df()
|
||||||
|
out = dict(gate_date=GATE_DATE, kill="NON_MISURABILE", veto_ok=None)
|
||||||
|
print(f"\n GATE PRE-REGISTRATO `PREVDAY-01` (23/08, RESULTS-0822 §54) — decisione {GATE_DATE}")
|
||||||
|
if r is None or len(r) < 2:
|
||||||
|
print(" serie forward assente o troppo corta: niente da misurare")
|
||||||
|
return out
|
||||||
|
today = today or pd.Timestamp.now(tz="UTC").normalize()
|
||||||
|
sh, n_g = sharpe_giornaliero(r)
|
||||||
|
n_att = giorni_attivi(r)
|
||||||
|
stato, motivo = kill_check(sh, n_att, today)
|
||||||
|
out.update(sharpe_giornaliero=sh, n_giorni=n_g, n_attivi=n_att, kill=stato, kill_motivo=motivo)
|
||||||
|
print(f" Sharpe forward GIORNALIERO (MODELED): {sh:+.2f} su {n_g} giorni, {n_att} attivi"
|
||||||
|
f" [lente oraria {_sharpe_orario(r):+.2f}: non si cita, §54]")
|
||||||
|
if stato == "KILL":
|
||||||
|
print(f" *** KILL: {motivo} ***")
|
||||||
|
else:
|
||||||
|
print(f" kill (Sharpe < {KILL_SHARPE:+.2f} su >= {KILL_MIN_ACTIVE_DAYS}g attivi): {stato} — {motivo}")
|
||||||
|
rc = ricostruzione(r, dfs, int(st["start_ts"]))
|
||||||
|
ok, vm = veto_check(rc["frac"], rc["div_per_giorno"], rc["div_per_giorno_recenti"], rc["div_recenti"])
|
||||||
|
out.update(ricostruzione=rc, veto_ok=ok, veto_motivo=vm)
|
||||||
|
print(f" veto d'integrita': {'ok' if ok else '*** VETO — la finestra si ESTENDE ***'} — {vm}"
|
||||||
|
f" ({rc['ricostruibili']}/{rc['n']} barre, {rc['assenti_nel_feed']} assenti nel feed)")
|
||||||
|
if today >= pd.Timestamp(GATE_DATE, tz="UTC"):
|
||||||
|
print(f" *** DECISIONE DOVUTA ({GATE_DATE}): (a) DSR >= 0,95 sulla famiglia dichiarata "
|
||||||
|
f"(b) cella al buio == congelata (c) delta libro > +0,05 col funding (d) weights_tilt_null "
|
||||||
|
f"(e) ADDS+robust_oos+non-hedge sulla cella al buio (f) day_boundary_robust "
|
||||||
|
f"(g) Sharpe forward > 0 [{sh:+.2f}] — TUTTE necessarie ***")
|
||||||
|
else:
|
||||||
|
print(f" decisione fra {(pd.Timestamp(GATE_DATE, tz='UTC') - today).days} giorni; "
|
||||||
|
f"le condizioni (a)-(b) sono STRUTTURALI e il forward non le cambia (§54)")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _sharpe_orario(r: pd.DataFrame) -> float:
|
||||||
|
x = r["net_modeled"].astype(float)
|
||||||
|
return float(x.mean() / x.std() * np.sqrt(24 * 365.25)) if x.std() > 0 else float("nan")
|
||||||
|
|
||||||
|
|
||||||
def print_status(st: dict, dfs: dict):
|
def print_status(st: dict, dfs: dict):
|
||||||
days = (max(int(dfs[a]["timestamp"].iloc[-1]) for a in ASSETS) - st["start_ts"]) / 86400_000
|
days = (max(int(dfs[a]["timestamp"].iloc[-1]) for a in ASSETS) - st["start_ts"]) / 86400_000
|
||||||
rm = st["cap_modeled"] / MODELED_CAPITAL - 1
|
rm = st["cap_modeled"] / MODELED_CAPITAL - 1
|
||||||
@@ -156,7 +316,9 @@ def print_status(st: dict, dfs: dict):
|
|||||||
print(f" MODELED ($2000 nominale): {rm*100:+6.2f}% eq ${st['cap_modeled']:.2f} maxDD {st['dd_modeled']*100:.1f}%")
|
print(f" MODELED ($2000 nominale): {rm*100:+6.2f}% eq ${st['cap_modeled']:.2f} maxDD {st['dd_modeled']*100:.1f}%")
|
||||||
print(f" REAL-$600 (min-order $5) : {rr*100:+6.2f}% eq ${st['cap_real']:.2f} maxDD {st['dd_real']*100:.1f}%")
|
print(f" REAL-$600 (min-order $5) : {rr*100:+6.2f}% eq ${st['cap_real']:.2f} maxDD {st['dd_real']*100:.1f}%")
|
||||||
print(f" -> fill-haircut MODELED-REAL: {(rm-rr)*100:+.2f} pp (lo scettico l'aveva segnalato)")
|
print(f" -> fill-haircut MODELED-REAL: {(rm-rr)*100:+.2f} pp (lo scettico l'aveva segnalato)")
|
||||||
print(f" log: {RETURNS_FILE}\n")
|
print(f" log: {RETURNS_FILE}")
|
||||||
|
print_gate(st, dfs)
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -88,15 +88,10 @@ EPS_CUSCINO = 0.01 # 1 centesimo: il criterio e' ">= cuscino", e
|
|||||||
# questo epsilon il rumore float boccia il caso di pareggio.
|
# questo epsilon il rumore float boccia il caso di pareggio.
|
||||||
|
|
||||||
|
|
||||||
def cuscino_richiesto_usd(equity_tot: float) -> tuple[float, str]:
|
# Il cuscino di regolamento vive in src/live/usde.py dal 10/09 (issue #3): cuscino_watch lo
|
||||||
"""Cuscino USDC di REGOLAMENTO = disaster-SL sulla massima esposizione lorda del libro.
|
# sorveglia ogni ora e questo script lo rispetta a ogni piano — due lettori, UNA formula (P1).
|
||||||
Derivato da config/live.json + src/live/book.py, mai ridichiarato qui."""
|
# L'alias resta per r0906_usde_tetto_sonda, che lo importa da qui.
|
||||||
cfg = json.loads((ROOT / "config" / "live.json").read_text())
|
cuscino_richiesto_usd = U.cuscino_richiesto_usd
|
||||||
sl = float(cfg["disaster_sl_pct"])
|
|
||||||
from src.live import book as B
|
|
||||||
frac, n_asset = float(B.WEIGHT), 2
|
|
||||||
lordo_max = equity_tot * frac * n_asset
|
|
||||||
return lordo_max * sl, f"{n_asset} asset x frac {frac} x SL {sl:.0%} su ${equity_tot:,.2f}"
|
|
||||||
|
|
||||||
|
|
||||||
def book_top(timeout: float = 5.0) -> tuple[float | None, float | None]:
|
def book_top(timeout: float = 5.0) -> tuple[float | None, float | None]:
|
||||||
@@ -165,7 +160,9 @@ def piano(eq_usdc: float, eq_usde: float, px: float, quota_target: float,
|
|||||||
motivi.append(f"gia' a bersaglio ({quota_ora:.1%} vs {quota_target:.0%}, toll {TOLL:.1%})")
|
motivi.append(f"gia' a bersaglio ({quota_ora:.1%} vs {quota_target:.0%}, toll {TOLL:.1%})")
|
||||||
if abs(delta_usd) < MIN_ORDER_USD:
|
if abs(delta_usd) < MIN_ORDER_USD:
|
||||||
motivi.append(f"delta ${abs(delta_usd):,.2f} sotto il minimo ${MIN_ORDER_USD:,.2f}")
|
motivi.append(f"delta ${abs(delta_usd):,.2f} sotto il minimo ${MIN_ORDER_USD:,.2f}")
|
||||||
if cap_usd is not None and val_target > cap_usd + EPS_CUSCINO:
|
# il tetto del venue limita l'ACQUISTO di USDE: una VENDITA va sempre verso il tetto, e bloccarla
|
||||||
|
# renderebbe morta la riconversione automatica di cuscino_watch (revisione fable 10/09)
|
||||||
|
if side == "buy" and cap_usd is not None and val_target > cap_usd + EPS_CUSCINO:
|
||||||
motivi.append(
|
motivi.append(
|
||||||
f"TETTO DEL VENUE: bersaglio ${val_target:,.2f} sopra ${cap_usd:,.2f} "
|
f"TETTO DEL VENUE: bersaglio ${val_target:,.2f} sopra ${cap_usd:,.2f} "
|
||||||
f"({cap_frac:.1%} dell'equity, misurato {cfg.get('venue_cap_misurato')}). "
|
f"({cap_frac:.1%} dell'equity, misurato {cfg.get('venue_cap_misurato')}). "
|
||||||
|
|||||||
+54
-11
@@ -66,6 +66,25 @@ VERDETTO_DOPO = "2026-08-29T12:30:00Z" # ultima finestra reward del 29/08 conc
|
|||||||
DECISIONE_QUOTA_DAL = "2026-08-31" # lunedi': si decide la quota
|
DECISIONE_QUOTA_DAL = "2026-08-31" # lunedi': si decide la quota
|
||||||
EPS_GIORNI = 0.5 # sotto mezza giornata l'APR non si annualizza
|
EPS_GIORNI = 0.5 # sotto mezza giornata l'APR non si annualizza
|
||||||
|
|
||||||
|
# --- lettura dei trade spot: i DUE limiti del canale, misurati il 2026-09-10 -----------------
|
||||||
|
# (1) `count` massimo di Deribit per get_user_trades_by_instrument: 1000. Il 07/09 il lettore
|
||||||
|
# chiedeva limit=50 e la sonda del 06/09 aveva fatto 54 ordini: i 4 non letti (400 USDE)
|
||||||
|
# sono finiti nel «reward» (issue #2). Se il venue restituisce ESATTAMENTE `limit` righe la
|
||||||
|
# lista puo' essere troncata e la somma NON e' leggibile (P12: non si attribuisce).
|
||||||
|
# (2) il gateway chiama l'endpoint senza `historical`/timestamp: Deribit restituisce solo le
|
||||||
|
# ultime 24h — DOCUMENTATO ("Accessing historical trades and orders using API": recent
|
||||||
|
# trades 24h, orders 30 min, count max 1000) e misurato (il fill 08/09 06:47Z invisibile
|
||||||
|
# alle 12:55Z del 10/09, quello del 09/09 21:47Z visibile). Se l'ultima lettura e' piu'
|
||||||
|
# vecchia della finestra, i trade fra la lettura e il bordo sono INVISIBILI e la somma non
|
||||||
|
# e' leggibile.
|
||||||
|
# ⚠️ BUCO DICHIARATO (D5): il cron gira ogni 24h + pochi secondi; con la tolleranza di 5 min
|
||||||
|
# i trade nei primi ≤5 min dopo la lettura precedente sarebbero invisibili e sommati come
|
||||||
|
# «leggibile». Innocuo: nessun attrezzo converte USDE alle 12:35 UTC, e senza tolleranza il
|
||||||
|
# giro quotidiano sarebbe SEMPRE «non leggibile» e il tasso non avrebbe mai finestre nuove.
|
||||||
|
TRADE_LIMIT = 1000
|
||||||
|
FINESTRA_GATEWAY_MS = 24 * 3_600_000
|
||||||
|
TOLL_FINESTRA_MS = 5 * 60_000
|
||||||
|
|
||||||
|
|
||||||
def _parse(ts: str) -> datetime:
|
def _parse(ts: str) -> datetime:
|
||||||
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
||||||
@@ -177,14 +196,36 @@ def _safe_client():
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _trades_spot_da(client, ts_ms: int) -> tuple[float | None, int | None]:
|
def trade_copertura(n_righe: int, prev_ts_ms: int, now_ms: int,
|
||||||
"""Somma con segno (buy +, sell -) dell'USDE scambiato spot dopo ts_ms. None = non leggibile."""
|
limit: int = TRADE_LIMIT, finestra_ms: int = FINESTRA_GATEWAY_MS,
|
||||||
|
toll_ms: int = TOLL_FINESTRA_MS) -> str | None:
|
||||||
|
"""PURA. None se la lista di trade COPRE l'intervallo (prev, now]; altrimenti il motivo per cui
|
||||||
|
NON lo copre — e allora la somma non si usa (P12: un reward calcolato su una lista incompleta
|
||||||
|
e' un'invenzione, il 07/09 valeva 400 USDE)."""
|
||||||
|
if n_righe >= limit:
|
||||||
|
return f"lista troncata: {n_righe} righe = limite {limit} del venue"
|
||||||
|
if now_ms - prev_ts_ms > finestra_ms + toll_ms:
|
||||||
|
ore = (now_ms - prev_ts_ms) / 3_600_000
|
||||||
|
return (f"ultima lettura {ore:.1f}h fa, oltre la finestra di {finestra_ms / 3_600_000:.0f}h "
|
||||||
|
f"del gateway: i trade piu' vecchi sono invisibili")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _trades_spot_da(client, ts_ms: int, now_ms: int | None = None
|
||||||
|
) -> tuple[float | None, int | None, str | None]:
|
||||||
|
"""Somma con segno (buy +, sell -) dell'USDE scambiato spot dopo ts_ms.
|
||||||
|
-> (somma, n, motivo). somma None = non leggibile, e `motivo` dice perche' (P4)."""
|
||||||
if client is None:
|
if client is None:
|
||||||
return None, None
|
return None, None, "gateway non raggiungibile"
|
||||||
try:
|
try:
|
||||||
rows = client.trade_history(U.SPOT, limit=50)
|
rows = client.trade_history(U.SPOT, limit=TRADE_LIMIT)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
return None, None
|
return None, None, f"trade_history: {type(e).__name__}"
|
||||||
|
if now_ms is None:
|
||||||
|
now_ms = int(datetime.now(timezone.utc).timestamp() * 1000)
|
||||||
|
motivo = trade_copertura(len(rows), ts_ms, now_ms)
|
||||||
|
if motivo is not None:
|
||||||
|
return None, None, motivo
|
||||||
tot, n = 0.0, 0
|
tot, n = 0.0, 0
|
||||||
for t in rows:
|
for t in rows:
|
||||||
if int(t.get("timestamp") or 0) <= ts_ms:
|
if int(t.get("timestamp") or 0) <= ts_ms:
|
||||||
@@ -192,7 +233,7 @@ def _trades_spot_da(client, ts_ms: int) -> tuple[float | None, int | None]:
|
|||||||
amt = float(t.get("amount") or 0)
|
amt = float(t.get("amount") or 0)
|
||||||
tot += amt if (t.get("direction") or "").lower() == "buy" else -amt
|
tot += amt if (t.get("direction") or "").lower() == "buy" else -amt
|
||||||
n += 1
|
n += 1
|
||||||
return round(tot, 8), n
|
return round(tot, 8), n, None
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
@@ -221,10 +262,11 @@ def main() -> int:
|
|||||||
if eq_usde is not None:
|
if eq_usde is not None:
|
||||||
usd, _nota = U.valuta(eq_usde, px)
|
usd, _nota = U.valuta(eq_usde, px)
|
||||||
|
|
||||||
trades_usde, n_trades = (None, None)
|
trades_usde, n_trades, trades_motivo = (None, None, None)
|
||||||
an = dict(delta=None, reward_stimato=None, reward_rilevato=False, con_trade=None)
|
an = dict(delta=None, reward_stimato=None, reward_rilevato=False, con_trade=None)
|
||||||
if stato == "OK" and prev is not None and prev.get("eq_usde") is not None:
|
if stato == "OK" and prev is not None and prev.get("eq_usde") is not None:
|
||||||
trades_usde, n_trades = _trades_spot_da(client, int(prev["ts"]))
|
trades_usde, n_trades, trades_motivo = _trades_spot_da(
|
||||||
|
client, int(prev["ts"]), int(now.timestamp() * 1000))
|
||||||
an = analizza(prev, eq_usde, trades_usde)
|
an = analizza(prev, eq_usde, trades_usde)
|
||||||
|
|
||||||
quota = None
|
quota = None
|
||||||
@@ -237,7 +279,7 @@ def main() -> int:
|
|||||||
stato=stato, motivo_blind=motivo_blind,
|
stato=stato, motivo_blind=motivo_blind,
|
||||||
eq_usde=eq_usde, px=px, px_fonte=px_fonte, usd=usd,
|
eq_usde=eq_usde, px=px, px_fonte=px_fonte, usd=usd,
|
||||||
eq_usdc=eq_usdc, quota=quota,
|
eq_usdc=eq_usdc, quota=quota,
|
||||||
trades_usde=trades_usde, n_trades=n_trades, **an,
|
trades_usde=trades_usde, n_trades=n_trades, trades_motivo=trades_motivo, **an,
|
||||||
)
|
)
|
||||||
v, v_motivo = verdetto(records + [rec], now)
|
v, v_motivo = verdetto(records + [rec], now)
|
||||||
rec["verdetto"], rec["verdetto_motivo"] = v, v_motivo
|
rec["verdetto"], rec["verdetto_motivo"] = v, v_motivo
|
||||||
@@ -312,7 +354,8 @@ def main() -> int:
|
|||||||
f" fa margine). Con cross X:SM sarebbero ~${cross:,.0f} (haircut"
|
f" fa margine). Con cross X:SM sarebbero ~${cross:,.0f} (haircut"
|
||||||
f" {c['haircut']:.0%})")
|
f" {c['haircut']:.0%})")
|
||||||
if an["delta"] is not None:
|
if an["delta"] is not None:
|
||||||
attr = ("non attribuibile (trade non leggibili)" if an["reward_stimato"] is None
|
attr = (f"non attribuibile (trade non leggibili: {trades_motivo})"
|
||||||
|
if an["reward_stimato"] is None
|
||||||
else f"reward stimato {an['reward_stimato']:+.6f} USDE"
|
else f"reward stimato {an['reward_stimato']:+.6f} USDE"
|
||||||
+ (" [con trade nel mezzo]" if an["con_trade"] else ""))
|
+ (" [con trade nel mezzo]" if an["con_trade"] else ""))
|
||||||
print(f" delta : {an['delta']:+.6f} USDE dall'ultima lettura -> {attr}")
|
print(f" delta : {an['delta']:+.6f} USDE dall'ultima lettura -> {attr}")
|
||||||
|
|||||||
@@ -92,6 +92,12 @@ MONITORS: tuple[MonitorSpec, ...] = (
|
|||||||
MonitorSpec("usde_watch", "live", "usde_watch.jsonl", 24.0, max_age_h=30.0,
|
MonitorSpec("usde_watch", "live", "usde_watch.jsonl", 24.0, max_age_h=30.0,
|
||||||
gate="allarmi depeg/quota USDE + verdetto eligibilita' 29/08",
|
gate="allarmi depeg/quota USDE + verdetto eligibilita' 29/08",
|
||||||
open_labeled=False),
|
open_labeled=False),
|
||||||
|
# Cuscino USDC di regolamento (cron :53, ORARIO, debito §5.17 / issue #3): se si ferma nessuno
|
||||||
|
# vede il saldo USDC scendere sotto il cuscino, e la riconversione automatica non parte.
|
||||||
|
# max_age 3h = due giri persi; timbri di esecuzione -> open_labeled=False.
|
||||||
|
MonitorSpec("cuscino_watch", "live", "cuscino_watch.jsonl", 1.0, max_age_h=3.0,
|
||||||
|
gate="cuscino USDC di regolamento: allerta + riconversione automatica",
|
||||||
|
open_labeled=False),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -93,3 +93,39 @@ def valuta(eq_usde: float, px: float | None) -> tuple[float, str]:
|
|||||||
return eq_usde, f"USDE {eq_usde:,.0f} valutato 1.0000 (indice non leggibile)"
|
return eq_usde, f"USDE {eq_usde:,.0f} valutato 1.0000 (indice non leggibile)"
|
||||||
p = min(px, 1.0)
|
p = min(px, 1.0)
|
||||||
return eq_usde * p, f"USDE {eq_usde:,.0f} @ {p:.4f}"
|
return eq_usde * p, f"USDE {eq_usde:,.0f} @ {p:.4f}"
|
||||||
|
|
||||||
|
|
||||||
|
# --- CUSCINO USDC DI REGOLAMENTO ------------------------------------------------------------
|
||||||
|
# P&L e funding dei perp USDC-lineari si regolano in USDC, non nel collaterale: a quota USDE alta
|
||||||
|
# il saldo USDC va negativo alla prima perdita del libro e Deribit lo finanzia allo 0,05%/GIORNO
|
||||||
|
# (KB "Cross collateral specifications": 18,25%/anno, 4,3x la resa USDE). Il cuscino richiesto e'
|
||||||
|
# il disaster-SL sulla MASSIMA esposizione lorda che il libro puo' prendere:
|
||||||
|
# n_asset x frac x disaster_sl_pct x equity_totale
|
||||||
|
# tutto letto da config/live.json e src/live/book.py, MAI ridichiarato (P1). Prima del 10/09 la
|
||||||
|
# formula viveva in scripts/live/usde_convert.py; il sorvegliante cuscino_watch la deriva da qui,
|
||||||
|
# e usde_convert pure — due lettori, una formula.
|
||||||
|
|
||||||
|
N_ASSET = 2
|
||||||
|
|
||||||
|
|
||||||
|
def frazione_cuscino() -> tuple[float, str]:
|
||||||
|
"""PURA sulla config. -> (frazione dell'equity totale che deve restare in USDC, come)."""
|
||||||
|
cfg = json.loads(CFG_PATH.read_text())
|
||||||
|
sl = float(cfg["disaster_sl_pct"])
|
||||||
|
from src.live import book as B # lazy: book importa shadow, che importa questo modulo
|
||||||
|
frac = float(B.WEIGHT)
|
||||||
|
return N_ASSET * frac * sl, f"{N_ASSET} asset x frac {frac} x SL {sl:.0%}"
|
||||||
|
|
||||||
|
|
||||||
|
def cuscino_richiesto_usd(equity_tot: float) -> tuple[float, str]:
|
||||||
|
"""Cuscino USDC di REGOLAMENTO in dollari, per un'equity totale data. -> (usd, come)."""
|
||||||
|
f, come = frazione_cuscino()
|
||||||
|
return equity_tot * f, f"{come} su ${equity_tot:,.2f}"
|
||||||
|
|
||||||
|
|
||||||
|
def quota_ripristino(margine_frac: float) -> float:
|
||||||
|
"""PURA sulla config. Quota USDE che lascia il cuscino PIU' un margine (frazione del cuscino):
|
||||||
|
e' il bersaglio della riconversione automatica quando il cuscino e' scoperto. Il margine serve
|
||||||
|
a non tornare sul bordo alla prima ora in perdita (P6: la persistenza e' latenza)."""
|
||||||
|
f, _ = frazione_cuscino()
|
||||||
|
return max(0.0, 1.0 - f * (1.0 + margine_frac))
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
"""cuscino_watch: il cuscino USDC di regolamento e' sorvegliato, e sotto zero si riconverte.
|
||||||
|
|
||||||
|
Blindano: (a) il cuscino e' DERIVATO da config + book (P1), la stessa formula di usde_convert;
|
||||||
|
(b) i tre stati e la soglia di preavviso; (c) la transizione non si consuma se l'invio fallisce
|
||||||
|
(debito §5.2); (d) le guardie della riconversione automatica — interruttore del libro, depeg,
|
||||||
|
cadenza — e la quota di ripristino che lascia margine."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from src.live import usde as U # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _carica(nome):
|
||||||
|
sp = importlib.util.spec_from_file_location(nome, ROOT / "scripts" / "live" / f"{nome}.py")
|
||||||
|
m = importlib.util.module_from_spec(sp)
|
||||||
|
sp.loader.exec_module(m)
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
W = _carica("cuscino_watch")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------ il cuscino e' derivato, non ridichiarato ------------------------------
|
||||||
|
|
||||||
|
def test_cuscino_derivato_da_config_e_book():
|
||||||
|
cfg = json.loads((ROOT / "config" / "live.json").read_text())
|
||||||
|
from src.live import book as B
|
||||||
|
f, come = U.frazione_cuscino()
|
||||||
|
assert abs(f - 2 * float(B.WEIGHT) * float(cfg["disaster_sl_pct"])) < 1e-12
|
||||||
|
usd, _ = U.cuscino_richiesto_usd(1000.0)
|
||||||
|
assert abs(usd - 1000.0 * f) < 1e-9
|
||||||
|
assert "SL" in come
|
||||||
|
|
||||||
|
|
||||||
|
def test_usde_convert_usa_la_stessa_formula():
|
||||||
|
C = _carica("usde_convert")
|
||||||
|
assert C.cuscino_richiesto_usd(4469.46) == U.cuscino_richiesto_usd(4469.46)
|
||||||
|
|
||||||
|
|
||||||
|
def test_quota_ripristino_lascia_margine_sul_cuscino():
|
||||||
|
f, _ = U.frazione_cuscino()
|
||||||
|
q0, q10 = U.quota_ripristino(0.0), U.quota_ripristino(0.10)
|
||||||
|
assert abs(q0 - (1 - f)) < 1e-12 # a margine zero e' il complemento del cuscino
|
||||||
|
assert q10 < q0 # col margine si vende UN PO' di piu'
|
||||||
|
# dopo la riconversione a q10 lo slack e' esattamente il 10% del cuscino
|
||||||
|
E = 4000.0
|
||||||
|
usdc_dopo = E * (1 - q10)
|
||||||
|
assert abs((usdc_dopo - E * f) - 0.10 * E * f) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------ i tre stati ------------------------------
|
||||||
|
|
||||||
|
def test_stati_ok_preavviso_scoperto():
|
||||||
|
assert W.giudica(1500.0, 4000.0, 1200.0)["stato"] == "OK" # slack 300 > 120
|
||||||
|
g = W.giudica(1300.0, 4000.0, 1200.0)
|
||||||
|
assert g["stato"] == "PREAVVISO" and abs(g["slack"] - 100.0) < 1e-9 and g["preavviso_usd"] == 120.0
|
||||||
|
assert W.giudica(1199.0, 4000.0, 1200.0)["stato"] == "SCOPERTO"
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_vedo_e_blind_non_ok():
|
||||||
|
assert W.giudica(None, 4000.0, 1200.0)["stato"] == "BLIND"
|
||||||
|
assert W.giudica(1500.0, None, None)["stato"] == "BLIND"
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------ transizione e trasporto ------------------------------
|
||||||
|
|
||||||
|
def test_ok_non_allerta_mai():
|
||||||
|
assert not W.transizione(None, "OK") and not W.transizione({"stato": "SCOPERTO"}, "OK")
|
||||||
|
|
||||||
|
|
||||||
|
def test_transizione_alla_prima_e_al_cambio_di_stato():
|
||||||
|
assert W.transizione(None, "PREAVVISO")
|
||||||
|
assert W.transizione({"stato": "OK", "allertato": None}, "PREAVVISO")
|
||||||
|
assert W.transizione({"stato": "PREAVVISO", "allertato": True}, "SCOPERTO")
|
||||||
|
assert not W.transizione({"stato": "PREAVVISO", "allertato": True}, "PREAVVISO")
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_invio_fallito_non_consuma_la_transizione():
|
||||||
|
"""Debito §5.2: il marcatore 'gia' detto' e' l'esito dell'invio."""
|
||||||
|
assert W.transizione({"stato": "SCOPERTO", "allertato": False}, "SCOPERTO")
|
||||||
|
assert W.transizione({"stato": "SCOPERTO", "allertato": None}, "SCOPERTO")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------ guardie della riconversione ------------------------------
|
||||||
|
|
||||||
|
def test_disarmare_il_libro_disarma_la_riconversione():
|
||||||
|
ok, m = W.puo_riconvertire([], 0, False, 1.0, 0.99)
|
||||||
|
assert not ok and "execution_enabled" in m
|
||||||
|
|
||||||
|
|
||||||
|
def test_sotto_depeg_non_si_vende_al_buio():
|
||||||
|
ok, m = W.puo_riconvertire([], 0, True, 0.985, 0.99)
|
||||||
|
assert not ok and "depeg" in m
|
||||||
|
ok, m = W.puo_riconvertire([], 0, True, None, 0.99)
|
||||||
|
assert not ok and "leggibile" in m
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_tentativo_ogni_sei_ore():
|
||||||
|
now = 1_789_000_000_000
|
||||||
|
prev = [{"ts": now - 2 * 3_600_000, "riconversione": {"tentata": True}}]
|
||||||
|
ok, m = W.puo_riconvertire(prev, now, True, 1.0, 0.99)
|
||||||
|
assert not ok and "non si insiste" in m
|
||||||
|
prev = [{"ts": now - 7 * 3_600_000, "riconversione": {"tentata": True}}]
|
||||||
|
assert W.puo_riconvertire(prev, now, True, 1.0, 0.99)[0]
|
||||||
|
# un record SENZA riconversione non conta come tentativo
|
||||||
|
assert W.puo_riconvertire([{"ts": now - 1000, "riconversione": None}], now, True, 1.0, 0.99)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_riconverti_lancia_lo_script_vero_con_esegui(monkeypatch):
|
||||||
|
chiamate = []
|
||||||
|
|
||||||
|
class R:
|
||||||
|
returncode, stdout, stderr = 0, " → piano valido\n ESITO: 10 USDE\n", ""
|
||||||
|
|
||||||
|
def finto_run(cmd, **kw):
|
||||||
|
chiamate.append(cmd)
|
||||||
|
return R()
|
||||||
|
|
||||||
|
monkeypatch.setattr(W.subprocess, "run", finto_run)
|
||||||
|
e = W.riconverti(0.67)
|
||||||
|
assert e["ok"] and chiamate and chiamate[0][1].endswith("usde_convert.py")
|
||||||
|
assert "--esegui" in chiamate[0] and "0.6700" in chiamate[0]
|
||||||
|
assert e["coda"][-1].startswith(" ESITO")
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_monitor_e_registrato_in_monitor_health():
|
||||||
|
from src.live.monitor_health import MONITORS
|
||||||
|
spec = [m for m in MONITORS if m.name == "cuscino_watch"]
|
||||||
|
assert spec and spec[0].cadence_h == 1.0 and spec[0].open_labeled is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_non_tentativo_non_consuma_il_budget():
|
||||||
|
"""Prezzo illeggibile, --secco, interruttore spento: `tentata=False` non blocca il giro dopo."""
|
||||||
|
now = 1_789_000_000_000
|
||||||
|
prev = [{"ts": now - 3_600_000, "riconversione": {"tentata": False, "motivo": "--secco"}}]
|
||||||
|
assert W.puo_riconvertire(prev, now, True, 1.0, 0.99)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_dopo_il_ripristino_lo_stato_e_OK_non_preavviso():
|
||||||
|
"""Il margine di ripristino deve superare la soglia di preavviso, o ogni 🚨 e' seguito da un ⚠️
|
||||||
|
per costruzione (P14). Include il floor di 1 USDE e la tolleranza di quota di usde_convert."""
|
||||||
|
assert W.MARGINE_RIPRISTINO > W.PREAVVISO_FRAC
|
||||||
|
f, _ = U.frazione_cuscino()
|
||||||
|
E = 4450.0
|
||||||
|
q = U.quota_ripristino(W.MARGINE_RIPRISTINO) + 0.005 # usde_convert si ferma entro TOLL
|
||||||
|
usdc_dopo = E * (1 - q) - 1.0 # e vende fino a 1 USDE in meno
|
||||||
|
assert W.giudica(usdc_dopo, E, E * f)["stato"] == "OK"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cron_installato_e_dichiarato_fuori_dal_tondo_e_dopo_il_book():
|
||||||
|
"""Legge la crontab VIVA (come test_book_cadenza), non un commento nello .sh (P1)."""
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
s = (ROOT / "scripts" / "cron_cuscino.sh").read_text()
|
||||||
|
assert "cuscino_watch.py --quiet" in s
|
||||||
|
if not shutil.which("crontab"):
|
||||||
|
return # SALTATO: distinto da 'assente'
|
||||||
|
out = subprocess.run(["crontab", "-l"], capture_output=True, text=True, timeout=10)
|
||||||
|
if out.returncode != 0 or "PythagorasGoal" not in out.stdout:
|
||||||
|
return # altra macchina / altro utente
|
||||||
|
righe = [ln for ln in out.stdout.splitlines() if "cron_cuscino.sh" in ln and not ln.startswith("#")]
|
||||||
|
assert len(righe) == 1, "cron_cuscino.sh deve essere installato UNA volta"
|
||||||
|
minuto, ora = righe[0].split()[:2]
|
||||||
|
assert minuto == "53" and ora == "*" # orario, dopo il book (:47)
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
"""GATE PREVDAY-01 cablato nel monitor (issue #4): kill, veto d'integrita', data.
|
||||||
|
|
||||||
|
Prima del 10/09 il gate esisteva solo nel testo (RESULTS-0822 §54) e la revisione settimanale lo
|
||||||
|
ha letto come «senza data». Qui si blinda che: (a) le costanti sono quelle scritte il 23/08;
|
||||||
|
(b) il kill non e' leggibile sotto 180 giorni attivi e scatta solo sotto -0,50; (c) il veto
|
||||||
|
blocca sotto l'80% di barre ricostruibili e su una crescita MATERIALE delle divergenze — non su
|
||||||
|
una barra sola (P14); (d) la ricostruzione riproduce una serie generata con la stessa aritmetica
|
||||||
|
di `advance`; (e) la lente del kill e' GIORNALIERA."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
sp = importlib.util.spec_from_file_location("paper_prevday", ROOT / "scripts" / "live" / "paper_prevday.py")
|
||||||
|
PP = importlib.util.module_from_spec(sp)
|
||||||
|
sp.loader.exec_module(PP)
|
||||||
|
|
||||||
|
|
||||||
|
def test_le_costanti_sono_quelle_del_gate_scritto_il_23_08():
|
||||||
|
assert PP.GATE_DATE == "2027-06-21"
|
||||||
|
assert PP.KILL_SHARPE == -0.50 and PP.KILL_MIN_ACTIVE_DAYS == 180
|
||||||
|
assert PP.MIN_RECON_FRAC == 0.80
|
||||||
|
# e CLAUDE.md §4 cita la stessa data (era assente fino al 10/09)
|
||||||
|
assert "2027-06-21" in (ROOT / "CLAUDE.md").read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def _serie(n_giorni: int, mu_h: float, sd_h: float, seed: int = 0, pos: float = 0.3) -> pd.DataFrame:
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
n = n_giorni * 24
|
||||||
|
ts = 1_782_000_000_000 + np.arange(n) * 3_600_000
|
||||||
|
x = rng.normal(mu_h, sd_h, n)
|
||||||
|
return pd.DataFrame(dict(ts=ts, dt=pd.to_datetime(ts, unit="ms", utc=True), net_modeled=x,
|
||||||
|
net_real=x, pos_btc=pos, pos_eth=pos))
|
||||||
|
|
||||||
|
|
||||||
|
def test_kill_non_leggibile_sotto_180_giorni_attivi():
|
||||||
|
r = _serie(81, -0.001, 0.004)
|
||||||
|
sh, n = PP.sharpe_giornaliero(r)
|
||||||
|
assert n == 81 and sh < PP.KILL_SHARPE # Sharpe pessimo...
|
||||||
|
stato, motivo = PP.kill_check(sh, PP.giorni_attivi(r), pd.Timestamp("2026-09-10", tz="UTC"))
|
||||||
|
assert stato == "NON_MATURO" and "99" in motivo # ...ma il kill non e' leggibile
|
||||||
|
|
||||||
|
|
||||||
|
def test_kill_scatta_sopra_180_giorni_solo_sotto_la_soglia():
|
||||||
|
r_male = _serie(200, -0.001, 0.004)
|
||||||
|
sh, _ = PP.sharpe_giornaliero(r_male)
|
||||||
|
assert PP.kill_check(sh, PP.giorni_attivi(r_male), pd.Timestamp("2027-01-01", tz="UTC"))[0] == "KILL"
|
||||||
|
oggi = pd.Timestamp("2027-01-01", tz="UTC")
|
||||||
|
assert PP.kill_check(-0.49, 200, oggi)[0] == "SUPERATO" # la soglia e' STRETTA: -0,49 passa
|
||||||
|
assert PP.kill_check(-0.51, 200, oggi)[0] == "KILL"
|
||||||
|
assert PP.kill_check(float("nan"), 200, oggi)[0] == "NON_MISURABILE"
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_giorni_flat_non_contano_come_attivi():
|
||||||
|
r = _serie(200, 0.0, 0.004)
|
||||||
|
r.loc[r.index[: 50 * 24], ["pos_btc", "pos_eth"]] = 0.0
|
||||||
|
assert PP.giorni_attivi(r) == 150
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_lente_del_kill_e_giornaliera_non_oraria():
|
||||||
|
"""§54: sulla lente oraria lo stesso forward vale +2,04 contro +1,56. Con autocorrelazione
|
||||||
|
positiva intragiornaliera la lente oraria SOVRASTIMA: il kill usa la giornaliera."""
|
||||||
|
rng = np.random.default_rng(3)
|
||||||
|
n = 120 * 24
|
||||||
|
base = np.repeat(rng.normal(0.0004, 0.004, 120), 24) # shock comune al giorno
|
||||||
|
x = base + rng.normal(0, 0.0005, n)
|
||||||
|
ts = 1_782_000_000_000 + np.arange(n) * 3_600_000
|
||||||
|
r = pd.DataFrame(dict(ts=ts, dt=pd.to_datetime(ts, unit="ms", utc=True), net_modeled=x,
|
||||||
|
net_real=x, pos_btc=0.3, pos_eth=0.3))
|
||||||
|
sh_g, _ = PP.sharpe_giornaliero(r)
|
||||||
|
assert PP._sharpe_orario(r) > sh_g * 2 # la lente oraria gonfia; il kill non la usa
|
||||||
|
|
||||||
|
|
||||||
|
def test_veto_sotto_80_pct_ricostruibili():
|
||||||
|
ok, m = PP.veto_check(0.79, 0.5, 0.5, 10)
|
||||||
|
assert not ok and "79.0%" in m
|
||||||
|
assert PP.veto_check(0.80, 0.5, 0.5, 10)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_veto_su_crescita_materiale_non_su_una_barra(monkeypatch):
|
||||||
|
"""Il 10/09 la serie reale aveva UNA barra divergente su 1943 (26/08 00:00, il giorno del fix
|
||||||
|
di advance()) e la prima stesura del veto la leggeva come 'crescita' (0,03/g contro 0,01/g)."""
|
||||||
|
assert PP.veto_check(0.999, 0.012, 0.032, 1)[0] # 1 barra: non e' crescita
|
||||||
|
ok, m = PP.veto_check(0.95, 0.10, 0.50, 12) # 12 recenti, tasso 5x: crescita
|
||||||
|
assert not ok and "crescita" in m
|
||||||
|
assert PP.veto_check(0.95, 0.30, 0.50, 12)[0] # tasso < 2x: no
|
||||||
|
|
||||||
|
|
||||||
|
def test_ricostruzione_riproduce_una_serie_generata_come_advance(monkeypatch):
|
||||||
|
"""Feed sintetico, target congelato deterministico (monkeypatch su prevday_target): la serie
|
||||||
|
registrata con l'aritmetica di advance si ricostruisce al 100%; una barra manomessa no."""
|
||||||
|
n = 24 * 10
|
||||||
|
ts = 1_782_000_000_000 + np.arange(n) * 3_600_000
|
||||||
|
rng = np.random.default_rng(7)
|
||||||
|
dfs = {}
|
||||||
|
for a, p0 in (("BTC", 60000.0), ("ETH", 2000.0)):
|
||||||
|
close = p0 * np.cumprod(1 + rng.normal(0, 0.003, n))
|
||||||
|
dfs[a] = pd.DataFrame(dict(timestamp=ts, datetime=pd.to_datetime(ts, unit="ms", utc=True), close=close))
|
||||||
|
tgt = {a: np.where((np.arange(n) // 24) % 2 == 0, 0.3, -0.3) for a in dfs}
|
||||||
|
monkeypatch.setattr(PP, "prevday_target",
|
||||||
|
lambda df: tgt["BTC"] if float(df["close"].iloc[0]) > 10000 else tgt["ETH"])
|
||||||
|
start = int(ts[23])
|
||||||
|
# registrazione con la stessa aritmetica di advance (ribilanciamento continuo)
|
||||||
|
pos = {a: float(tgt[a][23]) for a in dfs}
|
||||||
|
rows = []
|
||||||
|
for i in range(24, n):
|
||||||
|
net = 0.0
|
||||||
|
for a in dfs:
|
||||||
|
c = dfs[a]["close"].values
|
||||||
|
rr = c[i] / c[i - 1] - 1
|
||||||
|
net += PP.WEIGHT * (pos[a] * rr - PP.FEE_SIDE * abs(tgt[a][i] - pos[a]))
|
||||||
|
pos[a] = float(tgt[a][i])
|
||||||
|
rows.append(dict(ts=int(ts[i]), net_modeled=round(net, 6), pos_btc=pos["BTC"], pos_eth=pos["ETH"]))
|
||||||
|
r = pd.DataFrame(rows); r["dt"] = pd.to_datetime(r["ts"], unit="ms", utc=True)
|
||||||
|
rc = PP.ricostruzione(r, dfs, start)
|
||||||
|
assert rc["ricostruibili"] == rc["n"] == n - 24 and rc["assenti_nel_feed"] == 0
|
||||||
|
r2 = r.copy(); r2.loc[r2.index[-1], "net_modeled"] += 0.01
|
||||||
|
rc2 = PP.ricostruzione(r2, dfs, start)
|
||||||
|
assert rc2["ricostruibili"] == rc2["n"] - 1 and rc2["div_recenti"] == 1
|
||||||
@@ -205,3 +205,64 @@ def test_la_quota_si_decide_da_lunedi_e_solo_se_IDONEO():
|
|||||||
assert q("2026-09-30", "IDONEO") is True, "smette di chiedere: la decisione si dimentica"
|
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", "IN_ATTESA") is False, "chiede la quota su un conto non idoneo"
|
||||||
assert q("2026-08-31", "NON_IDONEO") is False
|
assert q("2026-08-31", "NON_IDONEO") is False
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------ lettura dei trade: i due limiti del canale (issue #2) ------------------------------
|
||||||
|
# Il 07/09 il lettore chiedeva limit=50, la sonda del 06/09 aveva fatto 54 ordini: 400 USDE di
|
||||||
|
# acquisti sono finiti nel «reward». Il lettore ora chiede il massimo del venue (1000) e, se la
|
||||||
|
# lista puo' essere incompleta — troncata al limite, o ultima lettura oltre la finestra di 24h
|
||||||
|
# che il gateway espone — dichiara «non leggibile» invece di sommare (P12).
|
||||||
|
|
||||||
|
class _ClientFinto:
|
||||||
|
def __init__(self, rows):
|
||||||
|
self.rows, self.limit_chiesto = rows, None
|
||||||
|
|
||||||
|
def trade_history(self, instrument, limit=20):
|
||||||
|
self.limit_chiesto = limit
|
||||||
|
return self.rows[:limit]
|
||||||
|
|
||||||
|
|
||||||
|
def _ordini(n, t0_ms, amt=100.0, passo_ms=15_000):
|
||||||
|
return [{"timestamp": t0_ms + i * passo_ms, "direction": "buy", "amount": amt} for i in range(n)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_controllo_positivo_54_ordini_si_sommano_tutti():
|
||||||
|
"""Il caso del 06-07/09: 54 ordini in 24h. Con il vecchio limit=50 ne mancavano 4."""
|
||||||
|
prev, now = 1_788_698_101_000, 1_788_784_501_000 # 06/09 12:35Z -> 07/09 12:35Z
|
||||||
|
c = _ClientFinto(_ordini(54, prev + 30_000_000))
|
||||||
|
tot, n, motivo = W._trades_spot_da(c, prev, now)
|
||||||
|
assert motivo is None and n == 54 and abs(tot - 5400.0) < 1e-9
|
||||||
|
assert c.limit_chiesto == W.TRADE_LIMIT >= 54
|
||||||
|
|
||||||
|
|
||||||
|
def test_lista_al_limite_del_venue_non_e_leggibile():
|
||||||
|
prev, now = 1_788_698_101_000, 1_788_784_501_000
|
||||||
|
c = _ClientFinto(_ordini(W.TRADE_LIMIT + 5, prev + 1000))
|
||||||
|
tot, n, motivo = W._trades_spot_da(c, prev, now)
|
||||||
|
assert tot is None and n is None and "troncata" in motivo
|
||||||
|
# e il reward NON si attribuisce (P12)
|
||||||
|
an = W.analizza({"eq_usde": 1000.0}, 1400.0, tot)
|
||||||
|
assert an["reward_stimato"] is None and not an["reward_rilevato"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_lettura_oltre_la_finestra_del_gateway_non_e_leggibile():
|
||||||
|
"""Giro saltato: 48h dall'ultima lettura. I trade fra 48h e 24h fa sono invisibili."""
|
||||||
|
prev = 1_788_698_101_000
|
||||||
|
now = prev + 48 * 3_600_000
|
||||||
|
c = _ClientFinto(_ordini(3, now - 3_600_000))
|
||||||
|
tot, n, motivo = W._trades_spot_da(c, prev, now)
|
||||||
|
assert tot is None and "finestra" in motivo
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_cadenza_del_cron_sta_dentro_la_tolleranza():
|
||||||
|
"""24h + qualche secondo (il cron delle 12:35 di due giorni consecutivi) e' coperta."""
|
||||||
|
prev = 1_788_698_101_000
|
||||||
|
assert W.trade_copertura(10, prev, prev + 24 * 3_600_000 + 2_000) is None
|
||||||
|
assert W.trade_copertura(10, prev, prev + 24 * 3_600_000 + W.TOLL_FINESTRA_MS + 1) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_trade_prima_della_lettura_precedente_non_contano():
|
||||||
|
prev, now = 1_788_698_101_000, 1_788_784_501_000
|
||||||
|
rows = _ordini(2, prev - 60_000) + [{"timestamp": prev + 1, "direction": "sell", "amount": 40.0}]
|
||||||
|
tot, n, motivo = W._trades_spot_da(_ClientFinto(rows), prev, now)
|
||||||
|
assert motivo is None and n == 1 and abs(tot + 40.0) < 1e-9
|
||||||
|
|||||||
Reference in New Issue
Block a user