Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 95424f0755 | |||
| de83909db9 | |||
| 1279ea605a | |||
| 835e0c8666 | |||
| 861cc7fc27 |
@@ -32,6 +32,12 @@ Documento di fondazione: `docs/diary/2026-06-19-deribit-history.md`.
|
||||
- Lo storico e' **ricostruito da Deribit mainnet e certificato**. Universo affidabile = **solo
|
||||
BTC/ETH** (tutti i TF); gli alt sono esclusi (illiquidi/divergenti/non certificabili).
|
||||
- Tutto il codice pre-reset e' archiviato in `Old/` (preservato in git, non cancellato).
|
||||
- 🚨 **Il README e' rimasto fermo al 2026-06-04 fino al 2026-09-02**, cioe' *quindici giorni prima*
|
||||
del reset: per 75 giorni la prima pagina del repo ha pubblicato la libreria pre-reset e i suoi
|
||||
numeri (PORT06 Sharpe 7,84/10,06, CAGR ~79%) come risultati correnti, mentre il progetto li aveva
|
||||
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
|
||||
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 **~$635**
|
||||
(NON i €2.000 nominali del paper trader).
|
||||
- Si riparte dalla ricerca di strategie NUOVE, su dati certi, con la metodologia della §8.
|
||||
@@ -115,7 +121,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% |
|
||||
| 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 |
|
||||
| performance del libro LIVE | «+243%» di equity (e' **96,3% un bonifico**) | **TWR +10,61%** spezzato sul versamento — +11,61% fino al 25/08, −0,89% dopo (02/09 14:02Z). Dal 02/09 lo stampa `trades_db.py --report` (debito 14 chiuso) |
|
||||
| performance del libro LIVE | «+243%» di equity (e' **96,3% un bonifico**) | **TWR +10,61%** spezzato sul versamento — +11,61% fino al 25/08, −0,89% dopo (lettura 02/09 13:47Z). Dal 02/09 lo stampa `trades_db.py --report` (debito 14 chiuso) |
|
||||
|
||||
📌 **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
|
||||
@@ -282,9 +288,23 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per
|
||||
6. ⚠️ **TLT ha 13,5 anni di storia in meno** (parte 2016-02 invece del 2002) → **GTAA01 gira su
|
||||
CINQUE gambe prima del 2016**, e l'assente e' quella obbligazionaria. Non e' un fetch da rifare (IB
|
||||
ritorna 0 barre). ⇒ in-sample e hold-out di GTAA01 **non sono la stessa strategia**.
|
||||
7. ⚠️ **Il docstring di `book_execute.py` prescrive "ogni ~230 minuti", il cron gira OGNI ORA.**
|
||||
**Il docstring e' sbagliato e la configurazione che gira e' quella giusta** — chi lo "correggesse"
|
||||
sposterebbe il libro sulla riga 4h, dove BTC **raddoppia** gli scatti del disaster-SL.
|
||||
7. ✅ **RIPARATO (2026-09-02).** Il docstring di `book_execute.py` prescriveva "ogni ~230 minuti"
|
||||
mentre il cron gira **ogni ora** (`47 * * * *`): era il docstring a essere sbagliato, e chi
|
||||
avesse "corretto" il **CRON** verso il docstring avrebbe spostato il libro dalla riga 1h (rotolante:
|
||||
BTC 0 scatti / ETH 1 in 7-8 anni) alla riga 4h (BTC 2, contro 1 del pavimento); la peggiore
|
||||
misurata e' 24h (ETH 5, con 2 in 30 giorni) — `r0823_sl_anchor.py`. Ora il docstring dichiara
|
||||
**CADENZA: ORARIA** con la ragione (giro idempotente: latenza SKH01 ≤1h; il rotolante e'
|
||||
**controllato** ogni ora e ri-ancorato solo oltre la tolleranza di `ensure_disaster_sl`, mark
|
||||
+5,263%/−4,762% o taglia >10%, quindi lo stop siede fra **−33,5% e −26,5%** dal mark corrente —
|
||||
«si ri-ancora ogni ora» era una frase sbagliata, corretta in revisione il 02/09; e «girare piu'
|
||||
fitto non costa ordini» pure: 22 dei 48 ordini live sono ri-taglie |Δ|≤$10, deadband in valuta
|
||||
assoluta, C2) e il divieto esplicito. **`tests/test_book_cadenza.py` (10 test)** tiene
|
||||
d'accordo le tre dichiarazioni — docstring, riga in `cron_book.sh`, **crontab installata**
|
||||
(letta con `crontab -l`, **tutte** le righe attive: due righe sono due esecuzioni; tre stati —
|
||||
illeggibile ⇒ SALTATO, leggibile senza questo progetto ⇒ SALTATO, leggibile col progetto ma senza
|
||||
`cron_book.sh` attivo ⇒ **ROSSO**) — verifica 60 min < `skyhook.LTF_MIN` 230 < 240 (riga 4h),
|
||||
minuto ≠ :00 **e fuori dallo slot di release** (`venue_probe.in_release_window`, col :07 come
|
||||
controllo positivo). Diario `2026-09-02b-debito-7-cadenza-docstring.md`.
|
||||
8. ⚠️ **Il Rulebook Deribit** (ADL, perdita socializzata, *emergency powers*, conti dormienti) **non
|
||||
lo sorveglia nessuno**; `MAINT_GRACE_HOURS`=2 **presume** gli annunci invece di leggerli.
|
||||
✅ *Ridotto in parte il 2026-08-25:* `src/live/venue_probe.py` interroga l'API **pubblica**
|
||||
@@ -339,10 +359,43 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per
|
||||
elencati, e il delta $ grezzo etichettato «movimenti INCLUSI» — il `%` grezzo **non compare piu'**.
|
||||
Test: 5 in `test_journal.py` (fra cui la riproduzione del +10,80% del diario 01/09, M23) + 2 in
|
||||
`test_trades_report.py` sul testo stampato, con `connect()` deviato in tmp. Diario
|
||||
`2026-09-02-debito-14-report-twr.md`. ⚠️ **Limite ereditato e dichiarato (D5):** un +10% di
|
||||
trading fra due letture consecutive tocca la soglia del rilevatore e, a mercato fermo, verrebbe
|
||||
classificato «movimento» — e' lo stesso limite del rilevatore live, non un difetto nuovo.
|
||||
`2026-09-02-debito-14-report-twr.md`. ⚠️ **Due limiti dichiarati dalla revisione del 02/09** (D5): (a) **l'intervallo che contiene un movimento certo esce INTERO dal rendimento** — il suo P&L di mercato finisce in `certi` e fuori da `trading`, con errore massimo pari a META' del movimento riconosciuto per costruzione del classificatore (il 25/08: ~$0,5 su $1.399); non si stima, si dichiara (P12); (b) **a base di equity zero `twr` E `trading` sono None**: il salto 0→X del primo versamento e' invisibile al classificatore e `trading` varrebbe l'intero conto. 🚨 **Seconda tornata (02/09, 15:30Z): il classificatore era CIECO ~23 ore al
|
||||
giorno.** Il feed 1h certificato si ferma alle 00:00 (rebuild 00:30): per le letture successive
|
||||
`asof` dava la stessa barra a t0 e t1, mercato "fermo" = 0, e **qualunque calo ≥10% del giorno
|
||||
sarebbe stato un «movimento»** — un crash o un depeg USDE stampato come prelievo, nel giornale
|
||||
(dal 25/08) e nel report. Riprodotto su copia del DB. Ora un salto fuori dalla copertura del feed
|
||||
(o con feed assente) e' **«mercato non misurabile»** ⇒ `ambiguo` con la ragione, e `twr`/`trading`
|
||||
sono **None** con motivo (prima, a feed assente, `e1/e0-1` tornava sotto l'etichetta TWR). Il
|
||||
«limite ereditato +10% a mercato fermo» scritto qui la mattina era un **artefatto della fixture
|
||||
piatta**, non un limite del classificatore (con mercato misurato il trading non puo' superare
|
||||
2·max_mkt): cancellato. E `pnl_giorno` ora **chiama** `rendimento_twr` (prima l'affermazione
|
||||
«la funzione che entrambi chiamano» era falsa: il giornale rifaceva `cum − certi`); la pagina
|
||||
stampa il TWR accanto al cumulato e la testata Telegram dell'analista qualifica il cumulato
|
||||
«di cui versati». `leva_tetto` del classificatore include la scala e il tetto di codice (P1).
|
||||
|
||||
15. ⚠️ **Il bound di mercato del classificatore dei movimenti guarda solo BTC/ETH, ma dal 26/08 il
|
||||
31% dell'equity e' USDE** (trovato in revisione il 02/09). Un depeg dell'USDE a BTC/ETH fermi:
|
||||
−1,5% (indice 0,95) resta sotto soglia e nel P&L; una rottura piena (−31%) supera 2× un mercato
|
||||
"fermo" e diventerebbe un **«movimento» scorporato come prelievo**, nel giornale e nel report —
|
||||
l'invenzione silenziosa che P12 vieta. Il bound corretto somma `quota_usde × |Δ indice usde_usdc|`,
|
||||
ma l'indice era registrato **una volta al giorno** (`usde_watch`, 12:35Z). ◐ **Primo passo fatto
|
||||
il 02/09:** `balance_watch` (orario, sola lettura) registra ora `usde_usdc` a ogni campione —
|
||||
None con la ragione se l'indice non e' leggibile, mai 1,0. **Manca il cablaggio nel bound**, che
|
||||
si fa quando la serie ha storia. Intanto: chi legge un «movimento» negativo senza bonifico in
|
||||
uscita **lo verifichi contro `balance_watch.jsonl`** prima di crederci.
|
||||
|
||||
16. ✅ **RIPARATO (2026-09-02).** Nessuno dei **18** script di `scripts/live/` usava argparse: un
|
||||
flag sconosciuto non era un errore, era **il ramo `else`**. Costo misurato lo stesso giorno
|
||||
durante una revisione: `trades_db.py --help` non stampava l'uso, cadeva in `sync()` e riscriveva
|
||||
`meta.ultimo_sync`; sugli altri due sarebbe stato peggio — `journal.py --help` avrebbe scritto
|
||||
pagina e riga di DB, `analista.py --help` avrebbe speso una chiamata al modello e mandato un
|
||||
Telegram. Ora `src/live/cli.valida` e' la **prima istruzione** di ogni `__main__`: `--help`
|
||||
stampa l'uso ed esce **0**, un flag ignoto esce **2** con l'elenco di quelli previsti (P4).
|
||||
Niente argparse: cambierebbe messaggi, codici d'uscita e `--help` di script che il cron gia'
|
||||
chiama — qui si lascia intatto cio' che gira. **`tests/test_cli_flag.py` (30)**: l'elenco degli
|
||||
script si **deriva dalla cartella** (uno nuovo senza guardia fa fallire il test), verifica che
|
||||
`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`.
|
||||
|
||||
---
|
||||
|
||||
@@ -557,10 +610,11 @@ uv run python scripts/analysis/certify_feed.py [--local] # certifica i f
|
||||
uv run python scripts/portfolio/run_portfolio.py # report del portafoglio (ricerca)
|
||||
uv run python scripts/live/paper_portfolio.py # avanza il paper (forward-only)
|
||||
uv run python scripts/live/trades_db.py --report # stato libro di bordo + P&L (TWR, non e1/e0)
|
||||
uv run python scripts/live/<script>.py --help # uso e flag (dal 02/09: un flag ignoto esce 2)
|
||||
uv run python scripts/live/trades_db.py --reconcile # incrocio delle 3 fonti sui fill
|
||||
uv run python scripts/live/journal.py # voce del giorno (numeri + lettura)
|
||||
uv run python scripts/live/analista.py --secco # analisi del giorno, senza salvare
|
||||
uv run pytest # test (832, tutti verdi al 02/09)
|
||||
uv run pytest # test (910, tutti verdi al 02/09)
|
||||
```
|
||||
|
||||
```python
|
||||
|
||||
@@ -1,423 +1,153 @@
|
||||
# PythagorasGoal
|
||||
|
||||
Sistema di riconoscimento pattern frattali e predizione per il trading di criptovalute (BTC, ETH), ispirato al framework teorico di Serleto & Malanga (*Pythagoras Trading Prediction*).
|
||||
Ricerca e esecuzione di strategie algoritmiche su BTC/ETH, con **un libro che gira con soldi veri**
|
||||
su Deribit mainnet dal 20 giugno 2026.
|
||||
|
||||
## Obiettivo
|
||||
> 🚨 **v2.0.0 — RESET del 2026-06-19. Tutto ciò che questo README diceva prima è archiviato in
|
||||
> `Old/` e non è fidato.** L'intera libreria di strategie "validata out-of-sample" (le famiglie
|
||||
> FADE/HONEST/PAIRS/TSMOM/SHAPE, i portafogli PORT01-06, gli Sharpe fra 6 e 10) era un **artefatto
|
||||
> di uno storico contaminato**: print fantasma di un feed *testnet* più storico Binance/USDT.
|
||||
> Ri-testate sul feed reale ricostruito da Deribit mainnet, **perdono ogni anno**. Documento di
|
||||
> fondazione: `docs/diary/2026-06-19-deribit-history.md`.
|
||||
>
|
||||
> Questo README descrive il progetto **dopo** il reset. È stato riscritto il 2026-09-02, dopo
|
||||
> essere rimasto fermo al 4 giugno — quindici giorni prima del reset — mentre pubblicava i numeri
|
||||
> che il progetto aveva già dichiarato falsi.
|
||||
|
||||
Partendo da un capitale iniziale di €1.000, raggiungere un profitto medio di €50 al giorno entro 6–8 mesi, tramite un portafoglio di strategie algoritmiche poco correlate fra loro — mean-reversion, trend/rotazione e spread market-neutral — validate out-of-sample e fee-aware.
|
||||
**L'autorità sui numeri e sulle decisioni è `CLAUDE.md`**, e il racconto completo sta in
|
||||
`docs/memory/`. Questo file è l'ingresso, non la fonte.
|
||||
|
||||
## Risultati
|
||||
---
|
||||
|
||||
> ⚠️ **Revisione 2026-05-28.** La famiglia squeeze-breakout (SQ/MT/ML/AD/CM/PD, con
|
||||
> accuracy storiche dichiarate 76-82%) è stata **scartata**: quei numeri erano un
|
||||
> **artefatto di look-ahead**. I backtest decidevano la direzione dalla candela di
|
||||
> breakout `close[i]` ma entravano a `close[i-1]` — impossibile dal vivo. Sotto
|
||||
> ingresso onesto (`close[i]`) e fee reali, l'edge sparisce e tutte perdono, anche
|
||||
> a fee zero. Dettagli e prove: `scripts/analysis/oos_validation.py`.
|
||||
## Cos'è vivo, adesso
|
||||
|
||||
Dopo una validazione **out-of-sample, fee-aware** di molte famiglie di strategie,
|
||||
emergono cinque famiglie con edge netto reale, tutte radicate nella stessa lezione
|
||||
(in cripto la **mean-reversion** funziona, la continuazione no) o nella diversificazione:
|
||||
| | |
|
||||
|---|---|
|
||||
| libro live | **TP01 + SKH01 a 75/25**, nettati in software su una sola posizione per asset (50/50 BTC/ETH) |
|
||||
| venue | Deribit mainnet, perpetual lineari USDC. Esecuzione **armata** dal 2026-06-20 |
|
||||
| capitale | ~$2.050 (USDC + USDE), non i €2.000 nominali di nessun paper trader |
|
||||
| cadenza | **oraria**, minuto `:47` (`scripts/cron_book.sh`) |
|
||||
| leva | lorda ~0,28x su un tetto di 1,00x. Il cap non ha mai morso: **il vincolo è il segnale** |
|
||||
| protezione on-book | un solo disaster-SL rotolante al −30% sulla posizione netta |
|
||||
|
||||
| Famiglia | Meccanismo | Strategie | Profilo (netto OOS) |
|
||||
|----------|-----------|-----------|---------------------|
|
||||
| **FADE** | mean-reversion intraday 1h (long/short, BTC/ETH) | MR01 Bollinger, MR02 Donchian, MR07 Return-reversal | Acc 52-55%, DD 18-34% |
|
||||
| **HONEST** | long-only multi-regime multi-crypto | DIP01 dip-buy, TR01 EMA-trend, ROT02 dual-momentum | CAGR 31-56%, DD 15-27% |
|
||||
| **PAIRS** | spread reversion *market-neutral* (2 gambe) | PR01 ETH/BTC, LTC/ETH, ADA/ETH, BTC/LTC, ETH/SOL | Sharpe 2.0-4.4, corr col mercato ~0.05 |
|
||||
| **TSMOM** | time-series momentum multi-orizzonte | TSM01 (3/6/12m + risk-off) | diversificatore, DD 15-22% |
|
||||
| **SHAPE** | ML walk-forward su feature di *forma* del prezzo | SH01 (LogisticRegression, orizzonte 12 barre) | diversificatore, corr +0.08 col resto |
|
||||
**Non** sono nel libro live: XS01, VRP01, GTAA01, XSR01. Vivono nel portafoglio di **ricerca**
|
||||
(paper, 5 sleeve), che è una serie diversa da quella che gira — quasi tutti i numeri di portafoglio
|
||||
del progetto sono su quella, non su questa.
|
||||
|
||||
Tutti i numeri sono **netti** dopo fee realistiche (Deribit 0.10% RT single-leg, 0.20%
|
||||
RT/coppia sui pairs), leva 3x, su finestra held-out. Le strategie sono robuste su griglia
|
||||
parametri, sweep fee 0.00-0.20% RT e — per i pairs — validate con **walk-forward** e
|
||||
config universale (niente cherry-picking).
|
||||
## I numeri, con la loro lente
|
||||
|
||||
### Portafoglio combinato (la vera leva anti-drawdown)
|
||||
Ogni numero va scritto con la lente e la banda che gli appartengono. La tabella completa
|
||||
("non citare" ↔ "citare") è in `CLAUDE.md` §2; qui i tre che contano.
|
||||
|
||||
Le famiglie sono **quasi scorrelate fra loro** (~0.05). Combinandole in un unico
|
||||
portafoglio equipesato il drawdown crolla sotto quello di ogni singola sleeve:
|
||||
| grandezza | valore onesto |
|
||||
|---|---|
|
||||
| rendimento del libro live | **TWR +10,6%** dall'armamento, spezzato sul versamento del 25/08: +11,61% prima, −0,9% dopo |
|
||||
| crescita di trading | **+$50** in 71 giorni, su 30 round-trip chiusi e $0,87 di fee totali |
|
||||
| Sharpe del portafoglio di ricerca | **1,95** [1,81 – 2,12] full, **1,54** [1,11 – 1,91] hold-out |
|
||||
|
||||
| Portafoglio | CAGR | Max DD | Sharpe |
|
||||
|-------------|------|--------|--------|
|
||||
| FADE (6 sleeve) | ~46% | 8% | 3.9 |
|
||||
| HONEST (3 sleeve) | ~46% | 13% | 2.2 |
|
||||
| **MASTER** (FADE + HONEST, 9) | ~47% | **5%** | 4.2 |
|
||||
| **MASTER + PAIRS + TSM01** (15) | ~67% | ~5% | ~6 |
|
||||
| **PORT06 live** (17 sleeve, cap pairs 33%, leva 2×, config EXIT-16) | ~79% | **2.6%** | 7.8 (FULL) / 10.1 (OOS) |
|
||||
⚠️ Il rendimento dell'equity **non** è la performance: il 96% della crescita del conto è un
|
||||
bonifico. Chi legge una percentuale su questo progetto deve sapere se è un TWR o un rapporto fra
|
||||
due saldi — è stato un difetto reale, riparato il 2026-09-02.
|
||||
|
||||
> 🔎 **Numeri sobri (anti-overfit).** L'OOS singolo cade nel regime favorevole 2024-25:
|
||||
> i valori di Sharpe/DD sopra sono ottimistici di circa il 50%. Da pianificare per le
|
||||
> decisioni: **Sharpe atteso ~5**, **worst-drawdown su 90 giorni ~6%**, profilo che regge
|
||||
> a leva 2x con slippage raddoppiato. Configurazione raccomandata: equal-weight, leva 2x,
|
||||
> con un cap sull'allocazione ai pairs (~30-35%, poiché concentrano ~57% del rischio).
|
||||
> Tutto resta da confermare nel paper trading live.
|
||||
## La riga che ordina tutto il resto
|
||||
|
||||
## Come funziona
|
||||
> La ricerca ha smesso di essere il vincolo il 2026-07-26, e **sei ondate successive lo hanno
|
||||
> confermato invece che ribaltarlo**. I vincoli sono **il capitale che entra** e **il conto che non
|
||||
> sparisce**.
|
||||
|
||||
### MR01 — Bollinger Fade (mean-reversion)
|
||||
Misurato: il miglior candidato nuovo vale **+0,046 €/giorno**; versare €500/mese invece di €250
|
||||
porta la probabilità di arrivare al traguardo in 20 anni **dal 14% all'85%**. E €100/mese in più
|
||||
equivalgono a **+4,07%/anno di drift**, cioè più di tutta la leva autorizzabile.
|
||||
|
||||
La strategia attiva sfrutta il fatto, emerso dai dati, che su BTC/ETH a 1h gli estremi
|
||||
di prezzo **rientrano verso la media** più di quanto proseguano:
|
||||
## Metodo — cosa deve superare una strategia nuova
|
||||
|
||||
1. **Bollinger Bands** (window `n`, `k` deviazioni standard) sul close.
|
||||
2. **Entry** — quando il close esce *sotto* la banda inferiore → **long** (o *sopra* la superiore → **short**). Ingresso a `close[i]`, eseguibile dal vivo.
|
||||
3. **Take-profit** alla media mobile (il rientro atteso).
|
||||
4. **Stop-loss** a `sl_atr × ATR` oltre l'estremo; **time-limit** a `max_bars`.
|
||||
Sei requisiti, nessuno negoziabile (`CLAUDE.md` §8, gate in `scripts/research/alt/altlib.py`):
|
||||
|
||||
Nessun look-ahead: direzione e livelli sono calcolati con dati fino a `close[i]`.
|
||||
1. **ingresso eseguibile** — direzione e prezzo da dati fino a `close[i]`, mai l'estremo di una candela;
|
||||
2. **backtest netto** dopo fee Deribit realistiche, più la leva;
|
||||
3. **out-of-sample** held-out, robustezza su griglia, sweep fee;
|
||||
4. **liquidità e plausibilità** — un edge su un book fermo o su wick fantasma non è un edge;
|
||||
5. i **gate**: `marginal_vs_tp01` (Sharpe marginale, non assoluto), `study_family_honest` con
|
||||
deflated-Sharpe ≥ 0,95, `day_boundary_robust`, `anchor_luck_band`, `weights_tilt_null`;
|
||||
6. codice, test e diario.
|
||||
|
||||
### Le altre famiglie
|
||||
Il progetto ha **73 regole di prim'ordine** (`CLAUDE.md` §6) — sul dato, sul metodo, sui costi,
|
||||
sulla produzione e sul piano — ognuna pagata almeno una volta. La più
|
||||
ricorrente, cinque occorrenze: *un sorvegliante deve derivare il proprio bersaglio dal codice
|
||||
sorvegliato, mai ridichiararlo — un controllo puntato su una configurazione diversa da quella che
|
||||
gira passa sempre, e non sta controllando niente.*
|
||||
|
||||
- **FADE** (oltre MR01): MR02 fada la rottura del canale Donchian verso il centro;
|
||||
MR07 fada il movimento di barra estremo misurato in deviazioni standard dei
|
||||
rendimenti. Stessa logica di reversione, indicatori indipendenti.
|
||||
- **HONEST** (long-only, multi-crypto): DIP01 compra i dip estremi e rivende al
|
||||
recupero; TR01 segue il trend con incrocio di EMA su un paniere; ROT02 ruota ogni
|
||||
giorno sui tre asset col momentum più forte, andando in cash quando BTC è sotto la
|
||||
sua media (risk-off). Coprono i regimi di trend e rotazione, complementari alle fade.
|
||||
- **PAIRS** (market-neutral): scommette sul rientro verso la media del log-ratio fra
|
||||
due cripto (z-score). Long su una, short sull'altra: l'esposizione netta al mercato è
|
||||
quasi nulla (correlazione ~0.02), il che la rende un diversificatore eccellente.
|
||||
- **TSMOM**: tiene gli asset con momentum positivo persistente su più orizzonti
|
||||
(3/6/12 mesi), con overlay risk-off. Rende meno ma è poco correlato, utile in ensemble.
|
||||
## Il dato
|
||||
|
||||
### Perché lo squeeze breakout è stato abbandonato
|
||||
- **La verità è Deribit mainnet**, perché è dove si esegue. Binance è un audit indipendente, mai
|
||||
un'ancora per "ripulire": è USDT, ~10 bps fuori, fino al 3% sotto depeg.
|
||||
- Universo certificato: **solo BTC/ETH**, ogni timeframe. Gli alt sono esclusi.
|
||||
- Lo storico si aggiorna **solo** con `rebuild_history.py` e si certifica **sempre** con
|
||||
`certify_feed.py`. Il vecchio downloader è la causa del reset.
|
||||
- La catena opzioni si **raccoglie** ogni ora (`collect_chain.py`, minuto `:25`): un'ora non
|
||||
raccolta è persa per sempre.
|
||||
|
||||
L'ipotesi originale era opposta — *continuazione* dopo la compressione di volatilità
|
||||
(Bollinger dentro Keltner → breakout direzionale). Su dati storici sembrava dare
|
||||
76-82% di accuracy, ma era un **artefatto di look-ahead**: il backtest entrava a
|
||||
`close[i-1]` con direzione decisa da `close[i]`. Replicando l'esecuzione reale
|
||||
(ingresso a `close[i]`) l'edge collassa al ~47% (lancio di moneta) e i costi fanno
|
||||
il resto. Il test sui breakout intra-barra a 5m conferma che il movimento *rientra*
|
||||
subito (mean-reversion), giustificando MR01. Tutta la famiglia squeeze è in `scripts/waste/`.
|
||||
|
||||
### Lezione metodologica
|
||||
|
||||
Ogni nuova strategia deve passare: (1) **ingresso eseguibile** senza look-ahead,
|
||||
(2) backtest **netto** dopo fee realistiche (0.10% RT Deribit), (3) validazione
|
||||
**out-of-sample** + robustezza su griglia parametri + sweep fee. Strumenti in
|
||||
`scripts/analysis/` (`strategy_research.py`, `oos_validation.py`, `intrabar_test.py`).
|
||||
|
||||
## Struttura progetto
|
||||
## Struttura
|
||||
|
||||
```
|
||||
PythagorasGoal/
|
||||
├── src/
|
||||
│ ├── data/ # Download e gestione dati (Cerbero MCP + Binance)
|
||||
│ ├── fractal/ # Indicatori frattali: Hurst, Higuchi FD, self-similarity
|
||||
│ ├── backtest/ # Motore di backtesting con fee e metriche
|
||||
│ ├── strategies/ # Classe base Strategy ABC + indicatori condivisi
|
||||
│ │ ├── base.py # Strategy, Signal, BacktestResult, YearlyStats
|
||||
│ │ └── indicators.py # keltner_ratio, detect_squeezes, ema, atr, rv, corr
|
||||
│ ├── live/ # Paper trading live su Deribit testnet
|
||||
│ │ ├── multi_runner.py # Orchestratore multi-strategia (strategie + pairs)
|
||||
│ │ ├── strategy_worker.py # Worker single-leg con stato persistente
|
||||
│ │ ├── pairs_worker.py # Worker a 2 gambe per i pairs (market-neutral)
|
||||
│ │ ├── strategy_loader.py # Import dinamico classi Strategy
|
||||
│ │ ├── cerbero_client.py # Client HTTP per Cerbero MCP
|
||||
│ │ ├── signal_engine.py # Squeeze + ML real-time (legacy) + validazione OOS
|
||||
│ │ └── telegram_notifier.py
|
||||
│ └── portfolio/ # Portafogli di prima classe (capitale condiviso, backtest + live)
|
||||
│ ├── base.py # SleeveSpec, Portfolio (.backtest), load_active_portfolio
|
||||
│ ├── weighting.py # Schemi di ponderazione: equal, cap, inverse_vol, cluster_rp, manual
|
||||
│ ├── sleeves.py # Builder unificato equity-per-sleeve (fonte unica, parità report)
|
||||
│ ├── ledger.py # PortfolioLedger: PnL/DD aggregati, persistenza e resume
|
||||
│ └── runner.py # PortfolioRunner live (Cerbero v2, sizing, ribilancio giornaliero)
|
||||
├── scripts/
|
||||
│ ├── strategies/ # Strategie con edge validato OOS (FADE, HONEST, PAIRS, TSMOM + portafogli)
|
||||
│ ├── portfolios/ # Definizioni PORT01-06 e report run() dei portafogli di prima classe
|
||||
│ ├── waste/ # Strategie scartate (squeeze SQ/MT/ML/AD/CM/PD, MR03, ROT01, W01-W28)
|
||||
│ └── analysis/ # Ricerca/validazione OOS fee-aware, gestione rischio, report
|
||||
├── strategies.yml # Config multi-strategy paper trader
|
||||
├── data/
|
||||
│ ├── raw/ # Parquet OHLCV (gitignored, ~70 MB)
|
||||
│ └── regime/ # DVOL + funding (Deribit mainnet) + cache feature regime (gitignored)
|
||||
├── VERSION # versione semver (cotta nell'immagine, mostrata nei msg Telegram)
|
||||
├── docs/
|
||||
│ ├── diary/ # Diario di ricerca giornaliero
|
||||
│ └── specs/ # Specifiche di design
|
||||
├── Dockerfile
|
||||
├── docker-compose.yml
|
||||
└── pyproject.toml
|
||||
src/
|
||||
data/downloader.py load_data(asset, tf) sui parquet certificati
|
||||
strategies/ trend_portfolio.py (TP01) · skyhook.py (SKH01) · base · indicators
|
||||
portfolio/ portfolio.py (N sleeve + weights_tilt_null) · sleeves.py · gtaa.py
|
||||
backtest/harness.py backtest onesto, senza look-ahead
|
||||
live/ book.py (esecutore netto) · deribit · livefeed · usde
|
||||
venue_watch · venue_probe · venue_news · monitor_health · scale_watch
|
||||
tradesdb · journal · analista · notifier · cli
|
||||
scripts/
|
||||
live/ book_execute · trades_db · journal · analista · balance_watch
|
||||
paper_* (forward-monitor) · usde_watch · usde_convert · fee_watch
|
||||
research/ r<data>_*.py — un file per esperimento, harness in alt/altlib.py
|
||||
analysis/ rebuild_history · certify_feed · audit_feed · multi_source_check
|
||||
cron_{book,daily,chain,balance,usde,opt_snapshot,vol_term}.sh
|
||||
docs/
|
||||
memory/ LA MEMORIA — 6 file, indicizzati in testa a CLAUDE.md
|
||||
research/ RESULTS-0822 (§1-73, un registro per filone) · BRIEF-0822 · SPEC-scale-key
|
||||
diary/ una voce per esperimento (144)
|
||||
journal/ libro di bordo, una voce al giorno, 4 livelli per provenienza
|
||||
tests/ 910, tutti verdi
|
||||
Old/ archivio pre-reset — consultabile, non fidato
|
||||
```
|
||||
|
||||
## Strategie attive
|
||||
|
||||
Le strategie single-asset estendono `src.strategies.base.Strategy`
|
||||
(`generate_signals() → backtest()`); i pairs hanno un worker dedicato a 2 gambe.
|
||||
|
||||
| Codice | Script | Famiglia | Descrizione |
|
||||
|--------|--------|----------|-------------|
|
||||
| **MR01** | `MR01_bollinger_fade.py` | FADE | Fada la banda di Bollinger, TP alla media, SL ad ATR |
|
||||
| **MR02** | `MR02_donchian_fade.py` | FADE | Fada la rottura del canale Donchian, TP al centro |
|
||||
| **MR07** | `MR07_return_reversal.py` | FADE | Fada il movimento di barra estremo (z dei rendimenti) |
|
||||
| **DIP01** | `DIP01_dip_reversion.py` | HONEST | Dip-buy long-only su z-score estremo |
|
||||
| **TR01** | `TR01_ema_trend.py` | HONEST | EMA 20/100 trend-following su paniere cripto (4h) |
|
||||
| **ROT02** | `ROT02_dual_momentum.py` | HONEST | Rotazione cross-sectional top-3 + risk-off (1d) |
|
||||
| **PR01** | `PR01_pairs_reversion.py` | PAIRS | Spread reversion market-neutral su 5 coppie |
|
||||
| **TSM01** | `tsmom_research.py` | TSMOM | Time-series momentum multi-orizzonte + risk-off |
|
||||
| **SH01** | `SH01_shape_ml.py` | SHAPE | LogisticRegression walk-forward su 17 feature di forma, orizzonte 12 barre (diversificatore) |
|
||||
|
||||
Le fade applicano tre protezioni live: un **filtro trend** (`trend_max`/`ema_long`,
|
||||
salta i segnali col prezzo troppo esteso rispetto alla EMA200), un **loss-guard Hurst**
|
||||
(`hurst_max=0.55`, salta i segnali in regime persistente/trending dove si concentrano gli stop-loss
|
||||
— dimezza il drawdown del portafoglio, calcolato dalle sole close) e l'**EXIT-16 close-confirm SL**
|
||||
(`sl_confirm_atr=0.5`, 2026-06-04: lo stop scatta solo se la barra *chiude* oltre `sl ∓ 0.5·ATR14` —
|
||||
gli stop intrabar da wick erano falsi negativi, l'overshoot che buca lo stop è proprio il movimento
|
||||
che la fade fada; a livello PORT06 porta l'OOS Sharpe da 8.82 a 10.06). Più un filtro `min_tp_frac`
|
||||
che scarta i micro-scalp col take-profit entro il costo delle fee. Le tre protezioni sono
|
||||
complementari: Hurst toglie il regime tossico, il trend-filter gli ingressi sovra-estesi, il
|
||||
close-confirm i falsi stop. Portafogli pronti: `PORT01`
|
||||
(honest), `PORT02` (fade), `PORT03` (master fade+honest), **`PORT06`** (master esteso, default live).
|
||||
|
||||
**Scartate** (in `scripts/waste/`): la famiglia squeeze (SQ01-04, ML01, MT01, PD01,
|
||||
CM01, AD01 — artefatto di look-ahead), MR03 Keltner (debole/ridondante con MR01) e
|
||||
ROT01 (dominata da ROT02).
|
||||
|
||||
### Comandi utili
|
||||
## Comandi
|
||||
|
||||
```bash
|
||||
# Backtest di una strategia
|
||||
uv run python scripts/strategies/MR01_bollinger_fade.py
|
||||
uv run python scripts/strategies/PR01_pairs_reversion.py
|
||||
|
||||
# Ricerca e validazione fee-aware out-of-sample
|
||||
uv run python scripts/analysis/strategy_research.py # screening famiglie + deep-dive fade
|
||||
uv run python scripts/analysis/strategy_research_v2.py # MR02 / MR03 / MR07
|
||||
uv run python scripts/analysis/oos_validation.py # perche' la famiglia squeeze e' scartata
|
||||
uv run python scripts/analysis/pairs_research.py # ricerca + verifica no-look-ahead dei pairs
|
||||
|
||||
# Gestione rischio, combinazione, report
|
||||
uv run python scripts/analysis/risk_management.py # filtro trend + portafoglio fade
|
||||
uv run python scripts/analysis/combine_portfolio.py # combinare fade + honest
|
||||
uv run python scripts/analysis/combine_v2.py # master esteso con pairs + TSM01
|
||||
uv run python scripts/analysis/report_families.py # report per anno di tutte le famiglie
|
||||
|
||||
# Validazione dei worker live (replay == backtest)
|
||||
uv run python scripts/analysis/validate_worker_mr01.py # worker single-leg su MR01
|
||||
uv run python scripts/analysis/validate_worker_pairs.py # worker a 2 gambe sui pairs
|
||||
uv run python scripts/analysis/live_smoke_pairs.py # smoke test feed live reale dei pairs
|
||||
uv sync # dipendenze
|
||||
uv run python scripts/analysis/rebuild_history.py --asset BTC ETH # storico da Deribit mainnet
|
||||
uv run python scripts/analysis/certify_feed.py # certifica i feed
|
||||
uv run python scripts/live/trades_db.py --report # stato del libro + TWR
|
||||
uv run python scripts/live/journal.py # voce del giorno
|
||||
uv run python scripts/live/analista.py --secco # analisi del giorno, senza salvare
|
||||
uv run python scripts/portfolio/run_portfolio.py # portafoglio di ricerca
|
||||
uv run pytest # 910 test
|
||||
```
|
||||
|
||||
## Paper Trading Live
|
||||
Ogni script di `scripts/live/` accetta `--help` e **rifiuta un flag che non conosce** (esce 2): fino
|
||||
al 2026-09-02 un flag sbagliato eseguiva l'azione di default, e su tre script quell'azione scriveva.
|
||||
|
||||
Il multi-strategy runner esegue N strategie in parallelo su dati live da Cerbero MCP,
|
||||
ognuna con €1000 USDC virtuali indipendenti. Gestisce due tipi di worker:
|
||||
## Gate aperti
|
||||
|
||||
- **Single-leg** (`strategy_worker.py`): per le strategie direzionali. Se un `Signal`
|
||||
porta `tp`/`sl`/`max_bars` in `metadata` (come le fade), chiude su take-profit /
|
||||
stop-loss / time-limit; altrimenti usa il fallback `hold_bars`/stop -2%.
|
||||
- **Due gambe** (`pairs_worker.py`): per i pairs market-neutral. Apre long su una gamba
|
||||
e short sull'altra, esce sul rientro dello z-score o per time-limit, conta le fee su
|
||||
entrambe le gambe. Validato: il replay storico coincide *esattamente* col backtest.
|
||||
Un gate si decide **alla data**, coi criteri scritti **prima**. Elenco completo in `CLAUDE.md` §4.
|
||||
|
||||
### Avvio
|
||||
| gate | data | dove punta oggi |
|
||||
|---|---|---|
|
||||
| STATARB | 2026-09-27 | ritiro (Sharpe −1,61 sulla serie vera) |
|
||||
| SCALA-01 — leva 1,00 → 1,25 | non prima del 2026-10-01 | chiave costruita e **inerte**; 9 condizioni, 2 fatte |
|
||||
| XSR01 | 2026-10-23 | sotto la lente rendita il meccanismo vale ~0 |
|
||||
| DVOLSPREAD | kill 2026-10-24 | serie −4,41 |
|
||||
| GTAA01 tenere/bloccare | al book a $15k | contributo reale +0,10/+0,12 di Sharpe, non incassabile sotto la soglia |
|
||||
|
||||
```bash
|
||||
# Locale
|
||||
uv run python -m src.live.multi_runner
|
||||
## Obiettivo, e cosa lo rende difficile
|
||||
|
||||
# Docker
|
||||
docker compose up -d
|
||||
```
|
||||
Il target dichiarato è **€50/giorno partendo da €1.000**. Non è raggiungibile a questo capitale:
|
||||
servono ~$313k (banda [$187k – $1,14M]) o **€1.733/mese per dieci anni** per averlo al 90% di
|
||||
probabilità. La leva non è la scorciatoia — quella difendibile vale quanto €100-150/mese di
|
||||
versamento. La via è target-vol, capitale e tempo.
|
||||
|
||||
### Configurazione
|
||||
|
||||
Le strategie attive sono definite in `strategies.yml`:
|
||||
|
||||
```yaml
|
||||
defaults:
|
||||
capital: 1000
|
||||
position_size: 0.15
|
||||
leverage: 3
|
||||
|
||||
strategies: # strategie single-leg
|
||||
- name: MR01_bollinger_fade
|
||||
asset: BTC
|
||||
tf: 1h
|
||||
enabled: true
|
||||
params: { bb_window: 50, k: 2.5, sl_atr: 2.0, max_bars: 24, trend_max: 3.0, ema_long: 200 }
|
||||
|
||||
pairs: # strategie a 2 gambe (market-neutral)
|
||||
- name: PR01_pairs_reversion
|
||||
a: ETH
|
||||
b: BTC
|
||||
tf: 1h
|
||||
enabled: true
|
||||
params: { n: 50, z_in: 2.0, z_exit: 0.75, max_bars: 72, jump_max: 0.08 }
|
||||
```
|
||||
|
||||
Per aggiungere una strategia: nuova riga in `strategies.yml` (sezione `strategies` o
|
||||
`pairs`), poi `docker compose restart`. Lo storico delle strategie esistenti rimane intatto.
|
||||
|
||||
### Persistenza
|
||||
|
||||
Ogni strategia ha la sua directory in `data/paper_trades/`:
|
||||
|
||||
```
|
||||
data/paper_trades/
|
||||
MR01_bollinger_fade__BTC__1h/
|
||||
trades.jsonl # Storico trade append-only
|
||||
status.json # Stato corrente (resume al restart, include tp/sl/max_bars)
|
||||
```
|
||||
|
||||
Notifiche Telegram per ogni trade (richiede `TELEGRAM_BOT_TOKEN` e `TELEGRAM_CHAT_ID` in `.env`).
|
||||
|
||||
## Paper Trading a Portafoglio
|
||||
|
||||
Accanto al multi-strategy runner originale — in cui ogni strategia gestisce autonomamente il proprio conto virtuale da €1.000 — il progetto dispone ora di un **paper trader a portafoglio** (`src/portfolio/`) che tratta l'insieme delle strategie come un unico organismo con un capitale condiviso.
|
||||
|
||||
### Come funziona
|
||||
|
||||
La definizione di un portafoglio (`SleeveSpec` + schema di peso) ha due facce sulla stessa sorgente dati:
|
||||
|
||||
- **Backtest** (`.backtest()`): ricostruisce le equity-curve di ogni sleeve tramite il builder unificato in `sleeves.py`, le pondera secondo lo schema scelto e calcola le metriche aggregate (CAGR, Sharpe, max DD). La parità con i report prodotti da `report_families.py` è garantita dalla fonte unica.
|
||||
- **Live** (`PortfolioRunner`): ogni ora il runner scarica le candele aggiornate via Cerbero v2, calcola i pesi correnti, avvia i worker appropriati per ogni sleeve attiva e registra il PnL aggregato nel ledger (`data/portfolios/{code}/`). Il ledger persiste tra i riavvii.
|
||||
|
||||
### Schemi di ponderazione
|
||||
|
||||
Il modulo `weighting.py` mette a disposizione cinque schemi: `equal` (default), `cap` (tetto per famiglia — p.es. `pairs: 0.33` per limitare la concentrazione), `inverse_vol` (pesi inversamente proporzionali alla volatilità storica), `cluster_rp` (equal tra cluster naturali poi inverse-vol all'interno del cluster) e `manual` (pesi liberi). Lo schema si specifica in `portfolios.yml` insieme al codice portafoglio e alla leva.
|
||||
|
||||
### Portafoglio di default: PORT06
|
||||
|
||||
La configurazione raccomandata è **PORT06** (`scripts/portfolios/PORT06_master_shape.py`): portafoglio master esteso che include tutte e sei le famiglie (FADE, HONEST, PAIRS, TSMOM, SHAPE), con schema `cap` che limita i pairs al 33% del capitale per moderare la loro concentrazione di rischio. Backtest canonico (dati al 2026-05-28): Sharpe 6.47 (FULL) / 8.82 (OOS), drawdown massimo 4.10% (FULL) / 1.30% (OOS), leva 2×; **con la config live attuale (EXIT-16 close-confirm): Sharpe 7.84 / 10.06, DD 2.60% / 1.15%**.
|
||||
|
||||
### Scope live
|
||||
|
||||
Il runner esegue **tutti e 17 gli sleeve** di PORT06: **fade** (MR01, MR02, MR07 × BTC/ETH),
|
||||
**honest** (DIP01, TR01-basket 4h, ROT02-rotation 1d), **pairs** (PR01, cinque coppie),
|
||||
**TSMOM** (TSM01 1d) e **shape** (SH01 × BTC/ETH). Worker dedicati: `StrategyWorker` (single-leg, fade/
|
||||
dip/**shape**), `PairsWorker` (2 gambe), `BasketTrendWorker`, `RotationWorker`, `TsmomWorker`. Il runner
|
||||
fetcha 1h da Cerbero v2 e resampla a 4h/1d; il pool di capitale, il ribilancio giornaliero e il ledger
|
||||
sono validati == backtest.
|
||||
|
||||
> **SH01 (2026-06-01):** gira come `StrategyWorker` normale (il walk-forward è interno a
|
||||
> `generate_signals`). Il vecchio `MLWorkerWrapper` usava il `SignalEngine` **squeeze scartato** —
|
||||
> rimosso. **Loss-guard Hurst (2026-06-02):** le fade saltano i segnali in regime persistente
|
||||
> (rolling-Hurst ≥ 0.55), dove si concentrano gli stop-loss — dimezza il drawdown del portafoglio
|
||||
> (FULL 4.1%→2.4%; stop-loss fade −67% in numero, perdite totali −68%). Calcolato dalle sole close,
|
||||
> attivo live (`hurst_max` nei params). Il report orario su Telegram **monitora lo stop-rate fade
|
||||
> prima/dopo l'attivazione** e dà il verdetto automatico quando il campione è sufficiente.
|
||||
|
||||
### Esecuzione reale (shadow, Deribit testnet)
|
||||
|
||||
Sette sleeve single-leg — le **6 fade** (MR01/MR02/MR07 × BTC/ETH) e **DIP01** (dal 2026-06-04) —
|
||||
eseguono ordini **reali su Deribit testnet** accanto al fill simulato (*shadow*: il sim resta la
|
||||
verità che guida le decisioni; il reale misura la fattibilità). Punti chiave:
|
||||
|
||||
- **Strumenti lineari USDC** (`BTC_USDC`/`ETH_USDC-PERPETUAL`): payoff lineare = matematica del
|
||||
backtest; fee e PnL in USDC. Quantizzazione `Decimal` di amount (step) e prezzi (tick).
|
||||
- **Take-profit reale = limit reduce-only AL livello** (v1.0.7): piazzato all'apertura, copre la
|
||||
sola quota del worker (gli strumenti sono condivisi fra worker e nettati per conto); alla
|
||||
chiusura il worker cancella il resting, riconcilia i fill dal trade history per `order_id` e
|
||||
chiude a market solo il residuo. Fix della divergenza misurata: il market-on-poll usciva
|
||||
+235 bps oltre il livello TP. Fill da resting = fee maker (~0%).
|
||||
- **Stop-loss close-confirm** (v1.1.0): uscita al close che sfonda il livello → market
|
||||
reduce-only al poll (nessun ordine stop sul book, per scelta: i trigger Deribit generano un
|
||||
nuovo order_id allo scatto, non verificabile, e i wick non devono stoppare).
|
||||
- **Verifica sul trade** (order_id in `get_trade_history`), fee reali dai `trades[]`, ledger
|
||||
reale parallelo persistito (`real_capital`), eventi `REAL_OPEN`/`REAL_TP_RESTING`/`REAL_CLOSE`
|
||||
nel log + alert Telegram (`REAL_EXEC_LIVE`, `REAL_OPEN_FAIL`).
|
||||
- Config in `portfolios.yml` → `overrides.execution {enabled, sleeves, instruments}`.
|
||||
**Pairs/rotation/TSMOM/shape restano simulati**: i pairs richiedono un executor a 2 gambe
|
||||
(leg-risk), i multi-asset un rebalance-to-target; roadmap nel diario.
|
||||
|
||||
### Versione & deploy
|
||||
|
||||
Ogni deploy ha una **versione** (file `VERSION`, semver) che compare nei messaggi Telegram (notifiche
|
||||
trade + report orario), così correli ogni messaggio al codice che l'ha generato. Il sorgente è **cotto
|
||||
nell'immagine** → per aggiornare il live serve un **rebuild**, non un semplice restart:
|
||||
|
||||
```bash
|
||||
./scripts/deploy.sh # bump patch (1.0.0 → 1.0.1) + commit + rebuild + ricrea container
|
||||
./scripts/deploy.sh minor # 1.0.x → 1.1.0
|
||||
```
|
||||
|
||||
Il volume `data/` persiste tra i deploy → i worker fanno RESUME dello stato (capitale, posizioni aperte).
|
||||
|
||||
### Avvio del paper trader a portafoglio
|
||||
|
||||
```bash
|
||||
# Backtest del portafoglio di default (PORT06)
|
||||
uv run python scripts/portfolios/PORT06_master_shape.py
|
||||
|
||||
# Paper trading live a portafoglio
|
||||
uv run python -m src.portfolio.runner
|
||||
|
||||
# Report orario su Telegram (stato + stop-rate fade prima/dopo loss-guard) — via cron
|
||||
uv run python scripts/portfolios/hourly_report.py
|
||||
|
||||
# Smoke test del data layer Cerbero v2
|
||||
uv run python scripts/analysis/smoke_portfolio.py
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Clona e installa
|
||||
git clone <repo-url> && cd PythagorasGoal
|
||||
uv sync
|
||||
|
||||
# Scarica dati storici (~70 MB)
|
||||
uv run python -m src.data.downloader
|
||||
|
||||
# Backtest strategia attiva
|
||||
uv run python scripts/strategies/MR01_bollinger_fade.py
|
||||
|
||||
# Paper trading live
|
||||
uv run python -m src.live.multi_runner
|
||||
```
|
||||
|
||||
### Requisiti
|
||||
|
||||
- Python ≥ 3.11
|
||||
- [uv](https://docs.astral.sh/uv/) come package manager
|
||||
- Accesso a Cerbero MCP (`cerbero-mcp.tielogic.xyz`) per dati Deribit live
|
||||
- Docker (opzionale, per deploy su VPS)
|
||||
|
||||
## Dati
|
||||
|
||||
| Asset | Timeframe | Copertura |
|
||||
|-------|-----------|-----------|
|
||||
| BTC, ETH | 5m / 15m / 1h | 2018-01 → oggi |
|
||||
| SOL, LTC, ADA, XRP, BNB, DOGE | 15m / 1h | 2019-2022 → oggi (variabile per asset) |
|
||||
|
||||
Fonte primaria: perpetual Deribit via Cerbero MCP. Fallback: Binance spot via ccxt.
|
||||
Formato: Apache Parquet (in `data/raw/`, gitignored).
|
||||
|
||||
> **Nota sul naming Deribit (per il feed live).** I major sono perpetui *inverse*
|
||||
> (`BTC-PERPETUAL`, `ETH-PERPETUAL`); gli altcoin sono perpetui *lineari USDC*
|
||||
> (`SOL_USDC-PERPETUAL`, `LTC_USDC-PERPETUAL`, …) con storia dal 2022. Attenzione:
|
||||
> `LTC-PERPETUAL`/`ADA-PERPETUAL` non esistono e `SOL-PERPETUAL` restituisce dati
|
||||
> errati — per gli altcoin usare sempre la forma `_USDC-PERPETUAL`.
|
||||
|
||||
### Discovery & validazione strumenti
|
||||
|
||||
`src/data/instruments.py` scopre e **valida** gli strumenti disponibili sugli
|
||||
exchange implementati — **Deribit** e **Hyperliquid** (esclusi Alpaca/stocks e
|
||||
**Bybit**, feed testnet inaffidabile). Ogni perpetuo viene testato sui dati
|
||||
storici realmente raccoglibili: esistenza, congruenza OHLC, contratto non-morto,
|
||||
liquidità e **congruenza prezzo cross-exchange** (mediana per base-coin, tolleranza
|
||||
5%) — così feed farlocchi e contratti sbagliati (es. `SOL-PERPETUAL`=9.6) vengono
|
||||
scartati. Il risultato è `data/instruments_registry.json` (strumenti validi +
|
||||
timeframe + data d'inizio).
|
||||
|
||||
**Solo gli strumenti validati possono essere scaricati**: il downloader ha un gate
|
||||
(`_download_cerbero_range`) che rifiuta quelli non nel registry. Rigenera con:
|
||||
|
||||
```bash
|
||||
uv run python -m src.data.instruments
|
||||
```
|
||||
|
||||
Simboli Deribit: BTC/ETH = `<COIN>-PERPETUAL` (inverse); altcoin =
|
||||
`<COIN>_USDC-PERPETUAL` (lineari USDC). Registry attuale (testnet): Deribit 18/106
|
||||
validi (major liquidi, BTC dal 2018), Hyperliquid 66/74.
|
||||
|
||||
## Riferimenti
|
||||
|
||||
- Serleto, L. & Malanga, C. — *Pythagoras Trading Prediction* (2024)
|
||||
- Serleto, L. & Malanga, C. — *Libro dei Frattali* (2024)
|
||||
|
||||
## Licenza
|
||||
|
||||
Uso privato. Non destinato alla distribuzione.
|
||||
**Onestà prima di tutto**: nessun numero va creduto finché non è netto fee, out-of-sample, robusto
|
||||
su griglia, e su dati certificati, liquidi ed eseguibili. Il resto di questo repo esiste per rendere
|
||||
quella frase verificabile invece che dichiarata.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# 2026-09-02 — Debito 14: il report stampa il TWR, non il bonifico
|
||||
|
||||
*Scritto il 2026-09-02 fra le 14:16Z e le 14:30Z. Numeri dalla serie `equity` di `trades.db`
|
||||
(1.678 letture) e dal report rilanciato dopo la riparazione.*
|
||||
*Scritto il 2026-09-02 fra le 14:16Z e le 14:22Z (commit `2f701b8`, 14:22:50Z; la prima stesura diceva «fino alle 14:30Z» senza averlo letto — corretto in revisione). Numeri dalla lettura di equity delle 13:47:01Z (1.678 letture) e dal report rilanciato dopo la riparazione.*
|
||||
|
||||
## Cosa c'era
|
||||
|
||||
@@ -51,3 +50,27 @@ nel test, nel CLAUDE.md e qui.
|
||||
dollari e la sua pagina ha una forma testata. Aggiungerlo è una riga, ma non era il debito.
|
||||
- Non ho scomposto lo scarto (~$25) fra round-trip + non realizzato e il trading al netto: era già
|
||||
fuori scope il 01/09 e lo resta.
|
||||
|
||||
## Revisione del codice, prima tornata (15:15Z, commit `835e0c8` 15:17:05Z) — tre cose trovate, tutte riparate
|
||||
|
||||
| trovato | vero? | riparazione |
|
||||
|---|---|---|
|
||||
| l'intervallo che contiene un movimento certo esce intero dal rendimento: il suo P&L di mercato finisce in `certi` e fuori da `trading`, e nessun documento lo diceva | sì, per costruzione. Errore massimo = metà del movimento riconosciuto (margine del classificatore). Il 25/08: ~$263 lordi a ±0,2% → ~$0,5 | dichiarato nel docstring, nel report, in CLAUDE.md §5.14 e in memoria 40. Non si stima (P12) |
|
||||
| a base di equity zero `trading` era un numero mentre `twr` era None, e sbagliato: il salto 0→X è invisibile al classificatore, `trading` valeva l'intero conto sotto l'etichetta "al netto dei versamenti" | sì. Non raggiungibile dal cron (non scrive mai 0), ma raggiungibile da un backfill | tre stati veri: `twr` e `trading` entrambi None con motivo; il report stampa n/d. Test in `test_journal.py` e `test_trades_report.py` |
|
||||
| movimento nel primo intervallo o due consecutivi → segmento `+0.00% [x -> x]` | sì, cosmetico | i segmenti a lunghezza zero non si producono; nessun tempo a mercato → TWR 0,0 dichiarato numero. Test |
|
||||
|
||||
Il numero pubblicato non cambia: +10,61% alle 14:02Z, +10,56% alle 14:47Z per la marcatura.
|
||||
|
||||
## Seconda tornata (15:30Z) — la piu' importante di tutte
|
||||
|
||||
| trovato | vero? | riparazione |
|
||||
|---|---|---|
|
||||
| il classificatore dei movimenti era **cieco ~23 ore al giorno**: il feed 1h si ferma alle 00:00, `asof` dava la stessa barra alle due letture, mercato «fermo» = 0, qualunque calo ≥10% del giorno diventava «movimento». Riprodotto dalla revisione su copia del DB: −12% appeso alle 14:47 → `certi` 1.399 → 1.153, TWR invariato invece di −2,71% | si'. Verificato: ultima barra 00:00, adesso 15:26 | nuovo stato **«mercato non misurabile»** (feed fermo, assente, o barra mancante) ⇒ `ambiguo` con la ragione; `twr` e `trading` None con motivo |
|
||||
| con `data/raw` assente (host ripristinato prima del rebuild) tutto era «ambiguo», `certi` 0, e il report stampava **+243% sotto l'etichetta TWR** | si' | stesso stato: n/d, mai il grezzo |
|
||||
| «`rendimento_twr` e' la funzione che entrambi chiamano» era **falso**: `pnl_giorno` rifaceva `cum − certi`, e la testata Telegram mandava il cumulato grezzo | si' | `pnl_giorno` chiama `rendimento_twr`; la pagina stampa il TWR accanto al cumulato; la testata dice «di cui versati → trading» |
|
||||
| `leva_tetto` del classificatore ridichiarava `frac × n` ignorando `book_scale_k` e `LEVA_LORDA_MAX` (P1) | si' | `min(frac × n × scala, LEVA_LORDA_MAX)`; fallback = tetto di codice |
|
||||
| il «limite ereditato: +10% di trading a mercato fermo» scritto la mattina era un **artefatto della fixture piatta** (a mercato misurato il trading non supera 2·max_mkt) | si' | cancellato ovunque; il limite vero e' quello sopra |
|
||||
| il bound di mercato guarda solo BTC/ETH, ma il 31% dell'equity e' USDE: un depeg pieno sarebbe un «prelievo» | si' | **non riparato**: l'indice USDE e' registrato una volta al giorno. Debito **§5.15** |
|
||||
| l'ora del report scritta in §2 (14:02Z) era l'ora di esecuzione, non quella della lettura (13:47:01Z); i tempi «scritto» dei diari erano posteriori ai commit | si' | corretti coi tempi veri |
|
||||
|
||||
Il numero pubblicato: TWR +10,61% alla lettura delle 13:47Z; +10,56% alle 14:47Z.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# 2026-09-02 — Debito 7: il docstring diceva 230 minuti, il cron gira ogni ora
|
||||
|
||||
*Scritto il 2026-09-02 fra le 14:36Z e le 14:38Z (commit `861cc7f`, 14:38:53Z; la prima stesura diceva «alle 14:45Z» senza averlo letto — corretto in revisione). Cadenza misurata dal log (`r0823_sl_anchor.py`: 1.443 giri in
|
||||
1.442 ore) e dalla crontab installata, letta dal test.*
|
||||
|
||||
## Il difetto
|
||||
|
||||
| fonte | cosa diceva | dal |
|
||||
|---|---|---|
|
||||
| `scripts/live/book_execute.py`, docstring | «va lanciato ogni ~230 minuti» | 20/06 |
|
||||
| `scripts/cron_book.sh`, intestazione | cadenza ORARIA, `47 * * * *` | 25/08 (prima `:07`, sempre orario) |
|
||||
| crontab della VPS | `47 * * * *` | 25/08 |
|
||||
|
||||
Era il **docstring** a sbagliare. E non era una svista innocua: sulla riga 4h (la più vicina ai 230
|
||||
minuti) BTC **raddoppia** gli scatti del disaster-SL rotolante (2 contro 1; a 24h ETH arriva a 5, con
|
||||
2 in 30 giorni). Il rischio era un lettore diligente che "allineasse" il cron al docstring.
|
||||
|
||||
## La riparazione
|
||||
|
||||
1. **Docstring riscritto**: `CADENZA: ORARIA`, la riga di crontab citata, la ragione (giro
|
||||
idempotente → girare più fitto della griglia non costa ordini; latenza ≤1h sugli ingressi/uscite
|
||||
software di SKH01; ri-ancoraggio orario del rotolante) e il divieto esplicito, con la data.
|
||||
2. **`tests/test_book_cadenza.py`, 10 test.** P1: non ridichiara «60 minuti», **deriva** le tre
|
||||
fonti e le confronta fra loro:
|
||||
- il docstring non contiene più «ogni ~230» e dichiara ORARIA;
|
||||
- la riga di crontab citata nel docstring è quella dichiarata in `cron_book.sh`;
|
||||
- la riga dichiarata è oraria (parser a 5 campi, solo casi regolari, rifiuta il resto),
|
||||
sotto i 230 della griglia e lontana dai 240 della riga peggiore, e fuori dal minuto `:00`;
|
||||
- **la crontab installata** (`crontab -l`) coincide con la dichiarata. Dove la crontab non è
|
||||
leggibile il test è **saltato**, non verde (P5). Sulla VPS oggi gira e passa.
|
||||
|
||||
## Numeri
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| test nuovi | 10, tutti verdi; suite dei moduli che toccano `book_execute`: 231 verdi |
|
||||
| cadenza dichiarata / installata | 60 min / 60 min |
|
||||
| griglia SKH01 | 230 min (la riga da non usare) |
|
||||
| riga peggiore misurata | 240 min (BTC 2 scatti contro 1) |
|
||||
|
||||
## Cosa NON ho fatto
|
||||
|
||||
- Non ho toccato il cron né `cron_book.sh`: la configurazione che gira era quella giusta.
|
||||
- Non ho aggiunto un sorvegliante a runtime sulla cadenza (contare i giri per ora nel log): il
|
||||
giornale già stampa «giri mancanti» ogni giorno, e il test copre la deriva di configurazione.
|
||||
|
||||
## Revisione del codice, prima tornata (15:15Z, commit `835e0c8`) — due cose trovate, tutte riparate
|
||||
|
||||
| trovato | vero? | riparazione |
|
||||
|---|---|---|
|
||||
| il docstring nuovo diceva «il disaster-SL rotolante si ri-ancora ogni ora»: falso. `ensure_disaster_sl` lascia il bracket com'è finché lo stop voluto è entro il 5% da quello piazzato e la taglia entro il 10%; il giro orario **controlla**, ri-ancora oltre la tolleranza (mark +5,263% / −4,762%). I «BTC 0 / ETH 1» di `r0823` sono misurati **con** questa isteresi | sì, verificato in `execution.py:233-235`. Propagato anche a CLAUDE.md §5.7 e memoria 40 | riscritto in tutti e tre i posti: «controllato ogni ora, ri-ancorato solo oltre la tolleranza; lo stop siede fra −26,3% e −33,3% dal mark corrente». Frase sbagliata su un meccanismo di sicurezza vivo: era la segnalazione più importante |
|
||||
| la guardia del test cercava due letterali («ogni ~230»): «ogni quattro ore» sarebbe passato | sì | la guardia estrae **ogni** prescrizione «ogni/every N unità» e pretende 60 minuti; controllo positivo (M15) su cinque frasi. Limite dichiarato (P13): una prosa che prescrive senza «ogni» passa |
|
||||
|
||||
## Seconda tornata (15:30Z)
|
||||
|
||||
| trovato | vero? | riparazione |
|
||||
|---|---|---|
|
||||
| la frase nuova di CLAUDE.md §5.7 era **invertita** («chi lo avesse corretto nel verso del cron»): correggere il docstring verso il cron e' proprio la riparazione; il pericolo e' correggere il CRON verso il docstring | si' | riscritta nel verso giusto |
|
||||
| la banda dello stop era sbagliata: con isteresi 5% sullo stop (0,70·mark) lo stop siede a 0,665–0,735 × mark, cioe' **−33,5% / −26,5%**, non −26,3/−33,3 | si' | corretta in docstring, CLAUDE.md, memoria |
|
||||
| «BTC raddoppia gli scatti» leggeva male `r0823`: il «2 contro 1» e' rotolante contro pavimento **dentro la riga 4h**; a 1h il rotolante ne fa 0, e la riga peggiore misurata e' **24h** (ETH 5) | si' (la revisione ha eseguito `r0823.simula`) | riscritto ovunque coi conteggi; nel test `RIGA_4H_MIN`, non «peggiore»; `LTF_MIN` importato da `skyhook` (P1) |
|
||||
| «girare piu' fitto non costa ordini» era falso: 22 dei 48 ordini live sono ri-taglie |Δ|≤$10 a segnale invariato; il deadband e' in valuta assoluta (C2) | si' (contati) | riscritto: «costa solo micro-ordini di ri-taglia», con la regola C2 |
|
||||
| «latenza ≤1h» senza condizione: vale solo con la feed 5m fresca; `fresh_5m` ripiega in silenzio sul giornaliero e `skh_feed_max_age_min` allerta e basta | si' | condizione scritta |
|
||||
| `_cron_installato` leggeva la **prima** riga: una seconda riga attiva sotto (`17 * * * *`, dentro lo slot di release) era invisibile | si' (simulato) | si contano **tutte** le righe attive, e devono essere una |
|
||||
| lo skip collassava tre stati: crontab illeggibile, leggibile senza il progetto, leggibile col progetto ma senza `cron_book.sh` attivo (= libro non schedulato) | si' | tre stati: SALTATO, SALTATO, **ROSSO** |
|
||||
| solo il vincolo 1 del `:47` era testato (minuto ≠ 00); il vincolo 2 (fuori dallo slot di release del martedi') no | si' | test con `venue_probe.in_release_window`, il :07 come controllo positivo |
|
||||
| `_cron_dichiarato` accettava solo `N * * * *` e falliva come «assente» su `*/30`; `cadenza_minuti` accettava `*/45`, `0 */5`, `*/0` | si' | regex a 5 campi qualunque, regolarita' e passo > 0 imposti, parametrizzati |
|
||||
| `r0823_sl_anchor.py` si rifiutava di girare nella finestra del **vecchio** `:07` e stampava a runtime che il docstring «prescrive ~230 min»; `cron_chain.sh` giustificava il :25 «fuori dal :07» | si' | guardia sul :47, prosa al passato, commento aggiornato |
|
||||
@@ -0,0 +1,103 @@
|
||||
# 2026-09-02 — Un flag sconosciuto non è l'azione di default
|
||||
|
||||
*Scritto fra le 15:45Z e le 16:05Z. Il difetto è stato misurato, non ipotizzato: la revisione del
|
||||
codice di questa sessione ha eseguito `trades_db.py --help` come sonda di tempi e lo ha visto
|
||||
sincronizzare il DB.*
|
||||
|
||||
## 1. Il difetto
|
||||
|
||||
Nessuno dei 18 script di `scripts/live/` usava argparse. Leggevano gli argomenti così:
|
||||
|
||||
```python
|
||||
if "--reconcile" in args: ...
|
||||
elif "--report" in args: ...
|
||||
else: sync() # <- qui finiva QUALUNQUE cosa non riconosciuta
|
||||
```
|
||||
|
||||
Un flag sbagliato non era un errore: era il ramo finale. Costo osservato e costi possibili:
|
||||
|
||||
| script | `--help` faceva | gravità |
|
||||
|---|---|---|
|
||||
| `trades_db.py` | `sync()`: riscriveva `meta.ultimo_sync` | osservato oggi, danno nullo |
|
||||
| `journal.py` | scriveva la pagina di oggi e una riga nel DB | non osservato, ma scrive |
|
||||
| `analista.py` | spendeva una chiamata al modello e mandava un Telegram | non osservato, ma spende |
|
||||
|
||||
## 2. La riparazione, e perché non è argparse
|
||||
|
||||
`src/live/cli.valida(nome, uso, flag=..., con_valore=...)`, chiamata come **prima istruzione** di
|
||||
ogni blocco `__main__` — prima di `connect()`, prima del sync, prima della rete.
|
||||
|
||||
- `--help` / `-h` → stampa l'uso, esce **0**;
|
||||
- flag non dichiarato, o valore mancante, o posizionale → stderr con **cosa** e **cosa si poteva**
|
||||
(P4), esce **2** (errore d'uso, distinto dall'1 con cui gli script segnalano un esito negativo);
|
||||
- tutto il resto invariato.
|
||||
|
||||
**Argparse no.** Cambierebbe messaggi d'errore, codici d'uscita e il comportamento di `--help` su
|
||||
18 script che il cron già invoca: romperebbe ciò che gira per riparare ciò che non gira mai. Qui
|
||||
serviva l'opposto — lasciare intatti i flag esistenti e rifiutare solo l'ignoto.
|
||||
|
||||
## 3. Il test guarda entrambe le metà del contratto
|
||||
|
||||
`tests/test_cli_flag.py`, 30 test:
|
||||
|
||||
- **l'elenco degli script si deriva dalla cartella** (P1): uno script nuovo che legge `sys.argv` a
|
||||
mano e non chiama `valida` fa fallire il test, non passa inosservato perché nessuno ha aggiornato
|
||||
una lista;
|
||||
- `valida` dev'essere la **prima istruzione** di `__main__` (uscire dopo l'effetto non è uscire);
|
||||
- l'uso deve **nominare** i flag che lo script accetta;
|
||||
- **i flag che il cron usa davvero devono restare accettati** (P15/P16, letti dai `cron_*.sh`): una
|
||||
guardia che ferma il libro di bordo la notte stessa sarebbe peggio del difetto;
|
||||
- end-to-end sui tre script che scrivono: `--help` esce 0 e **non tocca `trades.db`**; un flag
|
||||
ignoto esce 2 e non lo tocca (M15: il caso che ha causato il difetto, non un caso vicino).
|
||||
|
||||
Verificato a mano anche il contrario: `monitor_health.py --quiet`, `trades_db.py --sync --quiet`
|
||||
e `book_execute.py` in dry-run escono 0 come prima.
|
||||
|
||||
## 4. Il resto della sessione
|
||||
|
||||
**Indice USDE orario** (debito §5.15, primo passo). Il classificatore dei movimenti di capitale
|
||||
confronta due letture di equity a un'ora di distanza e il suo bound guarda solo BTC/ETH, mentre il
|
||||
31% dell'equity è USDE: un depeg sarebbe scorporato come un prelievo. `usde_watch` registra
|
||||
l'indice una volta al giorno — troppo rado. Ora `balance_watch` (orario, sola lettura) lo registra
|
||||
a ogni campione, `None` con la ragione se non leggibile, mai 1,0. Il cablaggio nel bound viene
|
||||
quando la serie ha storia: prima serve il dato, poi la regola.
|
||||
|
||||
**Pulizia lasciata dalla revisione.** `tests/helpers.carica_script` sostituisce la dodicesima copia
|
||||
del caricatore `importlib` (15 in giro, con nomi di modulo diversi per lo stesso file e differenze
|
||||
silenziose su `sys.modules`); il fill di prova si scrive con `upsert_fills` invece che con un
|
||||
`INSERT` a mano che lasciava `verified` NULL, una forma che la produzione non produce; le
|
||||
asserzioni sul testo del report non sono più ancorate al padding; `movimenti_capitale` accetta le
|
||||
righe già lette, così il report non fa due SELECT sulla stessa tabella a un'ora del cron — due
|
||||
letture ai due lati di una scrittura descriverebbero due istanti diversi.
|
||||
|
||||
## 5. Il README era fermo a quindici giorni prima del reset
|
||||
|
||||
Chiesto un aggiornamento della documentazione, ho aperto il README e ho trovato un documento del
|
||||
**2026-06-04**. Il reset v2.0.0 è del **19 giugno**. Per 75 giorni la prima pagina del repository ha
|
||||
descritto la libreria pre-reset come se fosse il presente:
|
||||
|
||||
| cosa diceva | stato vero |
|
||||
|---|---|
|
||||
| famiglie FADE / HONEST / PAIRS / TSMOM / SHAPE, strategie MR01…SH01 | archiviate in `Old/`, artefatto di feed contaminato |
|
||||
| PORT06: Sharpe **7,84 / 10,06**, DD 2,60%, CAGR ~79% | numeri che `CLAUDE.md` §2 elenca sotto «non citare» |
|
||||
| paper trader su `strategies.yml`, portafogli in `portfolios.yml` | **file inesistenti** |
|
||||
| `scripts/waste/`, `scripts/portfolios/`, `src/live/multi_runner.py` | **inesistenti** |
|
||||
| esecuzione shadow su Deribit **testnet** | il testnet è *la causa del reset*; oggi si esegue su mainnet con soldi veri |
|
||||
|
||||
Riscritto contro lo stato vero: cosa gira adesso, i numeri nella lente di §2 (TWR +10,6%, non la
|
||||
crescita del conto), il metodo e i sei requisiti, la struttura verificata file per file, i gate con
|
||||
le loro date, l'obiettivo con la sua onestà. Ogni percorso citato è stato controllato: esiste.
|
||||
Da 423 righe a 152.
|
||||
|
||||
**La lezione che ho registrato non è «aggiornare il README».** È che un reset invalida anche i
|
||||
documenti che nessuno rilegge, e l'inventario di cosa cita numeri morti va fatto il giorno del
|
||||
reset — non 75 giorni dopo, per caso, mentre si fa altro.
|
||||
|
||||
## 6. Cosa NON ho fatto
|
||||
|
||||
- I loader `importlib` dei test sulla **ricerca** (moduli in `scripts/research/`) restano come
|
||||
sono: sono a livello di modulo, con radici diverse, e toccarli non ripara niente.
|
||||
- Il bound USDE nel classificatore: manca la storia, non il codice.
|
||||
|
||||
- Non ho cercato altri documenti fermi al pre-reset fuori da `README.md` e `docs/`: l'inventario
|
||||
completo (ogni file che cita un numero morto) resta da fare, ed è la vera coda di questa scoperta.
|
||||
@@ -703,9 +703,16 @@ e i vincoli di deploy (PRIIPs/UCITS/broker).
|
||||
movimento certo e moltiplica i segmenti (gli ambigui non spezzano: restano nel rendimento,
|
||||
dichiarati accanto — P12). Il report stampa TWR, segmenti con le date, movimenti elencati,
|
||||
trading al netto, e il delta $ grezzo solo etichettato «movimenti INCLUSI». Verifica M23: la
|
||||
macchina riproduce il +10,80% del diario 01/09 sui suoi quattro punti. **Limite ereditato:** un
|
||||
+10% di trading fra due letture orarie consecutive, a mercato fermo, sarebbe classificato
|
||||
movimento — e' il limite dichiarato del rilevatore live (D5), non uno nuovo.
|
||||
macchina riproduce il +10,80% del diario 01/09 sui suoi quattro punti. **Due limiti trovati in revisione (02/09):** l'intervallo che contiene un movimento certo esce INTERO dal rendimento (il suo P&L di mercato va in `certi`; errore massimo META' del movimento, per costruzione; il 25/08 ~$0,5) e a base di equity zero `twr` e `trading` sono entrambi None, perche' il salto 0→X del primo versamento e' invisibile al classificatore. I segmenti a lunghezza zero (movimento nel primo intervallo, o due consecutivi) non si stampano. **Seconda tornata di revisione, stesso giorno:** il classificatore era **cieco ~23 ore al giorno** — il
|
||||
feed 1h si ferma alle 00:00, `asof` dava la stessa barra alle due letture, mercato «fermo» = 0, e
|
||||
qualunque calo ≥10% del giorno sarebbe stato un movimento (un crash stampato come prelievo, dal
|
||||
25/08); con feed assente tutto era «ambiguo» e il report stampava `e1/e0-1` sotto l'etichetta TWR.
|
||||
Ora «mercato non misurabile» è uno stato: `ambiguo` con la ragione, `twr`/`trading` None con motivo.
|
||||
Il «limite +10% a mercato fermo» scritto la mattina era un artefatto della fixture piatta, cancellato.
|
||||
`pnl_giorno` chiama `rendimento_twr` (la frase «entrambi la chiamano» è vera solo da questa tornata),
|
||||
la pagina stampa il TWR, la testata Telegram qualifica il cumulato «di cui versati», e il bound di
|
||||
leva include la scala e il tetto di codice. Resta aperto (§5.15): il bound guarda solo BTC/ETH, e il
|
||||
31% dell'equity è USDE — un depeg pieno sarebbe scorporato come prelievo.
|
||||
**Le tre fonti si INCROCIANO e non si sovrascrivono:** log (ora vera) x jsonl (i fill) x venue
|
||||
(autorevole ma **TRONCA** — 1 trade su BTC, 0 su ETH). `reconcile()` riporta le divergenze e
|
||||
**non ripara niente da solo**: fra due fonti che non concordano, una riparazione silenziosa e'
|
||||
@@ -915,6 +922,56 @@ Sulle 4 finestre osservate 3 sono rientrate entro l'ora → il `:47` ne avrebbe
|
||||
**Se al prossimo martedì il `:47` becca comunque la manutenzione, la previsione è sbagliata** e lo
|
||||
slot non è quello descritto in `venue_probe.RELEASE_*`: rileggerlo prima di spostare ancora.
|
||||
|
||||
### La cadenza è ORARIA, e ora lo dicono tutte e tre le fonti (debito §5.7, chiuso il 2026-09-02)
|
||||
|
||||
Il docstring di `book_execute.py` prescriveva «ogni ~230 minuti» (la griglia di SKH01) mentre il cron
|
||||
gira ogni ora. Non era cosmetico: `r0823_sl_anchor.py` misura che l'accumulo di scatti del disaster-SL
|
||||
rotolante è funzione della **cadenza del cron** (1h: BTC 0 / ETH 1 in 7-8 anni · 4h: BTC 2 · 24h: ETH 5
|
||||
con 2 in 30 giorni), quindi chi avesse "corretto" il CRON verso il docstring avrebbe spostato il libro
|
||||
dalla riga 1h (rotolante: BTC 0 / ETH 1) alla riga 4h (BTC 2 contro 1 del pavimento; la peggiore misurata è 24h, ETH 5). La riparazione è il docstring stesso — cadenza ORARIA, la ragione (giro
|
||||
idempotente: girare più fitto della griglia non costa ordini, e compra latenza ≤1h per gli
|
||||
ingressi/uscite software di SKH01 e un **controllo** orario del rotolante — che si ri-ancora solo oltre la tolleranza di `ensure_disaster_sl`, mark +5,263%/−4,762% o taglia >10%, quindi lo stop siede fra −33,5% e −26,5% dal mark corrente: la prima stesura diceva «si ri-ancora ogni ora» ed era sbagliata, corretta in revisione lo stesso giorno) e il divieto scritto —
|
||||
più `tests/test_book_cadenza.py`, che **deriva** (P1) le tre dichiarazioni e le confronta: la riga
|
||||
citata nel docstring, quella dichiarata nell'intestazione di `cron_book.sh` (`47 * * * *`) e la
|
||||
crontab **installata** (`crontab -l`; se non è leggibile il test è SALTATO, non verde — P5). Il
|
||||
parser accetta solo espressioni regolari e rifiuta il resto. Vincolo del minuto tondo verificato
|
||||
(≠ `:00`). La regola generale è la stessa del `:47`: **una cadenza si difende nella sua ragione, e
|
||||
un commento che dichiara una cadenza diversa da quella che gira si paga quando qualcuno lo "corregge"
|
||||
nel verso sbagliato.**
|
||||
|
||||
### Il README ha pubblicato per 75 giorni la libreria che il progetto aveva dichiarato falsa
|
||||
|
||||
Ultimo commit del README: **2026-06-04**. Reset v2.0.0: **2026-06-19**. In mezzo, quindici giorni; dopo,
|
||||
75 in cui la prima pagina del repository descriveva FADE/HONEST/PAIRS/TSMOM/SHAPE, i portafogli PORT01-06,
|
||||
il paper trader su `strategies.yml` e l'esecuzione shadow su testnet — con Sharpe 7,84/10,06 e CAGR ~79%
|
||||
presentati come risultati correnti. Metà dei file citati non esisteva più (`strategies.yml`,
|
||||
`portfolios.yml`, `scripts/waste/`, `scripts/portfolios/`, `src/live/multi_runner.py`); i numeri erano
|
||||
esattamente quelli che `CLAUDE.md` §2 elenca sotto «non citare», e provenivano dalla libreria che il
|
||||
reset aveva dichiarato artefatto di un feed contaminato.
|
||||
|
||||
Nessun danno sui soldi — il README non decide niente. Il danno è di **citazione**, ed è la forma peggiore:
|
||||
un lettore esterno (o un agente nuovo) parte da lì. **La lezione non è "tenere aggiornato il README"**,
|
||||
che nessuno fa più di quanto già non faccia: è che **un reset invalida anche i documenti che nessuno
|
||||
rilegge**, e l'inventario di cosa cita numeri morti va fatto il giorno del reset, quando si sa che sono
|
||||
morti — non 75 giorni dopo, quando qualcuno ci inciampa per caso. Riscritto il 2026-09-02 contro lo stato
|
||||
vero, con ogni riferimento a file verificato e i numeri nella lente di §2.
|
||||
|
||||
### Un flag sconosciuto non è l'azione di default (2026-09-02)
|
||||
|
||||
Nessuno dei 18 script di `scripts/live/` usava argparse: leggevano `sys.argv` con `in` e `index`,
|
||||
quindi un flag sbagliato non era un errore — era il ramo `else`. Il costo si è misurato lo stesso
|
||||
giorno, dentro una revisione: `trades_db.py --help` è caduto in `sync()` e ha riscritto
|
||||
`meta.ultimo_sync`. Sui due script che scrivono davvero sarebbe stato peggio: la pagina di giornale
|
||||
e una riga di DB, oppure una chiamata al modello più un Telegram. **La riparazione non è argparse** —
|
||||
cambierebbe messaggi d'errore, codici d'uscita e `--help` di script che il cron già chiama, cioè
|
||||
romperebbe ciò che gira per riparare ciò che non gira mai. È una funzione di venti righe
|
||||
(`src/live/cli.valida`) che rifiuta ciò che non è dichiarato ed esce **prima di qualunque effetto**:
|
||||
`--help` → 0, flag ignoto → 2 con l'elenco dei previsti (P4). Il test deriva l'elenco degli script
|
||||
dalla cartella (P1: un file nuovo senza guardia fallisce, non passa inosservato), controlla che
|
||||
`valida` sia la prima istruzione di `__main__` (uscire dopo `connect()` sarebbe uscire dopo
|
||||
l'effetto) e — l'altra metà del contratto, P15/P16 — che **i flag che il cron usa davvero restino
|
||||
accettati**: una guardia che ferma il libro di bordo la notte stessa sarebbe peggio del difetto.
|
||||
|
||||
### Cosa resta scoperto
|
||||
|
||||
1. **La sonda dice di chi è il guasto, non lo aggira.** Col gateway giù il libro continua ad
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# MINUTO :25, e la scelta NON e' arbitraria:
|
||||
# :00 cerbero-bite sparava la sua spazzata full-chain (~44 chiamate/s) e saturava il rate limit
|
||||
# Deribit per-IP — 12.186 risposte 429 in 26 ore, il 96% nel minuto tondo.
|
||||
# :07 cron_book.sh (esecuzione del book live). Il feed 5m di SKH01 passa da li': e' la cosa che
|
||||
# :47 cron_book.sh (esecuzione del book live; era :07 fino al 25/08). Il feed 5m di SKH01 passa da li': e' la cosa che
|
||||
# NON deve trovare l'IP occupato.
|
||||
# :25 nessun altro job. Il giro dura ~3 minuti a 4 chiamate/s, quindi finisce ben prima del :30.
|
||||
#
|
||||
|
||||
@@ -115,7 +115,21 @@ def scrivi(con, g: date, modello: str, secco: bool = False, verbose: bool = True
|
||||
return stato
|
||||
|
||||
|
||||
USO = """uso: analista.py [--giorno AAAA-MM-GG] [--modello NOME] [--secco] [--no-telegram] [--quiet]
|
||||
|
||||
(nessun flag) scrive l'analisi di OGGI nel campo `analisi` e manda la notifica
|
||||
--giorno G analizza il giorno G
|
||||
--modello M modello da usare (default: src.live.analista.MODELLO_DEFAULT)
|
||||
--secco stampa e NON salva, niente Telegram
|
||||
--no-telegram salva ma non manda la notifica
|
||||
--quiet non stampa
|
||||
|
||||
⚠️ SPENDE una chiamata al modello e MANDA un messaggio Telegram, salvo --secco/--no-telegram."""
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.live.cli import valida
|
||||
valida("analista.py", USO, flag=("--secco", "--no-telegram", "--quiet"),
|
||||
con_valore=("--giorno", "--modello"))
|
||||
con = T.connect()
|
||||
g = date.fromisoformat(_arg("--giorno", datetime.now(timezone.utc).date().isoformat()))
|
||||
st = scrivi(con, g, modello=_arg("--modello", A.MODELLO_DEFAULT),
|
||||
|
||||
@@ -85,6 +85,17 @@ def campiona(c: DeribitRead) -> dict:
|
||||
rec["n_posizioni"] = sum(1 for p in pos if abs(float(p.get("size") or 0)) > 1)
|
||||
except Exception:
|
||||
rec["nozionale_lordo"], rec["n_posizioni"] = None, None
|
||||
# INDICE usde_usdc, ORARIO (aggiunto 2026-09-02, debito §5.15). `usde_watch` lo registra una
|
||||
# volta al giorno (12:35Z): troppo rado per il classificatore dei movimenti di capitale, che
|
||||
# confronta due letture di equity a un'ora di distanza e oggi guarda solo BTC/ETH — con il
|
||||
# 31% dell'equity in USDE, un depeg sarebbe scorporato come un PRELIEVO. Questa e' la serie
|
||||
# che manca; il cablaggio nel bound viene dopo, quando ci sara' storia. None se non leggibile
|
||||
# (P5: "non vedo" non e' "il peg tiene").
|
||||
try:
|
||||
from src.live.usde import prezzo_indice
|
||||
rec["usde_usdc"] = prezzo_indice()
|
||||
except Exception as e:
|
||||
rec["usde_usdc"], rec["usde_usdc_errore"] = None, f"{type(e).__name__}"
|
||||
return rec
|
||||
|
||||
|
||||
|
||||
@@ -10,9 +10,24 @@ DOPPIO GATE DI SICUREZZA (entrambi necessari per inviare ordini reali):
|
||||
Senza entrambi e' un DRY-RUN (stampa il piano, NON invia). Reconciliation dopo ogni ordine; log in
|
||||
data/live/book_executions.jsonl.
|
||||
|
||||
⚠️ CADENZA: SKH01 decide su griglia 230m -> questo script va lanciato ogni ~230 minuti con la feed
|
||||
fresca all'ultima barra chiusa (NON il cron giornaliero, che mancherebbe gli ingressi). Gli exit di
|
||||
SKH sono SOFTWARE (latenza fino a fine barra 230m); solo il disaster-SL (-30%) e' on-book.
|
||||
⚠️ CADENZA: ORARIA — `47 * * * *` in `scripts/cron_book.sh` (il minuto e' spiegato li'). SKH01
|
||||
decide su griglia 230m, ma il giro e' IDEMPOTENTE (riconcilia al target netto corrente; sotto
|
||||
`min_order_usd` $5 -> HOLD). Girare piu' fitto della griglia costa solo MICRO-ORDINI di ri-taglia:
|
||||
il target e' ricalcolato ogni ora sull'equity marcata, e 22 dei 48 ordini live sono |delta|<=$10
|
||||
a segnale invariato — spiccioli oggi, ma il deadband e' in valuta ASSOLUTA (regola C2: a capitale
|
||||
maggiore un movimento dello 0,15% lo supera ogni ora). Compra due cose misurate: (1) gli
|
||||
ingressi/uscite SOFTWARE di SKH01 arrivano con latenza <=1h — CON LA FEED 5m FRESCA: se e' stantia
|
||||
`livefeed.fresh_5m` ripiega in silenzio sul feed giornaliero (latenza ~1 giorno) e
|
||||
`skh_feed_max_age_min` 30 ALLERTA, non blocca; (2) il disaster-SL rotolante (-30%, l'unico on-book)
|
||||
viene CONTROLLATO ogni ora e ri-ancorato solo oltre la tolleranza di `ensure_disaster_sl` (stop a
|
||||
>5% da quello piazzato, cioe' mark +5,263%/-4,762%, o taglia >10%) — con questa isteresi
|
||||
(`r0823_sl_anchor.py`): a cadenza 1h in 7-8 anni BTC 0 scatti / ETH 1; a 4h BTC 2; a 24h ETH 5,
|
||||
con 2 in 30 giorni. Lo stop siede quindi fra -33,5% e -26,5% dal mark corrente (0,665-0,735 x mark),
|
||||
non a -30% dall'ultima ora: e' rotolante, non un massimo di perdita (CLAUDE.md §1).
|
||||
🚨 NON "correggere" la cadenza a ~230 minuti: fino al 2026-09-02 questo docstring lo prescriveva,
|
||||
ed era il docstring a essere sbagliato, non il cron (debito §5.7). `tests/test_book_cadenza.py`
|
||||
tiene d'accordo docstring, `cron_book.sh` e crontab installata. NON il cron giornaliero: le entrate
|
||||
di SKH01 le mancherebbe.
|
||||
|
||||
uv run python scripts/live/book_execute.py # DRY-RUN (piano, nessun ordine)
|
||||
uv run python scripts/live/book_execute.py --execute # esegue SOLO se execution_enabled=true
|
||||
@@ -340,5 +355,14 @@ def main():
|
||||
raise
|
||||
|
||||
|
||||
USO = """uso: book_execute.py [--execute]
|
||||
|
||||
uv run python scripts/live/book_execute.py # DRY-RUN (piano, nessun ordine)
|
||||
uv run python scripts/live/book_execute.py --execute # esegue SOLO se execution_enabled=true
|
||||
|
||||
Dettaglio nel docstring in testa al file."""
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.live.cli import valida
|
||||
valida("book_execute.py", USO, flag=("--execute",), con_valore=())
|
||||
main()
|
||||
|
||||
@@ -93,5 +93,13 @@ def main() -> None:
|
||||
print(" regime QUIET: carry non raccoglibile (coerente con luglio 2026).")
|
||||
|
||||
|
||||
USO = """uso: cc01_regime_watch.py
|
||||
|
||||
(nessun flag) sorveglia il regime di CC01 e stampa lo stato.
|
||||
|
||||
Dettaglio nel docstring in testa al file."""
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.live.cli import valida
|
||||
valida("cc01_regime_watch.py", USO)
|
||||
main()
|
||||
|
||||
@@ -144,5 +144,15 @@ def main() -> int:
|
||||
return 0
|
||||
|
||||
|
||||
USO = """uso: edge_watch.py [--quiet]
|
||||
|
||||
edge_watch.py — sorveglianza dell'EDGE del book live. Riporta e allerta; non tocca nulla.
|
||||
uv run python scripts/live/edge_watch.py # report
|
||||
uv run python scripts/live/edge_watch.py --quiet # solo se qualcosa scatta (cron)
|
||||
|
||||
Dettaglio nel docstring in testa al file."""
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.live.cli import valida
|
||||
valida("edge_watch.py", USO, flag=("--quiet",), con_valore=())
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -252,5 +252,15 @@ def main() -> int:
|
||||
return 2 if r["level"] == "AZIONE" else 0
|
||||
|
||||
|
||||
USO = """uso: fee_watch.py [--quiet]
|
||||
|
||||
fee_watch.py — sorveglia lo schema fee di Deribit e applica la regola DECISA IN ANTICIPO.
|
||||
uv run python scripts/live/fee_watch.py # report
|
||||
uv run python scripts/live/fee_watch.py --quiet # stampa/allerta solo se qualcosa cambia
|
||||
|
||||
Dettaglio nel docstring in testa al file."""
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.live.cli import valida
|
||||
valida("fee_watch.py", USO, flag=("--quiet",), con_valore=())
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -41,7 +41,19 @@ def una(con, g: date, nota: str | None = None, verbose: bool = True) -> Path:
|
||||
return f
|
||||
|
||||
|
||||
USO = """uso: journal.py [--giorno AAAA-MM-GG] [--backfill N] [--nota "testo"] [--quiet]
|
||||
|
||||
(nessun flag) scrive la voce di OGGI (pagina marcata PARZIALE: il giorno non e' chiuso)
|
||||
--giorno G scrive la voce del giorno G
|
||||
--backfill N ricostruisce gli ultimi N giorni, dal piu' vecchio
|
||||
--nota "..." annota il giorno (campo dell'operatore, mai riscritto da un ricalcolo)
|
||||
--quiet non stampa la riga di riepilogo
|
||||
|
||||
⚠️ SCRIVE: una riga nel DB e una pagina in docs/journal/. Sola lettura su feed, log e venue."""
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.live.cli import valida
|
||||
valida("journal.py", USO, flag=("--quiet",), con_valore=("--giorno", "--backfill", "--nota"))
|
||||
# sincronizzare PRIMA: altrimenti la sezione 'Libro' (letta dal log) e la sezione 'P&L'
|
||||
# (letta dal DB) descrivono due istanti diversi e la pagina si contraddice da sola.
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "live"))
|
||||
|
||||
@@ -137,5 +137,14 @@ def main():
|
||||
raise
|
||||
|
||||
|
||||
USO = """uso: live_execute.py [--execute]
|
||||
|
||||
uv run python scripts/live/live_execute.py # DRY-RUN (piano, nessun ordine)
|
||||
uv run python scripts/live/live_execute.py --execute # esegue SOLO se execution_enabled=true
|
||||
|
||||
Dettaglio nel docstring in testa al file."""
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.live.cli import valida
|
||||
valida("live_execute.py", USO, flag=("--execute",), con_valore=())
|
||||
main()
|
||||
|
||||
@@ -74,5 +74,15 @@ def main():
|
||||
(f"{len(r['orders'])} ordine/i costruito/i sopra." if r["orders"] else "Target flat: 0 ordini."))
|
||||
|
||||
|
||||
USO = """uso: live_trend.py [--equity V] [--no-net]
|
||||
|
||||
uv run python scripts/live/live_trend.py # shadow su mainnet reale
|
||||
uv run python scripts/live/live_trend.py --equity 2000 # forza la base di sizing
|
||||
uv run python scripts/live/live_trend.py --no-net # offline: solo matematica + parita'
|
||||
|
||||
Dettaglio nel docstring in testa al file."""
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.live.cli import valida
|
||||
valida("live_trend.py", USO, flag=("--no-net",), con_valore=("--equity",))
|
||||
main()
|
||||
|
||||
@@ -88,5 +88,14 @@ def main():
|
||||
print(" Validato: invio ordine reale, fill, fee reali, reconciliation, ritorno a flat.")
|
||||
|
||||
|
||||
USO = """uso: microtest.py [--live]
|
||||
|
||||
uv run python scripts/live/microtest.py # DRY-RUN: nessun ordine inviato
|
||||
uv run python scripts/live/microtest.py --live # invia il round-trip REALE
|
||||
|
||||
Dettaglio nel docstring in testa al file."""
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.live.cli import valida
|
||||
valida("microtest.py", USO, flag=("--live",), con_valore=())
|
||||
main()
|
||||
|
||||
@@ -55,5 +55,15 @@ def main() -> int:
|
||||
return 0
|
||||
|
||||
|
||||
USO = """uso: monitor_health.py [--quiet]
|
||||
|
||||
monitor_health.py — i forward-monitor stanno registrando? Riporta e allerta, non tocca nulla.
|
||||
uv run python scripts/live/monitor_health.py # report
|
||||
uv run python scripts/live/monitor_health.py --quiet # stampa/allerta solo se scatta
|
||||
|
||||
Dettaglio nel docstring in testa al file."""
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.live.cli import valida
|
||||
valida("monitor_health.py", USO, flag=("--quiet",), con_valore=())
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -118,5 +118,13 @@ def main():
|
||||
print(f" GTAA (IB, asof {asof}): " + ", ".join(f"{k} {v:.0%}" for k, v in gw.items() if v) + f" | cash {cash:.0%}")
|
||||
|
||||
|
||||
USO = """uso: paper_combo.py [--reset] [--status]
|
||||
|
||||
uv run python scripts/live/paper_combo.py [--status|--reset]
|
||||
|
||||
Dettaglio nel docstring in testa al file."""
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.live.cli import valida
|
||||
valida("paper_combo.py", USO, flag=("--status", "--reset"), con_valore=())
|
||||
main()
|
||||
|
||||
@@ -87,5 +87,15 @@ def main():
|
||||
print(f" posizioni correnti: {pf.current_positions()}")
|
||||
|
||||
|
||||
USO = """uso: paper_portfolio.py [--reset] [--status]
|
||||
|
||||
uv run python scripts/live/paper_portfolio.py # avanza (init al 1o run)
|
||||
uv run python scripts/live/paper_portfolio.py --status # solo stato
|
||||
uv run python scripts/live/paper_portfolio.py --reset # azzera (riparte da ora)
|
||||
|
||||
Dettaglio nel docstring in testa al file."""
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.live.cli import valida
|
||||
valida("paper_portfolio.py", USO, flag=("--status", "--reset"), con_valore=())
|
||||
main()
|
||||
|
||||
@@ -224,5 +224,15 @@ def main() -> None:
|
||||
print("\n fatto. I file *.%s.* sono l'evidenza del difetto e restano nel backup." % SUFFISSO)
|
||||
|
||||
|
||||
USO = """uso: paper_regen.py [--solo V]
|
||||
|
||||
paper_regen.py — RIGENERA le serie dei forward-monitor rotti da `advance()` (§5.1).
|
||||
uv run python scripts/live/paper_regen.py # tutti e 4
|
||||
uv run python scripts/live/paper_regen.py --solo paper_statarb
|
||||
|
||||
Dettaglio nel docstring in testa al file."""
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.live.cli import valida
|
||||
valida("paper_regen.py", USO, flag=(), con_valore=("--solo",))
|
||||
main()
|
||||
|
||||
@@ -58,5 +58,15 @@ def main() -> None:
|
||||
print(f"\n invio: {rep['invio']}")
|
||||
|
||||
|
||||
USO = """uso: scale_watch.py [--quiet] [--secco]
|
||||
|
||||
uv run python scripts/live/scale_watch.py # osserva e stampa, invia se ALLARME
|
||||
uv run python scripts/live/scale_watch.py --secco # osserva e stampa, NON invia nulla
|
||||
uv run python scripts/live/scale_watch.py --quiet # una riga sola (per il cron)
|
||||
|
||||
Dettaglio nel docstring in testa al file."""
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.live.cli import valida
|
||||
valida("scale_watch.py", USO, flag=("--quiet", "--secco"), con_valore=())
|
||||
main()
|
||||
|
||||
@@ -146,5 +146,14 @@ def main() -> None:
|
||||
print("inviato" if send(txt) else "NON inviato (config Telegram assente o rete KO)")
|
||||
|
||||
|
||||
USO = """uso: telegram_daily.py [--dry-run]
|
||||
|
||||
uv run python scripts/live/telegram_daily.py # calcola e invia
|
||||
uv run python scripts/live/telegram_daily.py --dry-run # stampa e basta, non invia
|
||||
|
||||
Dettaglio nel docstring in testa al file."""
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.live.cli import valida
|
||||
valida("telegram_daily.py", USO, flag=("--dry-run",), con_valore=())
|
||||
main()
|
||||
|
||||
@@ -91,8 +91,15 @@ def report() -> None:
|
||||
print(f" movimenti capitale : {r['certi']:+,.2f} certi | {r['ambigui']:+,.2f} ambiguo/i"
|
||||
f" (restano nel P&L, dichiarati)")
|
||||
for ev in r["eventi"]:
|
||||
print(f" {ev['ts_dopo'][:16]} {ev['delta']:+,.2f} [{ev['classe']}]")
|
||||
print(f" trading da arming : {r['trading']:+,.2f} (equity al netto dei movimenti certi)")
|
||||
mk = ev.get("mercato_max_pct")
|
||||
dettaglio = (ev.get("mercato_nota") or "mercato non misurabile") if mk is None \
|
||||
else f"mercato max {100 * mk:.2f}% a leva piena, salto {100 * ev['pct']:+.1f}%"
|
||||
print(f" {ev['ts_dopo'][:16]} {ev['delta']:+,.2f} [{ev['classe']}: {dettaglio}]")
|
||||
if r["trading"] is None:
|
||||
print(f" trading da arming : n/d ({r['motivo']})")
|
||||
else:
|
||||
print(f" trading da arming : {r['trading']:+,.2f} (equity al netto dei movimenti "
|
||||
f"certi; l'ora del movimento esce intera, vedi journal.rendimento_twr)")
|
||||
if r["twr"] is None:
|
||||
print(f" TWR : n/d ({r['motivo']})")
|
||||
else:
|
||||
@@ -118,8 +125,16 @@ def report() -> None:
|
||||
con.close()
|
||||
|
||||
|
||||
USO = """uso: trades_db.py [--sync] [--report] [--reconcile] [--quiet]
|
||||
|
||||
--sync (default) legge log e jsonl e aggiorna data/live/trades.db — idempotente
|
||||
--report stato del libro di bordo + P&L (TWR, non e1/e0)
|
||||
--reconcile incrocio delle tre fonti sui fill (log x jsonl x venue)
|
||||
--quiet con --sync: non stampa il riepilogo"""
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = sys.argv[1:]
|
||||
from src.live.cli import valida
|
||||
args = valida("trades_db.py", USO, flag=("--sync", "--report", "--reconcile", "--quiet"))
|
||||
if "--reconcile" in args:
|
||||
reconcile()
|
||||
elif "--report" in args:
|
||||
|
||||
@@ -330,5 +330,15 @@ def main() -> int:
|
||||
return 0
|
||||
|
||||
|
||||
USO = """uso: usde_watch.py [--quiet]
|
||||
|
||||
usde_watch.py — sorveglianza giornaliera del collaterale USDE: reward, depeg, quota. SOLA LETTURA.
|
||||
uv run python scripts/live/usde_watch.py # report completo
|
||||
uv run python scripts/live/usde_watch.py --quiet # stampa/allerta solo transizioni
|
||||
|
||||
Dettaglio nel docstring in testa al file."""
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.live.cli import valida
|
||||
valida("usde_watch.py", USO, flag=("--quiet",), con_valore=())
|
||||
sys.exit(main())
|
||||
|
||||
@@ -98,5 +98,15 @@ def main() -> int:
|
||||
return 0
|
||||
|
||||
|
||||
USO = """uso: venue_watch.py [--quiet]
|
||||
|
||||
venue_watch.py — runner ORARIO del tripwire di venue. Allerta su Telegram, NON blocca nulla.
|
||||
uv run python scripts/live/venue_watch.py # un giro, stampa il report
|
||||
uv run python scripts/live/venue_watch.py --quiet # solo in caso di allarme (per il cron)
|
||||
|
||||
Dettaglio nel docstring in testa al file."""
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.live.cli import valida
|
||||
valida("venue_watch.py", USO, flag=("--quiet",), con_valore=())
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -17,7 +17,7 @@ SOLA LETTURA. Questo script:
|
||||
- non importa nulla che invii ordini se non per ISPEZIONARLO (la sottoclasse di §1 ha `_post` che
|
||||
SOLLEVA: se il codice di produzione toccasse la rete, il test fallirebbe rumorosamente);
|
||||
- tocca il venue solo con `--venue`, solo `get_open_orders` (lettura), 2 richieste, e si rifiuta
|
||||
di partire nei minuti :05-:10 (cron_book) e :24-:30 (cron_chain).
|
||||
di partire nei minuti :45-:50 (cron_book, al :47 dal 25/08) e :24-:30 (cron_chain).
|
||||
|
||||
uv run python scripts/research/r0823_sl_anchor.py # attesa + log + simulazione
|
||||
uv run python scripts/research/r0823_sl_anchor.py --venue # + riscontro sul conto (2 letture)
|
||||
@@ -283,8 +283,8 @@ def riscontro_venue(runs_info: dict):
|
||||
diritte di Deribit appartengono a un ALTRO progetto: usarle sarebbe inventare un percorso, e la
|
||||
domanda non ne ha bisogno. Si usa quindi cio' che il percorso sanzionato offre: `get_open_orders`.""")
|
||||
m = datetime.now(timezone.utc).minute
|
||||
if 5 <= m <= 10 or 24 <= m <= 30:
|
||||
print(f"\n ⏸ minuto :{m:02d} — finestra di cron_book (:07) / cron_chain (:25). Non interrogo.")
|
||||
if 45 <= m <= 50 or 24 <= m <= 30: # cron_book al :47 dal 2026-08-25 (era :07), cron_chain :25
|
||||
print(f"\n ⏸ minuto :{m:02d} — finestra di cron_book (:47) / cron_chain (:25). Non interrogo.")
|
||||
return
|
||||
from src.live.deribit import DeribitRead
|
||||
r = DeribitRead()
|
||||
@@ -395,10 +395,11 @@ def conseguenza():
|
||||
alta dopo una salita, lo stop siede vicino sotto il mercato, il crollo lo prende, si rientra e
|
||||
la gamba successiva lo riprende. **Il rischio di accumulo e' funzione della CADENZA DEL CRON,
|
||||
non di k**: il gradino di leva non lo crea e non lo amplifica.
|
||||
· ⚠️ E qui c'e' un aggancio operativo: il docstring di `scripts/live/book_execute.py` prescrive
|
||||
«ogni ~230 minuti» (la griglia di SKH01) mentre il cron gira **ogni ora** (misurato sopra: 1443
|
||||
giri in 1442 ore). Chi "correggesse" la cadenza verso il docstring sposterebbe il libro dalla
|
||||
riga 1h alla riga 4h — quella in cui BTC raddoppia gli scatti.
|
||||
· ⚠️ Aggancio operativo (RIPARATO il 2026-09-02, `tests/test_book_cadenza.py`): fino a quel
|
||||
giorno il docstring di `scripts/live/book_execute.py` prescriveva «ogni ~230 minuti» (la
|
||||
griglia di SKH01) mentre il cron gira **ogni ora** (misurato sopra: 1443 giri in 1442 ore).
|
||||
Chi avesse "corretto" il CRON verso il docstring avrebbe spostato il libro dalla riga 1h
|
||||
(rotolante: 0 scatti) alla riga 4h (2 scatti del rotolante contro 1 del pavimento).
|
||||
|
||||
(*) ⚠️ CAVEAT sulla colonna, e sul suo artefatto: in questa lente la posizione e' SEMPRE aperta
|
||||
dal 2018, quindi in modo `roll` l'"ingresso" e' un prezzo di anni prima e il -48%/-61% e' il
|
||||
@@ -501,9 +502,9 @@ def costo(out: dict):
|
||||
|
||||
⚠️ E c'e' un accoppiamento nuovo da registrare: l'accumulo di episodi e' funzione della CADENZA
|
||||
DEL CRON (1h: no · 4h: BTC 2 · 24h: ETH 5 con 2 in 30 giorni). Oggi la cadenza e' misurata sana,
|
||||
ma nessun sorvegliante la controlla contro questa conseguenza, e il docstring di `book_execute`
|
||||
ne prescrive una (~230 min) che cade nella riga peggiore. Non e' un'azione richiesta qui: e' il
|
||||
parametro da rileggere prima del gradino, insieme a G2.""")
|
||||
e dal 2026-09-02 `tests/test_book_cadenza.py` tiene d'accordo docstring, cron_book.sh e crontab
|
||||
(il docstring ne prescriveva una, ~230 min, nella riga 4h). Resta il parametro da rileggere prima
|
||||
del gradino, insieme a G2.""")
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -205,7 +205,10 @@ def testata(voce: dict) -> str:
|
||||
r = [f"<b>📓 Giornale — {_b(voce['giorno'])}</b>"]
|
||||
r.append(" equity " + (f"<b>${eq:,.2f}</b>" if eq is not None else "n/d")
|
||||
+ ("" if dq is None else f" ({dq:+.2f} oggi)")
|
||||
+ ("" if cum is None else f" · dall'arming {cum:+.2f}"))
|
||||
+ ("" if cum is None else f" · dall'arming {cum:+.2f}"
|
||||
+ (f" (di cui {p['mov_arming']:+.2f} versati -> trading "
|
||||
f"{p['trading_da_arming']:+.2f})"
|
||||
if p.get("mov_arming") and p.get("trading_da_arming") is not None else "")))
|
||||
if "errore" not in lb and lb.get("asset"):
|
||||
pos = " · ".join(f"{a} ${v['posizione']:+,.0f}" for a, v in sorted(lb["asset"].items()))
|
||||
r.append(f" {pos} · leva {lb.get('leva_lorda') or 0:.2f}x")
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Argomenti da riga di comando degli script operativi: un flag ignoto NON e' l'azione di default.
|
||||
|
||||
PERCHE' ESISTE (2026-09-02). Nessuno degli script di `scripts/live/` usava argparse: leggevano
|
||||
`sys.argv` con `in` e `index`, quindi un flag sbagliato non era un errore — era il ramo `else`.
|
||||
Costo misurato lo stesso giorno, durante una revisione: `trades_db.py --help` non ha stampato
|
||||
l'uso, e' caduto in `sync()` e ha riscritto `meta.ultimo_sync`. Gli altri due sono peggio:
|
||||
`journal.py --help` avrebbe scritto la pagina di oggi e una riga nel DB, `analista.py --help`
|
||||
avrebbe speso una chiamata al modello e mandato un messaggio Telegram.
|
||||
|
||||
TRE REGOLE, e nient'altro:
|
||||
1. **Nessun argparse.** Gli script hanno flag posizionali-per-nome (`--report`, `--secco`) e
|
||||
flag con valore (`--giorno 2026-08-21`); argparse cambierebbe i messaggi d'errore, il
|
||||
codice d'uscita e il comportamento di `--help` di 18 script che il cron gia' chiama. Qui
|
||||
serve il contrario: **lasciare intatto cio' che gira** e rifiutare cio' che non e' previsto.
|
||||
2. **Il cron non deve cambiare.** I flag dichiarati sono quelli che gli script gia' accettano:
|
||||
`valida()` non ne aggiunge e non ne toglie (P15: una regola si prova contro il codice che
|
||||
dovrebbe eseguirla). L'unica novita' e' `--help`, che prima non esisteva.
|
||||
3. **Si esce PRIMA di qualunque effetto.** `valida()` va chiamata come prima istruzione del
|
||||
blocco `__main__`, prima di `connect()`, prima del sync, prima della rete.
|
||||
|
||||
Codici d'uscita: 0 per `--help` (l'utente ha chiesto l'uso), 2 per un flag ignoto (errore d'uso,
|
||||
distinto dal 1 con cui gli script segnalano un esito negativo del loro lavoro).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
class UsoSbagliato(Exception):
|
||||
"""Flag non previsto da questo script. Portato con l'elenco di cio' che e' previsto (P4)."""
|
||||
|
||||
|
||||
def valida(nome: str, uso: str, *, flag: tuple[str, ...] = (), con_valore: tuple[str, ...] = (),
|
||||
argv: list[str] | None = None, esci: bool = True) -> list[str]:
|
||||
"""Controlla `argv` contro i flag dichiarati. Ritorna gli argomenti; solleva o esce se ignoti.
|
||||
|
||||
`flag` sono i booleani (`--quiet`), `con_valore` quelli che consumano l'argomento seguente
|
||||
(`--giorno 2026-08-21`). `--help`/`-h` stampano `uso` ed escono 0. Con `esci=False` non
|
||||
tocca `sys.exit` e solleva `UsoSbagliato`: e' la forma che i test usano.
|
||||
"""
|
||||
args = list(sys.argv[1:] if argv is None else argv)
|
||||
if "--help" in args or "-h" in args:
|
||||
if not esci:
|
||||
return args
|
||||
print(uso.strip())
|
||||
raise SystemExit(0)
|
||||
noti, ignoti, attesa_valore = set(flag) | set(con_valore), [], False
|
||||
for a in args:
|
||||
if attesa_valore: # e' il valore di un flag noto, non un flag
|
||||
attesa_valore = False
|
||||
continue
|
||||
if not a.startswith("-"):
|
||||
ignoti.append(a); continue # posizionale: nessuno di questi script ne ha
|
||||
if a not in noti:
|
||||
ignoti.append(a); continue
|
||||
attesa_valore = a in con_valore
|
||||
if attesa_valore:
|
||||
ignoti.append(f"{args[-1]} (manca il valore)")
|
||||
if ignoti:
|
||||
msg = (f"{nome}: argomento non previsto: {', '.join(ignoti)}\n"
|
||||
f" previsti: {' '.join(sorted(noti)) or '(nessuno)'}\n\n{uso.strip()}")
|
||||
if not esci:
|
||||
raise UsoSbagliato(msg)
|
||||
print(msg, file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
return args
|
||||
+76
-21
@@ -118,7 +118,7 @@ def _px_1h(asset: str) -> pd.Series | None:
|
||||
return None
|
||||
|
||||
|
||||
def movimenti_capitale(con, fino_ts: str) -> dict:
|
||||
def movimenti_capitale(con, fino_ts: str, rows=None) -> dict:
|
||||
"""Salti di equity fra letture consecutive: versamenti e prelievi, da scorporare dal P&L.
|
||||
|
||||
PERCHE' ESISTE. Il P&L di giornale e' un delta di equity, quindi un versamento ci finisce
|
||||
@@ -139,6 +139,9 @@ def movimenti_capitale(con, fino_ts: str) -> dict:
|
||||
non si distingue dal mercato e RESTA nel P&L. E' lo stesso limite del rilevatore live.
|
||||
"""
|
||||
from src.live import book as _book # lazy: prende la soglia viva, anche monkeypatchata
|
||||
# `rows` si puo' passare gia' letto: due SELECT sulla stessa tabella a un'ora del cron
|
||||
# possono cadere ai due lati di una scrittura e descrivere due istanti diversi.
|
||||
if rows is None:
|
||||
rows = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc <= ? ORDER BY ts_utc",
|
||||
(fino_ts,)).fetchall()
|
||||
soglia = float(_book.EQUITY_JUMP_ALERT)
|
||||
@@ -148,30 +151,48 @@ def movimenti_capitale(con, fino_ts: str) -> dict:
|
||||
return dict(eventi=[], certi=0.0, ambigui=0.0, soglia=soglia)
|
||||
try:
|
||||
cfg = json.loads((PROJECT_ROOT / "config" / "live.json").read_text())
|
||||
leva_tetto = float(cfg["max_notional_per_asset_frac"]) * len(ASSETS)
|
||||
# la leva lorda massima che il libro puo' tenere: frac x n_asset x scala, mai oltre il
|
||||
# tetto di CODICE (P1: derivata da `book`, non ridichiarata — la chiave di scala del
|
||||
# 01/09 entra qui da sola)
|
||||
scala = float(cfg.get("book_scale_k") or 1.0)
|
||||
leva_tetto = min(float(cfg["max_notional_per_asset_frac"]) * len(ASSETS) * scala,
|
||||
float(_book.LEVA_LORDA_MAX))
|
||||
except Exception:
|
||||
leva_tetto = 1.0 # il tetto di progetto: bound conservativo, usato solo qui
|
||||
leva_tetto = float(_book.LEVA_LORDA_MAX) # config illeggibile: il tetto di codice
|
||||
px = {a: _px_1h(a) for a in ASSETS}
|
||||
eventi = []
|
||||
for prev, cur in candidati:
|
||||
t0, t1 = pd.Timestamp(prev["ts_utc"]), pd.Timestamp(cur["ts_utc"])
|
||||
delta = float(cur["equity"] - prev["equity"])
|
||||
pct = delta / prev["equity"]
|
||||
mosse = []
|
||||
# Il mercato e' MISURABILE solo se il feed copre entrambe le letture. Il feed 1h
|
||||
# certificato si ferma alle 00:00 (rebuild alle 00:30): per il resto del giorno
|
||||
# `asof` restituirebbe la STESSA barra per t0 e t1, max_mkt 0, e qualunque calo >=10%
|
||||
# diventerebbe un "movimento" — un crash stampato come prelievo (revisione 02/09).
|
||||
# Non misurabile => `ambiguo` con la ragione (P5: "non vedo" non e' "mercato fermo").
|
||||
mosse, non_mis = [], []
|
||||
for a in ASSETS:
|
||||
s = px.get(a)
|
||||
if s is None or s.empty:
|
||||
continue
|
||||
non_mis.append(f"{a}: feed 1h non leggibile"); continue
|
||||
fine = s.index[-1] + pd.Timedelta(hours=1) # l'ultima barra copre un'ora
|
||||
if t0 < s.index[0] or t1 > fine:
|
||||
non_mis.append(f"{a}: feed 1h fermo a {s.index[-1]:%Y-%m-%dT%H:%M}Z"); continue
|
||||
p0, p1 = s.asof(t0), s.asof(t1)
|
||||
if p0 and p1 and not (pd.isna(p0) or pd.isna(p1)):
|
||||
if pd.isna(p0) or pd.isna(p1) or not p0 or not p1:
|
||||
non_mis.append(f"{a}: barra assente"); continue
|
||||
mosse.append(abs(float(p1) / float(p0) - 1.0))
|
||||
max_mkt = max(mosse) * leva_tetto if mosse else None
|
||||
classe = "movimento" if (max_mkt is not None and abs(pct) > 2.0 * max_mkt) else "ambiguo"
|
||||
if non_mis:
|
||||
max_mkt, classe, nota = None, "ambiguo", "mercato non misurabile: " + "; ".join(non_mis)
|
||||
else:
|
||||
max_mkt = max(mosse) * leva_tetto
|
||||
classe = "movimento" if abs(pct) > 2.0 * max_mkt else "ambiguo"
|
||||
nota = None
|
||||
eventi.append(dict(ts_prima=prev["ts_utc"], ts_dopo=cur["ts_utc"],
|
||||
prima=float(prev["equity"]), dopo=float(cur["equity"]),
|
||||
delta=delta, pct=float(pct),
|
||||
mercato_max_pct=(None if max_mkt is None else float(max_mkt)),
|
||||
classe=classe))
|
||||
mercato_nota=nota, classe=classe))
|
||||
return dict(eventi=eventi,
|
||||
certi=float(sum(e["delta"] for e in eventi if e["classe"] == "movimento")),
|
||||
ambigui=float(sum(e["delta"] for e in eventi if e["classe"] == "ambiguo")),
|
||||
@@ -191,31 +212,57 @@ def rendimento_twr(con, fino_ts: str) -> dict:
|
||||
apre uno all'equity DOPO; il TWR e' il prodotto dei rendimenti di segmento, meno uno. Gli
|
||||
eventi `ambiguo` NON spezzano niente (P12: restano nel rendimento, dichiarati accanto).
|
||||
|
||||
TRE STATI: `twr` e' un numero, oppure None con `motivo` (serie vuota, base zero).
|
||||
⚠️ LIMITE DICHIARATO (D5). L'intervallo che CONTIENE un movimento certo esce INTERO dal
|
||||
rendimento: il suo P&L di mercato (marcatura delle posizioni aperte, funding; in un buco di
|
||||
lettura — fino a 10h nella serie vera — l'intero buco) finisce in `certi`, quindi anche fuori
|
||||
da `trading`. E' limitato per costruzione dal margine del classificatore (|salto| >
|
||||
2·max_mkt·leva·equity), percio' l'errore massimo vale max_mkt·leva·equity: fino a META' del
|
||||
movimento riconosciuto. Il 25/08: libro a ~$263 lordi, mercato a ±0,2% -> ~$0,5 su $1.399,39.
|
||||
Non si stima e non si corregge (P12): si dichiara. Un intervallo di sola marcatura fra due
|
||||
movimenti consecutivi, o prima del primo, ha lunghezza zero e NON produce un segmento.
|
||||
|
||||
TRE STATI: `twr` e `trading` sono numeri, oppure None con `motivo`. Mercato NON misurabile
|
||||
per almeno un salto (feed 1h fermo, o assente) => entrambi None: non si conosce la classe
|
||||
del salto, quindi non si conosce il rendimento — e `e1/e0-1` non torna sotto un'etichetta
|
||||
piu' autorevole. Base di equity zero =>
|
||||
entrambi None: il salto 0 -> X del primo versamento e' INVISIBILE a `movimenti_capitale`
|
||||
(una base zero non ha percentuale), quindi `certi` lo perderebbe e `trading` varrebbe
|
||||
l'intero conto — un numero etichettato "al netto dei versamenti" che li contiene tutti.
|
||||
"""
|
||||
rows = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc <= ? ORDER BY ts_utc",
|
||||
(fino_ts,)).fetchall()
|
||||
if not rows:
|
||||
return dict(twr=None, motivo="nessuna lettura di equity", segmenti=[], eventi=[],
|
||||
certi=0.0, ambigui=0.0, delta_equity=None, trading=None, letture=0)
|
||||
mv = movimenti_capitale(con, fino_ts)
|
||||
certi = [e for e in mv["eventi"] if e["classe"] == "movimento"]
|
||||
mv = movimenti_capitale(con, fino_ts, rows=rows) # stessa lettura, un solo istante
|
||||
e0, e1 = float(rows[0]["equity"]), float(rows[-1]["equity"])
|
||||
if e0 <= 0:
|
||||
return dict(twr=None, trading=None,
|
||||
motivo="base di equity nulla: il primo versamento non e' classificabile",
|
||||
segmenti=[], eventi=mv["eventi"], certi=mv["certi"], ambigui=mv["ambigui"],
|
||||
delta_equity=e1 - e0, letture=len(rows))
|
||||
non_mis = [e for e in mv["eventi"] if e.get("mercato_nota")]
|
||||
if non_mis:
|
||||
# un salto oltre soglia che NON si e' potuto confrontare col mercato non e' ne' un
|
||||
# movimento ne' un rendimento: il TWR non si stampa (altrimenti `e1/e0-1` tornerebbe
|
||||
# sotto l'etichetta "TWR", il numero che il debito 14 e' nato per uccidere)
|
||||
return dict(twr=None, trading=None,
|
||||
motivo=(f"{len(non_mis)} salto/i non classificabile/i — "
|
||||
+ non_mis[0]["mercato_nota"]),
|
||||
segmenti=[], eventi=mv["eventi"], certi=mv["certi"], ambigui=mv["ambigui"],
|
||||
delta_equity=e1 - e0, letture=len(rows))
|
||||
certi = [e for e in mv["eventi"] if e["classe"] == "movimento"]
|
||||
segmenti, da_ts, da_eq = [], rows[0]["ts_utc"], e0
|
||||
for ev in certi: # gia' in ordine di tempo
|
||||
if da_eq:
|
||||
if da_eq and ev["ts_prima"] != da_ts: # lunghezza zero = niente segmento
|
||||
segmenti.append(dict(da=da_ts, a=ev["ts_prima"], e_da=da_eq, e_a=ev["prima"],
|
||||
ret=ev["prima"] / da_eq - 1.0))
|
||||
da_ts, da_eq = ev["ts_dopo"], ev["dopo"]
|
||||
if da_eq:
|
||||
if da_eq and rows[-1]["ts_utc"] != da_ts:
|
||||
segmenti.append(dict(da=da_ts, a=rows[-1]["ts_utc"], e_da=da_eq, e_a=e1,
|
||||
ret=e1 / da_eq - 1.0))
|
||||
if not segmenti or e0 <= 0:
|
||||
return dict(twr=None, motivo="base di equity nulla", segmenti=segmenti, eventi=mv["eventi"],
|
||||
certi=mv["certi"], ambigui=mv["ambigui"], delta_equity=e1 - e0,
|
||||
trading=e1 - e0 - mv["certi"], letture=len(rows))
|
||||
twr = 1.0
|
||||
for sg in segmenti:
|
||||
for sg in segmenti: # nessun segmento = nessun tempo a mercato = 0
|
||||
twr *= 1.0 + sg["ret"]
|
||||
return dict(twr=twr - 1.0, motivo=None, segmenti=segmenti, eventi=mv["eventi"],
|
||||
certi=mv["certi"], ambigui=mv["ambigui"], delta_equity=e1 - e0,
|
||||
@@ -240,7 +287,10 @@ def pnl_giorno(con, giorno: date) -> dict:
|
||||
e_arm = con.execute("SELECT equity FROM equity ORDER BY ts_utc LIMIT 1").fetchone()
|
||||
delta = (e_fin - e_ini) if (e_ini is not None and e_fin is not None) else None
|
||||
cum = (e_fin - e_arm["equity"]) if (e_fin is not None and e_arm) else None
|
||||
mv = movimenti_capitale(con, f"{g}T23:59:59+00:00")
|
||||
# il cumulato dall'arming, i movimenti e il trading al netto vengono da rendimento_twr:
|
||||
# UNA funzione per i due lettori (report e giornale), non due aritmetiche (revisione 02/09)
|
||||
r_twr = rendimento_twr(con, f"{g}T23:59:59+00:00")
|
||||
mv = dict(eventi=r_twr["eventi"], certi=r_twr["certi"], ambigui=r_twr["ambigui"])
|
||||
ev_g = [e for e in mv["eventi"] if e["ts_dopo"][:10] == g]
|
||||
mov_g = sum(e["delta"] for e in ev_g if e["classe"] == "movimento")
|
||||
amb_g = sum(e["delta"] for e in ev_g if e["classe"] == "ambiguo")
|
||||
@@ -254,7 +304,8 @@ def pnl_giorno(con, giorno: date) -> dict:
|
||||
movimenti_giorno=ev_g, mov_giorno=float(mov_g), mov_giorno_ambiguo=float(amb_g),
|
||||
trading_giorno=(delta - mov_g) if delta is not None else None,
|
||||
movimenti_arming=mv["eventi"], mov_arming=mv["certi"], mov_arming_ambiguo=mv["ambigui"],
|
||||
trading_da_arming=(cum - mv["certi"]) if cum is not None else None)
|
||||
trading_da_arming=(r_twr["trading"] if cum is not None else None),
|
||||
twr_da_arming=(r_twr["twr"] if cum is not None else None), twr_motivo=r_twr["motivo"])
|
||||
|
||||
|
||||
def salute(con, giorno: date) -> dict:
|
||||
@@ -626,6 +677,10 @@ def rendi_markdown(con, voce: dict) -> str:
|
||||
if p.get("mov_arming"):
|
||||
riga_c += (f" — di cui ${p['mov_arming']:+,.2f} versati/prelevati -> trading "
|
||||
f"**{_n(p.get('trading_da_arming'),'${:+,.2f}')}**")
|
||||
if p.get("twr_da_arming") is not None:
|
||||
riga_c += f" · TWR **{100 * p['twr_da_arming']:+.2f}%**"
|
||||
elif p.get("twr_motivo"):
|
||||
riga_c += f" · TWR n/d ({p['twr_motivo']})"
|
||||
if p.get("mov_arming_ambiguo"):
|
||||
riga_c += f" — ⚠️ ${p['mov_arming_ambiguo']:+,.2f} ambigui non attribuiti"
|
||||
L += ["", "## P&L", "",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Utilita' condivise dei test. `tests/` e' su sys.path (c'e' un conftest), quindi si importa
|
||||
con `from helpers import carica_script`.
|
||||
|
||||
PERCHE' ESISTE (2026-09-02). `scripts/` non e' un package importabile per nome, quindi ogni test
|
||||
che tocca uno script si caricava il modulo a mano con `importlib.util.spec_from_file_location`:
|
||||
**15 copie della stessa dozzina di righe**, con nomi di modulo diversi per lo stesso file e
|
||||
differenze silenziose (chi registra in `sys.modules` e chi no — e chi non lo fa rompe `pickle`,
|
||||
`dataclasses` e i decoratori che risolvono per nome). Una funzione sola, con la registrazione
|
||||
fatta sempre.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def carica_script(rel: str, nome: str | None = None):
|
||||
"""Carica per PERCORSO uno script del repo (es. "scripts/live/trades_db.py").
|
||||
|
||||
`nome` e' il nome sotto cui il modulo viene registrato in `sys.modules`; il default e' il
|
||||
nome del file. Registrare PRIMA di eseguire e' voluto: un modulo che si auto-importa o usa
|
||||
`dataclasses` durante l'esecuzione deve trovarsi.
|
||||
"""
|
||||
p = (ROOT / rel).resolve()
|
||||
assert p.exists(), f"script inesistente: {rel}"
|
||||
nome = nome or p.stem
|
||||
spec = importlib.util.spec_from_file_location(nome, p)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
sys.modules[nome] = mod
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
@@ -0,0 +1,207 @@
|
||||
"""La cadenza del libro live e' ORARIA, e le tre dichiarazioni devono restare d'accordo.
|
||||
|
||||
Debito §5.7 (chiuso il 2026-09-02): il docstring di `book_execute.py` prescriveva «ogni ~230
|
||||
minuti» mentre il cron gira ogni ora. Non era cosmetico: `r0823_sl_anchor.py` misura che gli
|
||||
scatti del disaster-SL rotolante dipendono dalla cadenza — 1h: BTC 0 / ETH 1 in 7-8 anni; 4h:
|
||||
BTC 2 (contro 1 del pavimento); 24h: ETH 5 con 2 in 30 giorni. Chi avesse "corretto" il CRON
|
||||
verso il docstring avrebbe spostato il libro dalla riga 1h a quella 4h.
|
||||
|
||||
P1: il bersaglio si DERIVA da cio' che gira. Qui le fonti sono tre — il docstring (cio' che un
|
||||
umano legge), `cron_book.sh` (cio' che dichiara di essere installato) e la crontab (cio' che
|
||||
gira davvero) — e il test le confronta fra loro invece di ridichiarare «60 minuti».
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.live import venue_probe as VP # noqa: E402 lo slot di release, dichiarato li'
|
||||
from src.strategies.skyhook import LTF_MIN # noqa: E402 la griglia di SKH01, NON ridichiarata
|
||||
|
||||
BOOK_EXECUTE = ROOT / "scripts" / "live" / "book_execute.py"
|
||||
CRON_BOOK = ROOT / "scripts" / "cron_book.sh"
|
||||
RIGA_4H_MIN = 240 # la riga in cui il rotolante di BTC passa da 0 a 2 scatti (r0823_sl_anchor.py);
|
||||
# la PEGGIORE misurata e' 24h (ETH 5, 2 in 30 giorni), non questa
|
||||
|
||||
_RX_CRON5 = re.compile(r"`([\d*/,-]+(?: [\d*/,-]+){4})`")
|
||||
|
||||
|
||||
def cadenza_minuti(expr: str) -> int:
|
||||
"""Minuti fra due esecuzioni di un'espressione crontab a 5 campi, SOLO per cadenze regolari
|
||||
(intervalli tutti uguali): `47 * * * *` -> 60, `*/30 * * * *` -> 30, `7 */4 * * *` -> 240.
|
||||
`*/45` (intervalli 45/15), `0 */5` (ultimo intervallo 4h) e `*/0` vengono RIFIUTATI."""
|
||||
campi = expr.split()
|
||||
assert len(campi) == 5, expr
|
||||
m, h, dom, mon, dow = campi
|
||||
assert dom == mon == dow == "*", f"cadenza non regolare: {expr}"
|
||||
if h == "*" and m.isdigit():
|
||||
return 60
|
||||
if h == "*" and m.startswith("*/"):
|
||||
passo = int(m[2:])
|
||||
if passo <= 0 or 60 % passo:
|
||||
raise ValueError(f"passo di minuti non regolare: {expr}")
|
||||
return passo
|
||||
if h.startswith("*/") and m.isdigit():
|
||||
passo = int(h[2:])
|
||||
if passo <= 0 or 24 % passo:
|
||||
raise ValueError(f"passo di ore non regolare: {expr}")
|
||||
return 60 * passo
|
||||
raise ValueError(f"espressione non riconosciuta: {expr}")
|
||||
|
||||
|
||||
def _docstring() -> str:
|
||||
return ast.get_docstring(ast.parse(BOOK_EXECUTE.read_text())) or ""
|
||||
|
||||
|
||||
def _cron_dichiarato() -> str:
|
||||
"""La prima espressione crontab a 5 campi che `cron_book.sh` dichiara fra backtick — di
|
||||
qualunque forma: e' `cadenza_minuti` a giudicarla, non questa regex (P4: un'espressione
|
||||
presente ma non oraria deve fallire come 'non oraria', non come 'assente')."""
|
||||
m = _RX_CRON5.search(CRON_BOOK.read_text())
|
||||
assert m, "cron_book.sh non dichiara piu' la sua riga di crontab fra backtick"
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def _crontab() -> str | None:
|
||||
"""Testo della crontab dell'utente, None se non leggibile (P5: distinto da 'vuota')."""
|
||||
if not shutil.which("crontab"):
|
||||
return None
|
||||
try:
|
||||
out = subprocess.run(["crontab", "-l"], capture_output=True, text=True, timeout=10)
|
||||
except Exception:
|
||||
return None
|
||||
return out.stdout if out.returncode == 0 else None
|
||||
|
||||
|
||||
def _righe_attive(testo: str, marcatore: str) -> list[str]:
|
||||
"""Le espressioni (5 campi) di TUTTE le righe attive che citano `marcatore`: due righe sono
|
||||
due esecuzioni, e la seconda non deve nascondersi dietro la prima."""
|
||||
return [" ".join(r.split()[:5]) for r in testo.splitlines()
|
||||
if marcatore in r and not r.lstrip().startswith("#")]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- regressione sul docstring
|
||||
# La guardia non cerca due letterali («ogni ~230»): estrae OGNI prescrizione di cadenza scritta
|
||||
# come «ogni/every N unita'» e pretende che valga 60 minuti. ⚠️ Limite dichiarato (P13): una
|
||||
# guardia sui numeri non copre il ragionamento — una prosa che prescrive una cadenza senza la
|
||||
# parola «ogni» passa. Il controllo positivo qui sotto dice cosa la guardia VEDE.
|
||||
|
||||
_NUMERI = {"un": 1, "una": 1, "due": 2, "tre": 3, "quattro": 4, "sei": 6, "otto": 8, "dodici": 12}
|
||||
_RX_CADENZA = re.compile(
|
||||
r"\b(?:ogni|every)\s*~?\s*(\d+|un'?|una|due|tre|quattro|sei|otto|dodici)?\s*'?\s*"
|
||||
r"(minuti|minuto|minutes?|min|m|ore|ora|hours?|h)\b", re.I)
|
||||
|
||||
|
||||
def prescrizioni_minuti(testo: str) -> list[int]:
|
||||
"""Ogni «ogni N unita'» del testo, in minuti. «ogni ora» -> 60, «ogni ~230 minuti» -> 230."""
|
||||
out = []
|
||||
for m in _RX_CADENZA.finditer(testo):
|
||||
n_raw, unita = m.group(1), m.group(2).lower()
|
||||
n = 1 if n_raw is None else (int(n_raw) if n_raw.isdigit() else _NUMERI[n_raw.rstrip("'")])
|
||||
out.append(n * (60 if unita.startswith(("or", "h")) else 1))
|
||||
return out
|
||||
|
||||
|
||||
def test_la_guardia_vede_le_prescrizioni_che_deve_vedere():
|
||||
# controllo positivo (M15): i modi in cui il docstring potrebbe ri-prescrivere la riga 4h
|
||||
assert prescrizioni_minuti("va lanciato ogni ~230 minuti con la feed") == [230]
|
||||
assert prescrizioni_minuti("il giro va lanciato ogni quattro ore") == [240]
|
||||
assert prescrizioni_minuti("every 4 hours, then every 230 min") == [240, 230]
|
||||
assert prescrizioni_minuti("controllato ogni ora; ogni 60 minuti") == [60, 60]
|
||||
assert prescrizioni_minuti("fino a 230m di latenza") == [] # non e' una prescrizione
|
||||
|
||||
|
||||
def test_il_docstring_prescrive_solo_la_cadenza_oraria():
|
||||
d = _docstring()
|
||||
p = prescrizioni_minuti(d)
|
||||
assert p, "il docstring deve contenere almeno una prescrizione di cadenza"
|
||||
assert all(x == 60 for x in p), f"prescrizioni non orarie nel docstring: {p}"
|
||||
assert re.search(r"CADENZA:\s*ORARIA", d), "il docstring deve dichiarare la cadenza ORARIA"
|
||||
|
||||
|
||||
def test_il_docstring_dichiara_la_stessa_riga_del_cron():
|
||||
# la riga di crontab citata nel docstring e' quella di cron_book.sh, non un'altra
|
||||
d = _docstring()
|
||||
assert _cron_dichiarato() in d
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- cron_book.sh
|
||||
|
||||
def test_cron_book_e_orario_sotto_la_griglia_skh_e_sotto_la_riga_4h():
|
||||
c = cadenza_minuti(_cron_dichiarato())
|
||||
assert c == 60
|
||||
assert c < LTF_MIN, "la cadenza deve stare SOTTO la griglia di SKH01 (latenza <= 1 barra)"
|
||||
assert c < RIGA_4H_MIN, "la riga 4h e' quella in cui il rotolante di BTC fa 2 scatti"
|
||||
assert "ORARIA" in CRON_BOOK.read_text()
|
||||
|
||||
|
||||
def test_cron_book_evita_il_minuto_tondo():
|
||||
# vincolo 1 del commento di cron_book.sh: fuori dai ~26s del :00 (rate-limit per-IP)
|
||||
minuto = int(_cron_dichiarato().split()[0])
|
||||
assert minuto != 0
|
||||
|
||||
|
||||
def test_cron_book_evita_lo_slot_di_release_deribit():
|
||||
# vincolo 2 del commento di cron_book.sh — il motivo vero del :07 -> :47 (4 giri morti in
|
||||
# 63 giorni): il giro del martedi' alle 09:MM deve cadere FUORI dallo slot dichiarato in
|
||||
# venue_probe (derivato, non riscritto). Il :07 lo violava; se qualcuno tornasse a :15
|
||||
# con docstring e crontab coerenti, gli altri test passerebbero e questo no.
|
||||
minuto = int(_cron_dichiarato().split()[0])
|
||||
base = datetime(2026, 9, 1, tzinfo=timezone.utc) # un lunedi'
|
||||
martedi = base + timedelta(days=(VP.RELEASE_WEEKDAY - base.weekday()) % 7)
|
||||
giro = martedi.replace(hour=VP.RELEASE_START_MIN // 60, minute=minuto)
|
||||
assert not VP.in_release_window(giro), f"il giro :{minuto:02d} cade nello slot di release"
|
||||
assert VP.in_release_window(giro.replace(minute=7)), "controllo positivo: il :07 ci cadeva"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expr,atteso", [("47 * * * *", 60), ("*/30 * * * *", 30),
|
||||
("7 */4 * * *", 240), ("0 */24 * * *", 1440)])
|
||||
def test_il_parser_della_cadenza(expr, atteso):
|
||||
assert cadenza_minuti(expr) == atteso
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expr", ["47 9 * * 2", "*/45 * * * *", "0 */5 * * *", "*/0 * * * *",
|
||||
"47 * 1 * *"])
|
||||
def test_il_parser_rifiuta_cio_che_non_e_regolare(expr):
|
||||
with pytest.raises((ValueError, AssertionError)):
|
||||
cadenza_minuti(expr)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- la crontab che GIRA
|
||||
|
||||
def test_la_crontab_installata_e_quella_dichiarata_e_una_sola():
|
||||
"""P16: il cron gira dalla macchina, non dal repo. TRE stati, non due (P5):
|
||||
- crontab NON leggibile -> SALTATO (non verificabile qui);
|
||||
- leggibile, e questo progetto non ci compare affatto -> SALTATO (macchina di sviluppo);
|
||||
- leggibile con altre righe del progetto ma SENZA cron_book.sh attivo -> ROSSO: il libro
|
||||
non e' schedulato dove il progetto e' installato (una riga commentata per manutenzione e
|
||||
dimenticata e' esattamente questo caso).
|
||||
E la riga deve essere UNA: due righe attive sono due esecuzioni l'ora."""
|
||||
testo = _crontab()
|
||||
if testo is None:
|
||||
pytest.skip("crontab non leggibile: non verificabile qui")
|
||||
if str(ROOT) not in testo and ROOT.name not in testo:
|
||||
pytest.skip("la crontab non cita questo progetto: non e' la macchina che lo esegue")
|
||||
righe = _righe_attive(testo, "cron_book.sh")
|
||||
assert righe, "questa macchina esegue il progetto ma cron_book.sh NON e' schedulato"
|
||||
assert len(righe) == 1, f"cron_book.sh schedulato {len(righe)} volte: {righe}"
|
||||
assert righe[0] == _cron_dichiarato()
|
||||
assert cadenza_minuti(righe[0]) == 60
|
||||
|
||||
|
||||
def test_due_righe_attive_non_si_nascondono_dietro_la_prima():
|
||||
finta = ("47 * * * * /x/PythagorasGoal/scripts/cron_book.sh\n"
|
||||
"# 7 * * * * /x/PythagorasGoal/scripts/cron_book.sh\n"
|
||||
"17 * * * * /x/PythagorasGoal/scripts/cron_book.sh\n")
|
||||
assert _righe_attive(finta, "cron_book.sh") == ["47 * * * *", "17 * * * *"]
|
||||
assert _righe_attive("# 47 * * * * /x/cron_book.sh\n", "cron_book.sh") == []
|
||||
+9
-15
@@ -192,12 +192,9 @@ def test_gate_requires_both_switches():
|
||||
|
||||
def test_config_default_disarmed(tmp_path, monkeypatch):
|
||||
"""load_config di book_execute mette execution_enabled=False di default (fail-safe)."""
|
||||
import importlib
|
||||
be = importlib.import_module("scripts.live.book_execute") if False else None
|
||||
# carica il modulo via path (scripts/ non e' un package importabile per nome)
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("book_execute", PROJECT_ROOT / "scripts/live/book_execute.py")
|
||||
mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
|
||||
# `scripts/` non e' un package importabile per nome: si carica via percorso
|
||||
from helpers import carica_script
|
||||
mod = carica_script("scripts/live/book_execute.py")
|
||||
monkeypatch.setattr(mod, "CONFIG", tmp_path / "nope.json") # config assente
|
||||
assert mod.load_config()["execution_enabled"] is False
|
||||
|
||||
@@ -419,9 +416,8 @@ def test_book_report_flags_skh_feed_error(monkeypatch):
|
||||
|
||||
def test_book_execute_surfaces_skh_error(monkeypatch, capsys):
|
||||
"""Se il report porta skh_error, _run() lo stampa E chiama notify (niente flat silenzioso)."""
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("book_execute", PROJECT_ROOT / "scripts/live/book_execute.py")
|
||||
mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
|
||||
from helpers import carica_script
|
||||
mod = carica_script("scripts/live/book_execute.py")
|
||||
|
||||
canned = dict(
|
||||
last_data=_fresh_bar(), online=True, real_equity=600.0, equity=600.0, eq_basis="test",
|
||||
@@ -481,9 +477,8 @@ def test_book_report_propagates_pos_error(monkeypatch):
|
||||
|
||||
def test_book_execute_halts_on_unreadable_position(monkeypatch, capsys):
|
||||
"""ARMATO + --execute + ordine presente: il gate DEVE fermarsi PRIMA di costruire il trader."""
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("book_execute", PROJECT_ROOT / "scripts/live/book_execute.py")
|
||||
mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
|
||||
from helpers import carica_script
|
||||
mod = carica_script("scripts/live/book_execute.py")
|
||||
|
||||
canned = dict(
|
||||
last_data=_fresh_bar(), online=True, real_equity=598.0, equity=598.0, eq_basis="mainnet USDC",
|
||||
@@ -542,9 +537,8 @@ def test_book_report_propagates_eq_fallback(monkeypatch):
|
||||
|
||||
def test_book_execute_eq_fallback_warns_but_proceeds(monkeypatch, capsys):
|
||||
"""eq_fallback: avvisa + notify MA PROSEGUE (non e' un halt, a differenza di pos_error)."""
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("book_execute", PROJECT_ROOT / "scripts/live/book_execute.py")
|
||||
mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
|
||||
from helpers import carica_script
|
||||
mod = carica_script("scripts/live/book_execute.py")
|
||||
|
||||
canned = dict(
|
||||
last_data=_fresh_bar(), online=True, real_equity=None, equity=2000.0,
|
||||
|
||||
@@ -116,11 +116,8 @@ def test_posizione_flat_non_puo_diventare_naked():
|
||||
# B. Isolamento per asset dentro book_execute._run().
|
||||
# ---------------------------------------------------------------------------
|
||||
def _carica_book_execute():
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"book_execute", PROJECT_ROOT / "scripts/live/book_execute.py")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
from helpers import carica_script
|
||||
return carica_script("scripts/live/book_execute.py")
|
||||
|
||||
|
||||
def _report_due_asset():
|
||||
|
||||
@@ -58,11 +58,8 @@ def test_funzionale_feed_stantio_blocca_lesecuzione_e_allerta(monkeypatch, capsy
|
||||
"""Il test che conta: conto ONLINE, posizione LEGGIBILE, ordine PRESENTE — cioe' la situazione
|
||||
esatta del 2026-07-14 — ma feed vecchio. Non deve partire nessun ordine e deve arrivare l'alert.
|
||||
"""
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"book_execute_iso", ROOT / "scripts" / "live" / "book_execute.py")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
from helpers import carica_script
|
||||
mod = carica_script("scripts/live/book_execute.py", "book_execute_iso")
|
||||
|
||||
canned = dict(
|
||||
last_data="2026-07-08", online=True, real_equity=598.0, equity=598.0,
|
||||
@@ -94,11 +91,8 @@ def test_funzionale_feed_stantio_blocca_lesecuzione_e_allerta(monkeypatch, capsy
|
||||
def test_config_senza_la_chiave_non_esplode(monkeypatch, capsys):
|
||||
"""Una config priva di max_data_age_days deve ricadere sul default, non sollevare KeyError
|
||||
dentro il percorso con soldi veri."""
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"book_execute_iso2", ROOT / "scripts" / "live" / "book_execute.py")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
from helpers import carica_script
|
||||
mod = carica_script("scripts/live/book_execute.py", "book_execute_iso2")
|
||||
canned = dict(last_data=str(pd.Timestamp.now(tz="UTC").normalize().date()), online=True,
|
||||
real_equity=598.0, equity=598.0, eq_basis="t", cap_per_asset=300.0,
|
||||
skh_error=None, pos_error=None, assets=[], orders=[])
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
"""Un flag ignoto non e' l'azione di default (2026-09-02).
|
||||
|
||||
Costo misurato durante una revisione dello stesso giorno: `trades_db.py --help` non stampava
|
||||
l'uso, cadeva in `sync()` e riscriveva `meta.ultimo_sync`. Su `journal.py` lo stesso errore
|
||||
avrebbe scritto una pagina e una riga di DB; su `analista.py` avrebbe speso una chiamata al
|
||||
modello e mandato un Telegram.
|
||||
|
||||
Il test guarda anche l'altra meta' del contratto (P15): i flag che il CRON usa devono restare
|
||||
accettati — una guardia che rompe il cron e' peggio del difetto che ripara.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.live.cli import UsoSbagliato, valida # noqa: E402
|
||||
|
||||
# Tutti gli script di scripts/live/ che NON usano argparse: la guardia deve coprirli tutti, e
|
||||
# l'elenco si DERIVA dalla cartella (P1) — un file nuovo senza guardia fa fallire il test, non
|
||||
# passa inosservato perche' nessuno ha aggiornato una lista.
|
||||
LIVE = Path(__file__).resolve().parents[1] / "scripts" / "live"
|
||||
|
||||
|
||||
def _senza_argparse() -> list[str]:
|
||||
fuori = []
|
||||
for f in sorted(LIVE.glob("*.py")):
|
||||
s = f.read_text()
|
||||
if f.name == "__init__.py" or "argparse" in s or "if __name__" not in s:
|
||||
continue
|
||||
fuori.append(f.name)
|
||||
return fuori
|
||||
|
||||
|
||||
SCRIPT_GUARDATI = tuple(_senza_argparse())
|
||||
assert SCRIPT_GUARDATI, "nessuno script da guardare: il modulo non sta controllando niente"
|
||||
SCRIVONO = ("trades_db.py", "journal.py", "analista.py") # i tre che hanno pagato il difetto
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- la funzione
|
||||
|
||||
def _v(argv, **kw):
|
||||
return valida("x.py", "uso: x.py", argv=argv, esci=False, **kw)
|
||||
|
||||
|
||||
def test_un_flag_noto_passa_e_torna_gli_argomenti():
|
||||
assert _v(["--report"], flag=("--report",)) == ["--report"]
|
||||
|
||||
|
||||
def test_un_flag_ignoto_e_un_errore_non_il_default():
|
||||
with pytest.raises(UsoSbagliato) as e:
|
||||
_v(["--help-me"], flag=("--report",))
|
||||
assert "--help-me" in str(e.value) and "--report" in str(e.value) # P4: cosa, e cosa si puo'
|
||||
|
||||
|
||||
def test_il_valore_di_un_flag_non_e_scambiato_per_un_flag():
|
||||
assert _v(["--giorno", "2026-08-21"], con_valore=("--giorno",)) == ["--giorno", "2026-08-21"]
|
||||
with pytest.raises(UsoSbagliato):
|
||||
_v(["--giorno"], con_valore=("--giorno",)) # manca il valore
|
||||
|
||||
|
||||
def test_un_valore_che_sembra_un_flag_resta_un_valore():
|
||||
assert _v(["--nota", "--quiet"], con_valore=("--nota",), flag=("--quiet",))
|
||||
|
||||
|
||||
def test_un_posizionale_e_ignoto():
|
||||
with pytest.raises(UsoSbagliato):
|
||||
_v(["report"], flag=("--report",))
|
||||
|
||||
|
||||
def test_senza_argomenti_non_solleva():
|
||||
assert _v([], flag=("--report",)) == []
|
||||
|
||||
|
||||
def test_help_non_e_un_errore():
|
||||
assert _v(["--help"], flag=("--report",)) == ["--help"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- gli script veri
|
||||
|
||||
def _uso(nome: str) -> str:
|
||||
"""La costante USO dello script, letta dal sorgente senza importarlo."""
|
||||
src = (ROOT / "scripts" / "live" / nome).read_text()
|
||||
for n in ast.parse(src).body:
|
||||
if isinstance(n, ast.Assign) and getattr(n.targets[0], "id", "") == "USO":
|
||||
return n.value.value
|
||||
raise AssertionError(f"{nome} non dichiara USO")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("nome", SCRIPT_GUARDATI)
|
||||
def test_lo_script_chiama_valida_prima_di_ogni_effetto(nome):
|
||||
"""`valida` dev'essere la PRIMA istruzione eseguibile del blocco __main__: uscire dopo aver
|
||||
aperto il DB o sincronizzato sarebbe uscire dopo l'effetto."""
|
||||
src = (ROOT / "scripts" / "live" / nome).read_text()
|
||||
main = [n for n in ast.parse(src).body
|
||||
if isinstance(n, ast.If) and "__main__" in ast.dump(n.test)]
|
||||
assert main, f"{nome}: nessun blocco __main__"
|
||||
corpo = [n for n in main[0].body if not isinstance(n, ast.ImportFrom | ast.Import)]
|
||||
chiamate = [n for n in ast.walk(corpo[0]) if isinstance(n, ast.Call)]
|
||||
assert any(getattr(c.func, "id", "") == "valida" for c in chiamate), \
|
||||
f"{nome}: la prima istruzione di __main__ non e' valida()"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("nome", SCRIPT_GUARDATI)
|
||||
def test_l_uso_elenca_esattamente_i_flag_dichiarati(nome):
|
||||
"""P4 e P1: il testo d'aiuto non e' prosa libera — deve nominare i flag che lo script accetta."""
|
||||
src = (ROOT / "scripts" / "live" / nome).read_text()
|
||||
dichiarati = set(re.findall(r'["\'](--[a-z-]+)["\']', src.split("if __name__")[1]))
|
||||
uso = _uso(nome)
|
||||
mancanti = {f for f in dichiarati if f not in uso}
|
||||
assert not mancanti, f"{nome}: flag accettati ma non documentati in USO: {mancanti}"
|
||||
|
||||
|
||||
# I flag che il cron usa DAVVERO, letti dagli script di cron: se la guardia ne rifiutasse uno,
|
||||
# il libro di bordo si fermerebbe la notte stessa (P16: il cron gira dal working tree).
|
||||
def test_i_flag_del_cron_sono_tutti_accettati():
|
||||
usati: dict[str, set[str]] = {}
|
||||
for sh in (ROOT / "scripts").glob("cron_*.sh"):
|
||||
for riga in sh.read_text().splitlines():
|
||||
for nome in SCRIPT_GUARDATI:
|
||||
if f"scripts/live/{nome}" in riga and not riga.lstrip().startswith("#"):
|
||||
usati.setdefault(nome, set()).update(re.findall(r"(--[a-z-]+)", riga))
|
||||
assert usati, "nessun cron chiama gli script guardati: il test non sta controllando niente"
|
||||
for nome, flag in usati.items():
|
||||
src = (ROOT / "scripts" / "live" / nome).read_text()
|
||||
noti = set(re.findall(r'["\'](--[a-z-]+)["\']', src.split("if __name__")[1]))
|
||||
assert flag <= noti, f"{nome}: il cron usa {flag - noti}, che la guardia rifiuterebbe"
|
||||
|
||||
# controllo positivo (M15): il confronto sa dire di no. Se il cron usasse un flag non
|
||||
# dichiarato, questo test lo direbbe — qui lo si verifica su un caso finto.
|
||||
assert not {"--inventato"} <= {"--quiet"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("nome", SCRIVONO)
|
||||
def test_help_esce_zero_e_non_scrive(nome):
|
||||
"""Controllo end-to-end (M15): il caso che ha causato il difetto, sui tre script veri."""
|
||||
prima = (ROOT / "data" / "live" / "trades.db").stat().st_mtime
|
||||
r = subprocess.run([sys.executable, str(ROOT / "scripts" / "live" / nome), "--help"],
|
||||
capture_output=True, text=True, cwd=ROOT, timeout=120)
|
||||
assert r.returncode == 0, r.stderr[-500:]
|
||||
assert "uso:" in r.stdout
|
||||
assert (ROOT / "data" / "live" / "trades.db").stat().st_mtime == prima
|
||||
|
||||
|
||||
@pytest.mark.parametrize("nome", SCRIVONO)
|
||||
def test_un_flag_ignoto_esce_due_e_non_scrive(nome):
|
||||
prima = (ROOT / "data" / "live" / "trades.db").stat().st_mtime
|
||||
r = subprocess.run([sys.executable, str(ROOT / "scripts" / "live" / nome), "--nonesiste"],
|
||||
capture_output=True, text=True, cwd=ROOT, timeout=120)
|
||||
assert r.returncode == 2, r.stdout[-300:]
|
||||
assert "non previsto" in r.stderr
|
||||
assert (ROOT / "data" / "live" / "trades.db").stat().st_mtime == prima
|
||||
|
||||
|
||||
def test_ogni_script_senza_argparse_ha_la_guardia():
|
||||
"""L'elenco e' derivato dalla cartella: uno script nuovo che legge sys.argv a mano e non
|
||||
chiama `valida` fa fallire questo test invece di ripetere il difetto in silenzio."""
|
||||
senza = [n for n in SCRIPT_GUARDATI
|
||||
if "valida(" not in (LIVE / n).read_text().split("if __name__")[1]]
|
||||
assert not senza, f"script senza guardia sui flag: {senza}"
|
||||
assert len(SCRIPT_GUARDATI) >= 17, f"troppo pochi script guardati: {SCRIPT_GUARDATI}"
|
||||
@@ -568,3 +568,115 @@ def test_twr_riproduce_il_numero_del_diario_01_09(db, monkeypatch):
|
||||
r = J.rendimento_twr(db, "2026-09-01T23:59:59+00:00")
|
||||
assert round(100 * r["twr"], 2) == 10.80
|
||||
assert r["certi"] == pytest.approx(1399.39)
|
||||
|
||||
|
||||
def test_twr_a_base_zero_e_None_anche_sul_trading(db, monkeypatch):
|
||||
# revisione 02/09: a base zero `movimenti_capitale` non vede il salto 0 -> X del primo
|
||||
# versamento, quindi `trading = e1-e0-certi` varrebbe l'intero conto. Tre stati, non due.
|
||||
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
|
||||
_serie_equity(db, [("2026-08-24T09:47:01+00:00", 0.0),
|
||||
("2026-08-24T10:47:01+00:00", 598.06),
|
||||
("2026-08-25T10:47:01+00:00", 610.0)])
|
||||
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
|
||||
assert r["twr"] is None and r["trading"] is None and "base" in r["motivo"]
|
||||
|
||||
|
||||
def test_twr_due_movimenti_consecutivi_non_producono_segmenti_a_lunghezza_zero(db, monkeypatch):
|
||||
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
|
||||
_serie_equity(db, [("2026-08-24T10:47:01+00:00", 600.0),
|
||||
("2026-08-24T11:47:01+00:00", 2000.0), # movimento nel PRIMO intervallo
|
||||
("2026-08-24T12:47:01+00:00", 3400.0), # movimento consecutivo
|
||||
("2026-08-25T10:47:01+00:00", 3434.0)]) # +1% di trading
|
||||
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
|
||||
assert len(r["segmenti"]) == 1 and r["segmenti"][0]["ret"] == pytest.approx(0.01)
|
||||
assert r["twr"] == pytest.approx(0.01)
|
||||
assert all(s["da"] != s["a"] for s in r["segmenti"])
|
||||
assert r["certi"] == pytest.approx(1400.0 + 1400.0)
|
||||
|
||||
|
||||
def test_twr_senza_tempo_a_mercato_e_zero_non_None(db, monkeypatch):
|
||||
# due letture, la seconda e' un versamento: nessun segmento, rendimento 0, dichiarato numero
|
||||
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
|
||||
_serie_equity(db, [("2026-08-24T10:47:01+00:00", 600.0),
|
||||
("2026-08-24T11:47:01+00:00", 2000.0)])
|
||||
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
|
||||
assert r["twr"] == 0.0 and r["segmenti"] == [] and r["trading"] == pytest.approx(0.0)
|
||||
|
||||
|
||||
# ------------------------------------------------- mercato NON misurabile (revisione 02/09)
|
||||
# Il feed 1h certificato si ferma alle 00:00 (rebuild alle 00:30): per il resto del giorno
|
||||
# `asof` restituiva la STESSA barra per le due letture, max_mkt 0, e qualunque calo >=10%
|
||||
# diventava un "movimento" — un crash stampato come prelievo. E con il feed assente tutto
|
||||
# era "ambiguo", certi=0, e il TWR tornava a essere e1/e0-1 sotto l'etichetta sbagliata.
|
||||
|
||||
def test_un_salto_oltre_la_fine_del_feed_e_ambiguo_con_la_ragione(db, monkeypatch):
|
||||
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto()) # copre fino al 27/08
|
||||
_serie_equity(db, [("2026-08-28T10:47:01+00:00", 2000.0),
|
||||
("2026-08-28T11:47:01+00:00", 1760.0)]) # -12% dopo la fine
|
||||
p = J.pnl_giorno(db, date(2026, 8, 28))
|
||||
ev = p["movimenti_giorno"]
|
||||
assert ev[0]["classe"] == "ambiguo" and ev[0]["mercato_max_pct"] is None
|
||||
assert "fermo a 2026-08-27" in ev[0]["mercato_nota"]
|
||||
assert p["mov_giorno"] == 0.0 # NON scorporato
|
||||
assert p["trading_da_arming"] is None and p["twr_da_arming"] is None
|
||||
assert "non classificabile" in p["twr_motivo"]
|
||||
|
||||
|
||||
def test_con_il_feed_assente_il_twr_e_None_non_il_grezzo(db, monkeypatch):
|
||||
monkeypatch.setattr(J, "_px_1h", lambda a: None)
|
||||
_serie_equity(db, [("2026-08-25T10:47:01+00:00", 667.0),
|
||||
("2026-08-25T11:47:01+00:00", 2066.0)])
|
||||
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
|
||||
assert r["twr"] is None and r["trading"] is None
|
||||
assert "non leggibile" in r["motivo"]
|
||||
assert r["eventi"][0]["classe"] == "ambiguo"
|
||||
|
||||
|
||||
def test_il_giornale_e_il_report_usano_la_stessa_funzione(db, monkeypatch):
|
||||
# P1, revisione 02/09: pnl_giorno non rifa' piu' `cum - certi`; prende trading e TWR da
|
||||
# rendimento_twr, e la pagina lo stampa accanto al cumulato
|
||||
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
|
||||
_serie_equity(db, [("2026-08-24T10:47:01+00:00", 600.0),
|
||||
("2026-08-25T10:47:01+00:00", 630.0),
|
||||
("2026-08-25T11:47:01+00:00", 2030.0),
|
||||
("2026-08-25T12:47:01+00:00", 2009.7)])
|
||||
p = J.pnl_giorno(db, date(2026, 8, 25))
|
||||
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
|
||||
assert p["trading_da_arming"] == pytest.approx(r["trading"])
|
||||
assert p["twr_da_arming"] == pytest.approx(r["twr"]) == pytest.approx(1.05 * 0.99 - 1)
|
||||
md = J.rendi_markdown(db, _voce(pnl=p, giorno="2026-08-25"))
|
||||
assert "TWR **+3.95%**" in md
|
||||
|
||||
|
||||
def test_la_leva_del_classificatore_include_la_scala_e_il_tetto_di_codice(db, monkeypatch, tmp_path):
|
||||
# P1: la chiave di scala del 01/09 entra nel bound da sola; e il fallback e' il tetto di CODICE
|
||||
from src.live import book as B
|
||||
cfg = tmp_path / "live.json"
|
||||
cfg.write_text(json.dumps({"max_notional_per_asset_frac": 0.5, "book_scale_k": 1.25}))
|
||||
monkeypatch.setattr(J, "PROJECT_ROOT", tmp_path); (tmp_path / "config").mkdir()
|
||||
cfg.rename(tmp_path / "config" / "live.json")
|
||||
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
|
||||
_serie_equity(db, [("2026-08-25T10:47:01+00:00", 600.0), ("2026-08-25T11:47:01+00:00", 2000.0)])
|
||||
mv = J.movimenti_capitale(db, "2026-08-25T23:59:59+00:00")
|
||||
assert mv["leva_tetto"] == pytest.approx(min(0.5 * 2 * 1.25, B.LEVA_LORDA_MAX))
|
||||
|
||||
|
||||
def test_balance_watch_registra_anche_l_indice_usde(monkeypatch):
|
||||
"""Debito §5.15, primo passo (02/09): serve una serie ORARIA dell'indice usde_usdc — quella
|
||||
di `usde_watch` e' giornaliera, e il classificatore confronta letture a un'ora di distanza.
|
||||
Il campione la porta, e un indice non leggibile e' None con la ragione, mai 1.0 (P5)."""
|
||||
from helpers import carica_script
|
||||
bw = carica_script("scripts/live/balance_watch.py")
|
||||
|
||||
class _C:
|
||||
def account_summary(self, cur): return {"balance": 1.0, "equity": 1.0}
|
||||
def positions(self, cur): return []
|
||||
|
||||
from src.live import usde as U
|
||||
monkeypatch.setattr(U, "prezzo_indice", lambda *a, **k: 0.9997)
|
||||
assert bw.campiona(_C())["usde_usdc"] == 0.9997
|
||||
|
||||
def _rotto(*a, **k): raise RuntimeError("giu'")
|
||||
monkeypatch.setattr(U, "prezzo_indice", _rotto)
|
||||
rec = bw.campiona(_C())
|
||||
assert rec["usde_usdc"] is None and rec["usde_usdc_errore"] == "RuntimeError"
|
||||
|
||||
+41
-16
@@ -6,25 +6,21 @@ finisce in tmp grazie alla fixture autouse di conftest: qui non si tocca il libr
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from helpers import carica_script # noqa: E402
|
||||
from src.live import journal as J # noqa: E402
|
||||
from src.live import tradesdb as T # noqa: E402
|
||||
|
||||
|
||||
def _carica_script():
|
||||
spec = importlib.util.spec_from_file_location("trades_db_script", ROOT / "scripts/live/trades_db.py")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
return carica_script("scripts/live/trades_db.py", "trades_db_script")
|
||||
|
||||
|
||||
def _px_piatto(livello=100.0):
|
||||
@@ -33,11 +29,11 @@ def _px_piatto(livello=100.0):
|
||||
|
||||
|
||||
def _prepara(con, equity):
|
||||
con.execute("INSERT INTO fills (fill_id, ts_utc, ts_source, asset, side, qty, price, fee) "
|
||||
"VALUES ('f1','2026-08-24T10:47:15+00:00','test','BTC','buy',0.001,60000.0,0.01)")
|
||||
for ts, eq in equity:
|
||||
con.execute("INSERT INTO equity (ts_utc, equity, src) VALUES (?,?,?)", (ts, eq, "test"))
|
||||
con.commit()
|
||||
# il fill si scrive con `upsert_fills`, non con un INSERT a mano: una riga scritta a mano
|
||||
# aveva `verified` NULL, una forma che la produzione non produce mai
|
||||
T.upsert_fills(con, [T.Fill(ts_utc="2026-08-24T10:47:15+00:00", ts_source="test",
|
||||
asset="BTC", side="buy", qty=0.001, price=60000.0, fee=0.01)])
|
||||
T.upsert_equity(con, [(ts, eq, "test") for ts, eq in equity])
|
||||
|
||||
|
||||
def test_il_report_stampa_il_TWR_e_non_il_grezzo(monkeypatch, capsys):
|
||||
@@ -51,10 +47,10 @@ def test_il_report_stampa_il_TWR_e_non_il_grezzo(monkeypatch, capsys):
|
||||
con.close()
|
||||
_carica_script().report()
|
||||
out = capsys.readouterr().out
|
||||
assert "TWR : +3.95%" in out
|
||||
assert "+234.95%" not in out and "+235" not in out # il grezzo non compare piu'
|
||||
assert "movimenti capitale : +1,400.00 certi" in out
|
||||
assert "trading da arming : +9.70" in out
|
||||
assert "TWR" in out and "+3.95%" in out
|
||||
assert "+234.95%" not in out # il grezzo non compare piu'
|
||||
assert "+1,400.00 certi" in out
|
||||
assert "+9.70" in out
|
||||
assert "movimenti di capitale INCLUSI" in out # il delta $ resta, etichettato
|
||||
|
||||
|
||||
@@ -65,4 +61,33 @@ def test_il_report_senza_movimenti_dice_lo_stesso_numero_di_prima(monkeypatch, c
|
||||
con.close()
|
||||
_carica_script().report()
|
||||
out = capsys.readouterr().out
|
||||
assert "TWR : +5.00%" in out and "movimenti capitale" not in out
|
||||
assert "+5.00%" in out and "movimenti capitale" not in out
|
||||
|
||||
|
||||
def test_il_report_a_base_zero_dice_n_d_e_non_l_intero_conto(monkeypatch, capsys):
|
||||
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
|
||||
con = T.connect()
|
||||
_prepara(con, [("2026-08-24T09:47:01+00:00", 0.0),
|
||||
("2026-08-24T10:47:01+00:00", 598.06),
|
||||
("2026-08-25T10:47:01+00:00", 610.0)])
|
||||
con.close()
|
||||
_carica_script().report()
|
||||
out = capsys.readouterr().out
|
||||
assert out.count("n/d") >= 2 and "TWR" in out
|
||||
assert "+610.00 di equity" in out and "arming : +610.00" not in out
|
||||
|
||||
|
||||
def test_il_report_con_il_feed_assente_non_stampa_il_grezzo_come_TWR(monkeypatch, capsys):
|
||||
# revisione 02/09: con data/raw assente (host ripristinato prima di rebuild_history) ogni
|
||||
# salto era "ambiguo", certi=0, e il report stampava +243% sotto l'etichetta TWR
|
||||
monkeypatch.setattr(J, "_px_1h", lambda a: None)
|
||||
con = T.connect()
|
||||
_prepara(con, [("2026-08-24T10:47:01+00:00", 600.0),
|
||||
("2026-08-25T11:47:01+00:00", 2000.0),
|
||||
("2026-08-26T10:47:01+00:00", 2020.0)])
|
||||
con.close()
|
||||
_carica_script().report()
|
||||
out = capsys.readouterr().out
|
||||
assert out.count("n/d") >= 2 and "TWR" in out
|
||||
assert "% =" not in out and "+236" not in out
|
||||
assert "[ambiguo: mercato non misurabile" in out
|
||||
|
||||
Reference in New Issue
Block a user