Compare commits
15 Commits
2f701b8469
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a4e942c8bb | |||
| 5b72a4b188 | |||
| a2b03aa2b6 | |||
| 37d1565a1e | |||
| f82f685528 | |||
| 79afe41ec2 | |||
| 8f2e6c5c2c | |||
| 0a2f780d3c | |||
| bb40e87d13 | |||
| 9f55652d99 | |||
| 95424f0755 | |||
| de83909db9 | |||
| 1279ea605a | |||
| 835e0c8666 | |||
| 861cc7fc27 |
@@ -1,423 +1,153 @@
|
|||||||
# PythagorasGoal
|
# 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
|
## Cos'è vivo, adesso
|
||||||
> 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`.
|
|
||||||
|
|
||||||
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) |
|
**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
|
||||||
| **FADE** | mean-reversion intraday 1h (long/short, BTC/ETH) | MR01 Bollinger, MR02 Donchian, MR07 Return-reversal | Acc 52-55%, DD 18-34% |
|
del progetto sono su quella, non su questa.
|
||||||
| **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 |
|
|
||||||
|
|
||||||
Tutti i numeri sono **netti** dopo fee realistiche (Deribit 0.10% RT single-leg, 0.20%
|
## I numeri, con la loro lente
|
||||||
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).
|
|
||||||
|
|
||||||
### 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
|
| grandezza | valore onesto |
|
||||||
portafoglio equipesato il drawdown crolla sotto quello di ogni singola sleeve:
|
|---|---|
|
||||||
|
| 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 |
|
⚠️ 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
|
||||||
| FADE (6 sleeve) | ~46% | 8% | 3.9 |
|
due saldi — è stato un difetto reale, riparato il 2026-09-02.
|
||||||
| 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) |
|
|
||||||
|
|
||||||
> 🔎 **Numeri sobri (anti-overfit).** L'OOS singolo cade nel regime favorevole 2024-25:
|
## La riga che ordina tutto il resto
|
||||||
> 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.
|
|
||||||
|
|
||||||
## 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
|
## Metodo — cosa deve superare una strategia nuova
|
||||||
di prezzo **rientrano verso la media** più di quanto proseguano:
|
|
||||||
|
|
||||||
1. **Bollinger Bands** (window `n`, `k` deviazioni standard) sul close.
|
Sei requisiti, nessuno negoziabile (`CLAUDE.md` §8, gate in `scripts/research/alt/altlib.py`):
|
||||||
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`.
|
|
||||||
|
|
||||||
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;
|
## Il dato
|
||||||
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.
|
|
||||||
|
|
||||||
### 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à
|
## Struttura
|
||||||
(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
|
|
||||||
|
|
||||||
```
|
```
|
||||||
PythagorasGoal/
|
src/
|
||||||
├── src/
|
data/downloader.py load_data(asset, tf) sui parquet certificati
|
||||||
│ ├── data/ # Download e gestione dati (Cerbero MCP + Binance)
|
strategies/ trend_portfolio.py (TP01) · skyhook.py (SKH01) · base · indicators
|
||||||
│ ├── fractal/ # Indicatori frattali: Hurst, Higuchi FD, self-similarity
|
portfolio/ portfolio.py (N sleeve + weights_tilt_null) · sleeves.py · gtaa.py
|
||||||
│ ├── backtest/ # Motore di backtesting con fee e metriche
|
backtest/harness.py backtest onesto, senza look-ahead
|
||||||
│ ├── strategies/ # Classe base Strategy ABC + indicatori condivisi
|
live/ book.py (esecutore netto) · deribit · livefeed · usde
|
||||||
│ │ ├── base.py # Strategy, Signal, BacktestResult, YearlyStats
|
venue_watch · venue_probe · venue_news · monitor_health · scale_watch
|
||||||
│ │ └── indicators.py # keltner_ratio, detect_squeezes, ema, atr, rv, corr
|
tradesdb · journal · analista · notifier · cli
|
||||||
│ ├── live/ # Paper trading live su Deribit testnet
|
scripts/
|
||||||
│ │ ├── multi_runner.py # Orchestratore multi-strategia (strategie + pairs)
|
live/ book_execute · trades_db · journal · analista · balance_watch
|
||||||
│ │ ├── strategy_worker.py # Worker single-leg con stato persistente
|
paper_* (forward-monitor) · usde_watch · usde_convert · fee_watch
|
||||||
│ │ ├── pairs_worker.py # Worker a 2 gambe per i pairs (market-neutral)
|
research/ r<data>_*.py — un file per esperimento, harness in alt/altlib.py
|
||||||
│ │ ├── strategy_loader.py # Import dinamico classi Strategy
|
analysis/ rebuild_history · certify_feed · audit_feed · multi_source_check
|
||||||
│ │ ├── cerbero_client.py # Client HTTP per Cerbero MCP
|
cron_{book,daily,chain,balance,usde,opt_snapshot,vol_term}.sh
|
||||||
│ │ ├── signal_engine.py # Squeeze + ML real-time (legacy) + validazione OOS
|
docs/
|
||||||
│ │ └── telegram_notifier.py
|
memory/ LA MEMORIA — 6 file, indicizzati in testa a CLAUDE.md
|
||||||
│ └── portfolio/ # Portafogli di prima classe (capitale condiviso, backtest + live)
|
research/ RESULTS-0822 (§1-77, un registro per filone) · BRIEF-0822 · SPEC-scale-key
|
||||||
│ ├── base.py # SleeveSpec, Portfolio (.backtest), load_active_portfolio
|
diary/ una voce per esperimento (144)
|
||||||
│ ├── weighting.py # Schemi di ponderazione: equal, cap, inverse_vol, cluster_rp, manual
|
journal/ libro di bordo, una voce al giorno, 4 livelli per provenienza
|
||||||
│ ├── sleeves.py # Builder unificato equity-per-sleeve (fonte unica, parità report)
|
tests/ 1008, tutti verdi
|
||||||
│ ├── ledger.py # PortfolioLedger: PnL/DD aggregati, persistenza e resume
|
Old/ archivio pre-reset — consultabile, non fidato
|
||||||
│ └── 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Strategie attive
|
## Comandi
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Backtest di una strategia
|
uv sync # dipendenze
|
||||||
uv run python scripts/strategies/MR01_bollinger_fade.py
|
uv run python scripts/analysis/rebuild_history.py --asset BTC ETH # storico da Deribit mainnet
|
||||||
uv run python scripts/strategies/PR01_pairs_reversion.py
|
uv run python scripts/analysis/certify_feed.py # certifica i feed
|
||||||
|
uv run python scripts/live/trades_db.py --report # stato del libro + TWR
|
||||||
# Ricerca e validazione fee-aware out-of-sample
|
uv run python scripts/live/journal.py # voce del giorno
|
||||||
uv run python scripts/analysis/strategy_research.py # screening famiglie + deep-dive fade
|
uv run python scripts/live/analista.py --secco # analisi del giorno, senza salvare
|
||||||
uv run python scripts/analysis/strategy_research_v2.py # MR02 / MR03 / MR07
|
uv run python scripts/portfolio/run_portfolio.py # portafoglio di ricerca
|
||||||
uv run python scripts/analysis/oos_validation.py # perche' la famiglia squeeze e' scartata
|
uv run pytest # 1008 test
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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,
|
## Gate aperti
|
||||||
ognuna con €1000 USDC virtuali indipendenti. Gestisce due tipi di worker:
|
|
||||||
|
|
||||||
- **Single-leg** (`strategy_worker.py`): per le strategie direzionali. Se un `Signal`
|
Un gate si decide **alla data**, coi criteri scritti **prima**. Elenco completo in `CLAUDE.md` §4.
|
||||||
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.
|
|
||||||
|
|
||||||
### 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
|
## Obiettivo, e cosa lo rende difficile
|
||||||
# Locale
|
|
||||||
uv run python -m src.live.multi_runner
|
|
||||||
|
|
||||||
# Docker
|
Il target dichiarato è **€50/giorno partendo da €1.000**. Non è raggiungibile a questo capitale:
|
||||||
docker compose up -d
|
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
|
**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
|
||||||
Le strategie attive sono definite in `strategies.yml`:
|
quella frase verificabile invece che dichiarata.
|
||||||
|
|
||||||
```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.
|
|
||||||
|
|||||||
+8
-5
@@ -10,14 +10,17 @@
|
|||||||
"max_data_age_days": 2,
|
"max_data_age_days": 2,
|
||||||
"_nota_skh_feed": "Freschezza del feed 5m usato per il segnale SKH01 (2026-07-26). fresh_5m ricade sul feed certificato IN SILENZIO se il fetch pubblico Deribit fallisce, e il certificato si rigenera 1x/giorno: senza controllo la latenza d'uscita di SKH01 passa da ~1h a ~1 giorno senza segnalazione. Sopra soglia book_execute ALLERTA e NON blocca (bloccare fermerebbe anche TP01, nettato sullo stesso strumento, per un guasto di rete). Diario 2026-07-26-t1-esecuzione-skh-live.md.",
|
"_nota_skh_feed": "Freschezza del feed 5m usato per il segnale SKH01 (2026-07-26). fresh_5m ricade sul feed certificato IN SILENZIO se il fetch pubblico Deribit fallisce, e il certificato si rigenera 1x/giorno: senza controllo la latenza d'uscita di SKH01 passa da ~1h a ~1 giorno senza segnalazione. Sopra soglia book_execute ALLERTA e NON blocca (bloccare fermerebbe anche TP01, nettato sullo stesso strumento, per un guasto di rete). Diario 2026-07-26-t1-esecuzione-skh-live.md.",
|
||||||
"skh_feed_max_age_min": 30,
|
"skh_feed_max_age_min": 30,
|
||||||
"_nota_usde": "Collaterale USDE a rendimento (test di eligibilita' 2026-08-26: 500 USDE, verdetto entro il 29/08 — diario 2026-08-26-usde-analisi). L'autorita' che LEGGE questa sezione e' src/live/usde.py; la usano shadow._collaterale_usde (equity oraria del book) e scripts/live/usde_watch.py (sorveglianza giornaliera 12:35 UTC). Criteri delle soglie, dichiarati (P6): depeg_warn 0.99 = fuori dalla banda operativa dello spot (~3 bps) e oltre il clamp +-0.5% per fonte dell'indice usde_usdc — a quel prezzo non e' rumore di book; depeg_crit 0.95 = meta' del buffer di haircut (10%) consumata. quota_target 0.70 = la quota DECISA dall'operatore il 2026-08-30 (la decisione di quota che il gate USDE-01 apriva; anticipata di un giorno sul rinvio al 31/08, su richiesta esplicita dell'operatore \"porta in usde tutto il capitale che non viene usato\"). Il 70% NON e' un argmax (M8): e' il massimo compatibile col CUSCINO DI REGOLAMENTO, cioe' il vincolo che r0830_usde_quota non aveva guardato — il P&L e il funding dei perp USDC-lineari si regolano in USDC, non nel collaterale, quindi a quota alta il saldo USDC va negativo alla prima perdita del libro e Deribit lo finanzia a interesse. Il cuscino richiesto e' il disaster-SL sulla massima esposizione lorda: n_asset x frac x disaster_sl_pct = 2 x 0.5 x 0.30 = 30% dell'equity, che lascia esattamente il 70%. quota_max_frac 0.50 = tetto di ALLERTA sulla quota USDE/equity totale (N4: la quota e' l'unica leva contro il rischio emittente, -100% = 25 anni di resa, non recuperabile). Alzato a 0.85 il 2026-08-30 in previsione della quota al 70% e RIMESSO A 0.50 lo stesso giorno: il 70% NON e' raggiungibile. 🚨 venue_cap_frac 0.318 = TETTO DEL VENUE sull'USDE, misurato e NON documentato da Deribit (ne' 'Cross collateral specifications' ne' 'Yield-generating collateral' prevedono un limite sulle quantita' detenibili; il Cap ETHENA che esiste diluisce il TASSO a livello di exchange, non limita gli acquisti). Ogni acquisto oltre il tetto e' rifiutato con `not_enough_funds_in_currency` pur avendo $1.400 disponibili: messaggio FUORVIANTE. E' una FRAZIONE dell'equity, non un livello: provato il 31/08 lasciando scendere l'equity di $8, il tetto e' sceso con lei. ⚠️ DIPENDE DAL MODELLO DI MARGINE, ma pochissimo — misurato a saldo neutro sotto entrambi: SEGREGATO S:SM [643,18-644,18) con equity $2.055,56 = 31,29-31,34%; CROSS X:SM [654,18-655,18) con equity $2.054,90 = 31,84-31,88%. Il passaggio a cross ha comprato +0,55pp = **+11 USDE (~$11)**: il modello entra nel tetto, ma non lo spiega. ⇒ la quota resta ~31-32% sotto qualunque configurazione e il 70% di quota_target NON e' raggiungibile (manca un fattore ~2,2x). Il valore 0.318 e' il bordo BASSO del bracket CROSS, che e' il modello attivo: fa fallire il piano PRIMA dell'ordine. Se si torna a S:SM va rimesso a 0.312. haircut 0.05 — ✅ DIVERGENZA CHIUSA il 2026-08-31 (era 0.10, SBAGLIATO). La pagina margini del conto non espone l'haircut come numero: si RICAVA per differenza fra le due righe, perche' il modello CROSS conta l'USDE scontato e il SEGREGATO non lo conta affatto. Contributo dell'USDE al cross $610,81 su $643,05 di valore -> 5,0137%: l'ipotesi 5% torna a $0,09, la 10% sbaglia di $32,06. Riproducibile: `scripts/research/r0831_margini_conto.py` (N11). Il 26/08 il 10% fu registrato come 'verificato sul venue' senza lasciare traccia di come, e non lo era. 🚨 SCOPERTO NELLA STESSA LETTURA, e vale piu' dell'haircut: il modello di margine ATTIVO e' **Segregated: Standard Margin (S:SM)**, NON cross-collateral. Nella tabella del modello attivo l'USDE **non compare**: non fa margine per i perp USDC-settled del book. Percio' l'haircut oggi non si applica affatto — ed e' esattamente il motivo per cui la misura del 31/08 trovava $0,0000 accantonati. Passare a X:SM aggiungerebbe $610,87 di margine utilizzabile: e' una decisione dell'operatore, non un refactor, e porta con se' la meccanica cross (collateral fee 0,05%/giorno sul saldo negativo, ribilanciamento automatico).",
|
"_nota_usde": "Collaterale USDE a rendimento (test di eligibilita' 2026-08-26: 500 USDE, verdetto entro il 29/08 — diario 2026-08-26-usde-analisi). L'autorita' che LEGGE questa sezione e' src/live/usde.py; la usano shadow._collaterale_usde (equity oraria del book) e scripts/live/usde_watch.py (sorveglianza giornaliera 12:35 UTC). Criteri delle soglie, dichiarati (P6): depeg_warn 0.99 = fuori dalla banda operativa dello spot (~3 bps) e oltre il clamp +-0.5% per fonte dell'indice usde_usdc — a quel prezzo non e' rumore di book; depeg_crit 0.95 = meta' del buffer di haircut (10%) consumata. quota_target 0.70 (CHIAVE TOLTA il 2026-09-10, issue #7: il bersaglio operativo e' DERIVATO dalle bande del cuscino, vedi usde._nota_cuscino — quanto segue e' la storia della decisione) = la quota DECISA dall'operatore il 2026-08-30 (la decisione di quota che il gate USDE-01 apriva; anticipata di un giorno sul rinvio al 31/08, su richiesta esplicita dell'operatore \"porta in usde tutto il capitale che non viene usato\"). Il 70% NON e' un argmax (M8): e' il massimo compatibile col CUSCINO DI REGOLAMENTO, cioe' il vincolo che r0830_usde_quota non aveva guardato — il P&L e il funding dei perp USDC-lineari si regolano in USDC, non nel collaterale, quindi a quota alta il saldo USDC va negativo alla prima perdita del libro e Deribit lo finanzia a interesse. Il cuscino richiesto e' il disaster-SL sulla massima esposizione lorda: n_asset x frac x disaster_sl_pct = 2 x 0.5 x 0.30 = 30% dell'equity, che lascia esattamente il 70%. quota_max_frac 0.50 = tetto di ALLERTA sulla quota USDE/equity totale (N4: la quota e' l'unica leva contro il rischio emittente, -100% = 25 anni di resa, non recuperabile). Alzato a 0.85 il 2026-08-30 in previsione della quota al 70% e RIMESSO A 0.50 lo stesso giorno perche' il 70% pareva irraggiungibile; RIMESSO A 0.85 il 2026-09-06, quando la quota e' arrivata al 68,7% su autorizzazione dell'operatore e la sonda non ha trovato tetto. 🚨 venue_cap_frac: il 06/09 il tetto NON C'ERA (null, vedi venue_cap_misurato) — quanto segue e' la storia del 30-31/08, tenuta perche' il tetto puo' tornare: venue_cap_frac 0.318 = TETTO DEL VENUE sull'USDE, misurato e NON documentato da Deribit (ne' 'Cross collateral specifications' ne' 'Yield-generating collateral' prevedono un limite sulle quantita' detenibili; il Cap ETHENA che esiste diluisce il TASSO a livello di exchange, non limita gli acquisti). Ogni acquisto oltre il tetto e' rifiutato con `not_enough_funds_in_currency` pur avendo $1.400 disponibili: messaggio FUORVIANTE. E' una FRAZIONE dell'equity, non un livello: provato il 31/08 lasciando scendere l'equity di $8, il tetto e' sceso con lei. ⚠️ DIPENDE DAL MODELLO DI MARGINE, ma pochissimo — misurato a saldo neutro sotto entrambi: SEGREGATO S:SM [643,18-644,18) con equity $2.055,56 = 31,29-31,34%; CROSS X:SM [654,18-655,18) con equity $2.054,90 = 31,84-31,88%. Il passaggio a cross ha comprato +0,55pp = **+11 USDE (~$11)**: il modello entra nel tetto, ma non lo spiega. ⇒ la quota resta ~31-32% sotto qualunque configurazione e il 70% di quota_target NON e' raggiungibile (manca un fattore ~2,2x). Il valore 0.318 e' il bordo BASSO del bracket CROSS, che e' il modello attivo: fa fallire il piano PRIMA dell'ordine. Se si torna a S:SM va rimesso a 0.312. haircut 0.05 — ✅ DIVERGENZA CHIUSA il 2026-08-31 (era 0.10, SBAGLIATO). La pagina margini del conto non espone l'haircut come numero: si RICAVA per differenza fra le due righe, perche' il modello CROSS conta l'USDE scontato e il SEGREGATO non lo conta affatto. Contributo dell'USDE al cross $610,81 su $643,05 di valore -> 5,0137%: l'ipotesi 5% torna a $0,09, la 10% sbaglia di $32,06. Riproducibile: `scripts/research/r0831_margini_conto.py` (N11). Il 26/08 il 10% fu registrato come 'verificato sul venue' senza lasciare traccia di come, e non lo era. 🚨 SCOPERTO NELLA STESSA LETTURA, e vale piu' dell'haircut: il modello di margine ATTIVO e' **Segregated: Standard Margin (S:SM)**, NON cross-collateral. Nella tabella del modello attivo l'USDE **non compare**: non fa margine per i perp USDC-settled del book. Percio' l'haircut oggi non si applica affatto — ed e' esattamente il motivo per cui la misura del 31/08 trovava $0,0000 accantonati. Passare a X:SM aggiungerebbe $610,87 di margine utilizzabile: e' una decisione dell'operatore, non un refactor, e porta con se' la meccanica cross (collateral fee 0,05%/giorno sul saldo negativo, ribilanciamento automatico).",
|
||||||
"usde": {
|
"usde": {
|
||||||
"index_name": "usde_usdc",
|
"index_name": "usde_usdc",
|
||||||
"haircut": 0.05,
|
"haircut": 0.05,
|
||||||
"quota_target": 0.7,
|
"_nota_cuscino": "Dal 2026-09-10 (issue #7) la quota USDE NON e' un numero scelto: e' DERIVATA dal cuscino USDC di regolamento (n_asset x frac x disaster_sl_pct = 30% dell'equity) e da queste tre frazioni del cuscino, lette da src/live/usde.py per scripts/live/cuscino_watch.py (cron :53). cuscino_margine_frac 0.20 = bersaglio unico di ogni conversione: quota = 1 - 0.30 x 1.20 = 0.64 (il vecchio quota_target 0.70 lasciava slack ZERO: ogni ora in perdita avrebbe venduto). cuscino_preavviso_frac 0.10 = sotto, allerta. cuscino_riacquisto_frac 0.40 = sopra, si RICOMPRA USDE fino al bersaglio (un bonifico alza lo slack di 0,7 x importo e fa scattare il riacquisto da solo). Isteresi [0 ; 0.40] x cuscino con bersaglio a 0.20: oscillare richiede +-6% di equity di slack, cioe' +-8,6% di equity USDC (slack = 0,7 USDC - 0,3 USDE). Soglie DICHIARATE, non ottimizzate (M8): un giro costa ~6 bps sull'importo mosso. Ordine obbligato: 0 < preavviso < margine < riacquisto (usde.bande_cuscino lo verifica).",
|
||||||
"venue_cap_frac": 0.318,
|
"cuscino_preavviso_frac": 0.1,
|
||||||
"venue_cap_misurato": "2026-08-31 (X:SM)",
|
"cuscino_margine_frac": 0.2,
|
||||||
"quota_max_frac": 0.5,
|
"cuscino_riacquisto_frac": 0.4,
|
||||||
|
"venue_cap_frac": null,
|
||||||
|
"venue_cap_misurato": "2026-09-06: NESSUN tetto fino al 68,7% (fermata dal cuscino di regolamento al 70%, non dal venue): +2.434 USDE in 39 ordini a saldo crescente, zero rifiuti (r0906_usde_tetto_sonda.py). Il 30-31/08 il tetto c'era: [643-644) e [654-655) USDE su ~$2.055 = 31,3-31,9%. Non spiegato: e' SPARITO, non e' scalato. null = nessun tetto noto; usde_convert si affida al chunk+backoff sui rifiuti",
|
||||||
|
"quota_max_frac": 0.85,
|
||||||
"depeg_warn": 0.99,
|
"depeg_warn": 0.99,
|
||||||
"depeg_crit": 0.95
|
"depeg_crit": 0.95
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# 2026-09-02 — Debito 14: il report stampa il TWR, non il bonifico
|
# 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`
|
*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.*
|
||||||
(1.678 letture) e dal report rilanciato dopo la riparazione.*
|
|
||||||
|
|
||||||
## Cosa c'era
|
## 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.
|
dollari e la sua pagina ha una forma testata. Aggiungerlo è una riga, ma non era il debito.
|
||||||
- Non ho scomposto lo scarto (~$25) fra round-trip + non realizzato e il trading al netto: era già
|
- Non ho scomposto lo scarto (~$25) fra round-trip + non realizzato e il trading al netto: era già
|
||||||
fuori scope il 01/09 e lo resta.
|
fuori scope il 01/09 e lo resta.
|
||||||
|
|
||||||
|
## Revisione del codice, 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.
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# 2026-09-06 — USDE: dal 14,6% al 68,7%. Il tetto del venue del 30-31/08 non c'era piu'; il versamento di prova del 25/08 dichiarato
|
||||||
|
|
||||||
|
**Ore:** 20:56-21:16Z. **Autorizzazioni dell'operatore, in sessione:** *«usa piu' USDE se ne hai bisogno»*,
|
||||||
|
poi *«cerca % massima raggiungibile in USDE»*, sulla decisione gia' presa il 30/08 (quota bersaglio 70%,
|
||||||
|
`usde.quota_target`). E, a meta' sessione: *«i 25 euro erano un versamento di prova»*.
|
||||||
|
|
||||||
|
## 1. Il versamento del 04/09 aveva dimezzato la quota
|
||||||
|
|
||||||
|
Il versamento di **$2.410,14 del 04/09 14:47Z** (riconosciuto dal classificatore: mercato max 0,56% a leva
|
||||||
|
piena contro un salto del +116,8%) ha portato l'equity a **~$4.495** e la quota USDE da 31,3% a **14,6%**.
|
||||||
|
Nessun automatismo la riportava su.
|
||||||
|
|
||||||
|
## 2. Primo passo: al tetto di config (31,8%) — e la lettura SBAGLIATA che ne ho dato
|
||||||
|
|
||||||
|
`usde_convert.py --quota 0.318 --esegui`: 774 USDE in 8 ordini @ 1,0005 medio, zero rifiuti, quota 31,79%.
|
||||||
|
Ho scritto in CLAUDE.md «il tetto ha tenuto la stessa frazione a equity 2,2× maggiore: terza conferma».
|
||||||
|
**Falso**: nessun rifiuto fino al 31,8% dimostra che il tetto e' **≥** 31,8%, non che e' **=** 31,8%. Il
|
||||||
|
dato non conteneva la conferma; l'ho letta perche' me l'aspettavo. Corretto venti minuti dopo, quando
|
||||||
|
l'operatore ha chiesto la % massima e la sonda ha mostrato che la conferma non c'era.
|
||||||
|
|
||||||
|
## 3. La sonda: nessun tetto fino al 68,7%
|
||||||
|
|
||||||
|
`scripts/research/r0906_usde_tetto_sonda.py` — a saldo crescente, passi piccoli, stesso metodo dei diari
|
||||||
|
30-31/08 (un probe unico di taglia sbagliata produce un falso negativo): sale finche' il venue non rifiuta,
|
||||||
|
ripete il rifiuto dopo aver riletto il book (il messaggio `not_enough_funds_in_currency` e' lo stesso di un
|
||||||
|
limite che non incrocia), scende di passo a ogni doppio rifiuto, e conferma il muro a saldo neutro
|
||||||
|
(SELL 1 → BUY 1 ok → BUY 1 rifiutato). Guardie: prezzo dal book (ask+5 tick, tetto 1,0010), quota mai oltre
|
||||||
|
il 70% e **USDC mai sotto il cuscino di regolamento** derivato da config (`usde_convert.cuscino_richiesto_usd`).
|
||||||
|
|
||||||
|
| corsa | passi | da → a (USDE) | quota | rifiuti |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 21:02-21:09 | 30 × 2 | 1.428,7 → 1.488,7 | 31,8% → 33,1% | **0** (fermata dal mio limite +60) |
|
||||||
|
| 21:11-21:15 | 16 × 100 | 1.488,7 → **3.088,7** | 33,1% → **68,7%** | **0** (fermata dal cuscino: +100 avrebbe superato il 70%) |
|
||||||
|
|
||||||
|
**Totale del giorno: 2.434 USDE in 39 ordini @ 1,0005, ~$1,2 di spread.** Conto finale: USDC **$1.407,40** +
|
||||||
|
USDE **3.088,72** = $4.496; cuscino richiesto $1.349 → slack **+$58**. Registro: `data/live/usde_convert.jsonl`
|
||||||
|
(due record `sonda_tetto`, con ogni ordine).
|
||||||
|
|
||||||
|
## 4. Cosa dice il risultato — e cosa non dice
|
||||||
|
|
||||||
|
1. **Il tetto del 30-31/08 era vero allora ed e' sparito ora.** Tre bracket riproducibili a 1 USDE (31,21-31,26% ·
|
||||||
|
31,29-31,34% · 31,84-31,88%) contro 2.434 USDE accettati oggi senza un rifiuto. **Non e' scalato, e'
|
||||||
|
sparito**: la frase del 31/08 «il 70% non e' raggiungibile ne' ora ne' mai» e' falsificata.
|
||||||
|
2. **Non spiegato** (D5). Ipotesi non verificabili da qui (il gateway non espone il transaction log ne' i
|
||||||
|
parametri del cap): un'allocazione per-conto del Cap ETHENA che varia col saldo aggregato dell'exchange;
|
||||||
|
un vincolo sui fondi depositati di recente (ma l'USDC del 04/09 e' stato convertibile a 2 giorni); un
|
||||||
|
cambio di policy. **Il tetto puo' tornare**: quando tornera', bloccherebbe gli ACQUISTI, mai le vendite
|
||||||
|
(tutte le SELL sono sempre passate). `venue_cap_frac` → **null** con la misura in `venue_cap_misurato`;
|
||||||
|
`usde_convert` si affida al chunk+backoff sui rifiuti, che e' cio' che ha sempre fatto.
|
||||||
|
3. **Il limite che morde e' NOSTRO: il cuscino di regolamento.** P&L e funding dei perp si regolano in USDC;
|
||||||
|
a 68,7% restano $58 sopra il cuscino. ⚠️ **Nessuno sorveglia il cuscino**: `usde_watch` allerta sulla
|
||||||
|
quota (`quota_max_frac`, riportata a **0,85**, il valore che l'operatore aveva scelto il 30/08 «in previsione
|
||||||
|
della quota al 70%»), non sull'USDC. Una perdita del libro fa salire la quota da sola: a −$58 il cuscino e'
|
||||||
|
scoperto, e Deribit finanzia un saldo USDC negativo allo 0,05%/giorno (18%/anno). Debito nuovo, §5.17.
|
||||||
|
4. **Rischio emittente**: a 24,2% l'evento Ethena valeva 3,1× il maxDD del libro (30/08); a 68,7% vale **~8,8×**.
|
||||||
|
E' la decisione del 30/08 presa con l'informazione completa (r0830_usde_quota), oggi eseguita. Resa attesa
|
||||||
|
~4,1% × $3.089 ≈ **$127/anno** (era $26): ancora meno di un mese di versamento.
|
||||||
|
|
||||||
|
## 5. Il versamento di prova del 25/08: dichiarato, scorporato, e il TWR scende di 4 punti
|
||||||
|
|
||||||
|
Trovato stamattina per differenza (equity 07:07→08:07 **+$21,11** con le posizioni a **−$3,3/−4,3**) e
|
||||||
|
confermato dall'operatore: **€25 di prova** prima del bonifico da $1.399,39 delle 11:47. Il rilevatore non
|
||||||
|
poteva vederlo (+3,3% contro soglia 10%) e abbassare la soglia farebbe di ogni ora di mercato un candidato:
|
||||||
|
**l'unica fonte che sa l'importo e' l'operatore**. Costruito il canale:
|
||||||
|
|
||||||
|
- `data/live/movimenti_dichiarati.jsonl` (append-only, nel perimetro di backup — P11), letto da
|
||||||
|
`journal.movimenti_dichiarati()`; `movimenti_capitale` lo fonde coi rilevati: fonte `dichiarato`, importo
|
||||||
|
dell'operatore (non il salto), `dopo = prima + importo` cosi' il mercato dell'ora **resta** nel rendimento;
|
||||||
|
se coincide con un salto rilevato l'importo dichiarato vince (`dichiarato+rilevato`); fuori dalle letture →
|
||||||
|
`avvisi`, non applicato; riga rotta → `avvisi`, le altre restano (P3).
|
||||||
|
- Importo registrato **$24,90 ± 0,50**: stimato per differenza (salto +21,11, mercato −3,3/−4,3 dal log del
|
||||||
|
book, fee 0,03) — il gateway non espone `get_deposits`, quindi il numero USDC esatto non e' leggibile; la
|
||||||
|
banda sta nel record e nel report (P12).
|
||||||
|
- `tests/test_journal.py`: +6 (scorporo sotto soglia · dichiarato+rilevato · fuori letture · riga rotta ·
|
||||||
|
senza file niente cambia · la nota di giornale lo dice); `conftest` isola il file nei test.
|
||||||
|
|
||||||
|
| | prima (20:47Z) | dopo (21:14Z) |
|
||||||
|
|---|---|---|
|
||||||
|
| movimenti certi | $3.809,53 | **$3.834,43** |
|
||||||
|
| trading da arming | +$87,35 | **+$62,45** |
|
||||||
|
| TWR | +11,98% | **+7,83%** = +8,14% × −0,62% × −0,12% × +0,46% |
|
||||||
|
|
||||||
|
**Un terzo del «trading» era un bonifico da 25 euro.** Regola nuova (CLAUDE.md §2): ogni versamento, anche
|
||||||
|
di prova, si dichiara nel file il giorno stesso.
|
||||||
|
|
||||||
|
## 6. File toccati
|
||||||
|
|
||||||
|
`config/live.json` (usde: venue_cap_frac null, quota_max_frac 0.85, nota) · `src/live/journal.py` ·
|
||||||
|
`scripts/live/trades_db.py` · `tests/conftest.py` · `tests/test_journal.py` (+6) ·
|
||||||
|
`scripts/research/r0906_usde_tetto_sonda.py` (nuovo) · `data/live/movimenti_dichiarati.jsonl` (nuovo) ·
|
||||||
|
`CLAUDE.md` §1 §2 §4 §5.
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
# 2026-09-09 — Guadagnare nei crolli: revisione dei sistemi, il crollo catturato, piu' monete su Deribit
|
||||||
|
|
||||||
|
**Richiesta dell'operatore:** *"fai analisi e revisione dei sistemi adottati e trova soluzione per
|
||||||
|
guadagnare anche nei crolli (usa le opzioni se serve), anche + monete ma sempre in deribit"*.
|
||||||
|
Analisi e revisione col modello `fable` (revisore fresco, agente separato), test con Opus.
|
||||||
|
|
||||||
|
**Esito in una riga:** nel **giorno** del crollo il libro live **perde** (−0,48%/g, positivo nel 18%
|
||||||
|
dei giorni ≤ −5%); per **finestra** e' immune-o-positivo e **dal 2022 chiude in utile 8/8 peggiori
|
||||||
|
finestre**, tutte per la gamba SHORT di SKH01 marcata all'uscita; la sola leva che aumenterebbe quel
|
||||||
|
guadagno e' il peso di SKH01, che il gate del progetto boccia su una condizione in-sample; le
|
||||||
|
opzioni, misurate per la prima volta su un **crollo catturato a quote vere**, danno un rapporto di
|
||||||
|
credito identico al rally (f_net 0,74 vs 0,714) — **non** il `f` di stress che la regola del 19/06
|
||||||
|
aspettava (crollo a vol bassa, gate di VRP01 chiuso, famiglia inverse); XRP, l'unica "altra
|
||||||
|
moneta" liquida e non misurata di Deribit, **diluisce** esattamente come SOL. **Libro, pesi,
|
||||||
|
config, cron INVARIATI. Nessun ordine.**
|
||||||
|
|
||||||
|
## 0. Prima di misurare: cosa era gia' morto (memoria letta, non riaperta)
|
||||||
|
|
||||||
|
| meccanismo "guadagnare nei crolli" | esito | perche' e' morto (chi riapre deve battere questo) |
|
||||||
|
|---|---|---|
|
||||||
|
| gamba short del trend a 1d (§42) | SCARTATO | −1,91%/anno di drift in 0/24 ancore; il 2022 lo tiene a galla a meta' |
|
||||||
|
| put deep-OTM statica (§46) | REFUTATO 0/162 | beta del libro +0,076: non si assicura un libro gia' piatto |
|
||||||
|
| collar ≤15g (§71) · pavimento come licenza (§72) | REFUTATI | il DD lo riduce il TETTO (troncatura, C9); la put sola peggiora il DD 16/16 |
|
||||||
|
| gamma scalping long-vol | SCARTATO | paga il VRP ogni anno, ogni frequenza |
|
||||||
|
| DVOL direzionale · gate macro · skew de-risk | HEDGE / RIDONDANTI | TP01 e' gia' flat nei crolli (Δ 0,00) |
|
||||||
|
| SOL terza gamba (22/08) | ESCLUSO dall'operatore | hold-out −0,166 in 0/24; un anno solo (2023) |
|
||||||
|
| XS01 su Deribit (§50) | MORTO per ampiezza | 11 gambe con ≥1 anno: 1,265 → 0,116 |
|
||||||
|
|
||||||
|
Condizione di riapertura scritta tre volte (§3 short-vol, §71, §72): **un `f` di stress reale su
|
||||||
|
un crash catturato**. E' la cosa che oggi si poteva misurare e non era stata misurata.
|
||||||
|
|
||||||
|
## 1. Il libro nei crolli — `r0909_libro_nei_crolli.py`
|
||||||
|
|
||||||
|
Lente: sleeve di ricerca, ancora canonica, fee 0,10% RT, funding fuori; pesi importati da
|
||||||
|
`src/live/book` (P1). ⚠️ **SKH01 e' marcato all'USCITA del trade** (`harness.backtest_signals`):
|
||||||
|
la serie giornaliera e' zero nell'87,7% dei giorni, e uno short aperto nella finestra e chiuso
|
||||||
|
fuori e' accreditato fuori — per questo la tabella per GIORNO mostra SKH01 ≈ 0 e quella per
|
||||||
|
FINESTRA il guadagno. Indice 50/50 BTC/ETH; giorno di crollo ≤ −5%; episodio = 12 peggiori
|
||||||
|
finestre di 20 giorni non sovrapposte + 9 episodi nominati prima. Classi ESCLUSIVE nell'ordine
|
||||||
|
dichiarato: immune (|libro| < |idx|/10, col segno) → POSITIVO → PERDE.
|
||||||
|
|
||||||
|
| misura | valore |
|
||||||
|
|---|---|
|
||||||
|
| beta del libro all'indice | **+0,0769 sulla finestra di §46** (2021-03-24+; memoria +0,076: riprodotto al millesimo) · +0,086 su tutta la storia · +0,002 nei giorni ≤ −3% |
|
||||||
|
| 160 giorni ≤ −5% (indice −7,76%/g) | libro **−0,48%/g**: TP01 −0,63, SKH01 +0,15; **libro > 0 nel 18%** dei giorni; somme per anno: 2024 −11,9%, 2025 −7,3%, 2022 +8,2%, 2026 +4,7% |
|
||||||
|
| 24 giorni ≤ −10% (indice −14,4%/g) | libro −0,51%/g: TP01 −0,93, SKH01 **+0,42** |
|
||||||
|
| 12 peggiori finestre 20g | **4 POSITIVO / 6 immune (4 col segno +) / 2 PERDE**; libro > 0 in 8/12; **dal 2022 in 8/8** (mediana +3,3% contro indice −29%); le 2 perdite sono 2019 e maggio 2021 (TP01 ancora long: il trend non si era girato) |
|
||||||
|
| 9 episodi nominati | libro > 0 in LUNA (+4,3%), giugno 2022 (+3,1%), FTX (+0,7%), agosto 2024 (+0,4%), 5/02/2026 (+4,4%), 1-5/06/2026 (+3,3%); COVID −0,04%, feb 2025 −1,5%, maggio 2021 −3,4% |
|
||||||
|
| da dove viene il guadagno | la gamba **SHORT di SKH01 = 108%** dei guadagni negli episodi con libro > 0 (scomposizione ESATTA: long + short == intera su 2735/2735 giorni, verificato con assert; TP01 contribuisce in negativo); per anno al peso 0,25: 2022 **+10,7%**, 2026 **+7,7%** |
|
||||||
|
| peggior giorno del libro (lente canonica) | 2025-10-10 −3,38%, tutto TP01 con esposizione **0,505**, indice −9,7%: **e' un giorno di crollo con TP01 mezzo long** (lo squeeze di §33 sta sulla lente hourly e qui non si riproduce: dichiarato nel docstring) |
|
||||||
|
| riga informativa 37,5/62,5 (§26) | batte il 75/25 in 12/12 episodi, **ma** Sharpe 1,81 vs 1,82 e maxDD 12,1% vs 9,4%: paga i crolli con gli squeeze |
|
||||||
|
|
||||||
|
🚨 **Errore catturato in revisione (fable):** la prima stesura provava "GUADAGNA" prima di
|
||||||
|
"immune" e stampava **8/12 GUADAGNA**; con le classi nell'ordine dichiarato sono **4 POSITIVO +
|
||||||
|
6 immune**. *L'ordine delle regole era il verdetto.* Ora il verdetto e' tre affermazioni separate
|
||||||
|
(giorno / finestra / provenienza) e il conteggio nudo libro > 0 e' stampato accanto.
|
||||||
|
|
||||||
|
📌 **Il meccanismo "guadagnare nei crolli" esiste ed e' in produzione: e' SKH01 short** — ma
|
||||||
|
guadagna **dopo** il giorno del crollo (marcato all'uscita), non dentro. Nel senso stretto della
|
||||||
|
domanda dell'operatore il libro **perde nel giorno** e recupera nella finestra. Le due perdite
|
||||||
|
storiche per finestra sono di TP01 nei crolli che arrivano *dentro* un trend rialzista (2019,
|
||||||
|
2021): e' il costo del long-flat (§42 ha misurato che la short del trend lo aggrava).
|
||||||
|
|
||||||
|
**La leva che aumenterebbe il guadagno e' il peso di SKH01, e il gate la boccia:**
|
||||||
|
`r0726_reeval_live_weight.py` rilanciato: argmax 0,35, plateau [0,25-0,50] (il live e' dentro),
|
||||||
|
`weights_tilt_null` 0,25→0,35: `delta_insample −0,0026`, `pctl_hold 31,2`, **`gate_pass False`**.
|
||||||
|
⚠️ Precisazione della revisione: lo script legge la cache `data/_cache/skh_sigtab/*` **ferma al
|
||||||
|
2026-07-25** — sono le stesse cifre del diario 26/07, non un ri-esame coi segnali di oggi. La
|
||||||
|
condizione che fallisce e' pero' **in-sample (pre-2025)**, che 46 giorni di dati nuovi non possono
|
||||||
|
cambiare: la decisione 75/25 resta vincolante e la sua condizione di riapertura resta non
|
||||||
|
soddisfatta, ma la data da citare e' il 26/07.
|
||||||
|
|
||||||
|
## 2. Il crollo catturato — `r0909_crash_catturato.py`
|
||||||
|
|
||||||
|
**Fatto nuovo:** `bite_archive.parquet` copre con bid E ask il crollo del **1-5 giugno 2026**
|
||||||
|
(BTC −25% / ETH −30% dal 31/05 al minimo; ETH −11,1% il 05/06) su una scadenza (19/06, 14-21
|
||||||
|
DTE). Il campione del 22/08 (§11) partiva **dopo**, dal fondo. Limiti dichiarati prima: n = 1
|
||||||
|
crollo, 1 scadenza, tenor 14-21 g (VRP01 e' a 7), spot dal feed 1h certificato, regolamento allo
|
||||||
|
spot dell'ora di scadenza, **famiglia INVERSE** (premi in ETH: VRP01 opererebbe sulla USDC-lineare,
|
||||||
|
che il conto margina e che NON e' nell'archivio). Ancora d'ingresso = ogni snapshot 30/05-04/06;
|
||||||
|
mediana e banda.
|
||||||
|
|
||||||
|
⚠️ **Non e' un crollo a vol alta:** DVOL pre 37 (BTC) / 49 (ETH) all'IV-rank 0,05 / 0,10; il
|
||||||
|
**picco** del crollo (49 / 69) sta al **29° / 46° percentile storico** — sotto la mediana. Il gate
|
||||||
|
IV-rank ≥ 0,30 di VRP01 e' **chiuso in tutte le ore** e il modello del sleeve segna 0,0 su tutte le
|
||||||
|
settimane di giugno: VRP01 non sarebbe stato a mercato.
|
||||||
|
|
||||||
|
| ETH, ingressi PRE-crollo (286 snapshot = **4 strutture** di strike, δ −0,27/−0,09, larghezza 200) | reale | modello (BS su DVOL) | f |
|
||||||
|
|---|---|---|---|
|
||||||
|
| credito del put credit spread | | | **0,74** [0,65-0,82] — 22/08 in rally: 0,714 |
|
||||||
|
| P&L a scadenza per unita' | **−$174** | −$168 | 1,04 — **tautologico nell'83%** dei trade: entrambe le gambe ITM ⇒ payoff = larghezza per reale e modello, resta solo f_net |
|
||||||
|
| perdita sul rischio (fee incluse) | 101% [65-102] | | = perdita **MASSIMA** dello spread (+ fee); 65% per la 1900/1600 |
|
||||||
|
| peggior MTM, chiusura **tagliata alla larghezza** | **−$172** | −$161 | **1,08** [1,07-1,31]; a quell'ora IV corta 68 = DVOL 68 |
|
||||||
|
| put δ−0,10 comprata all'ask: costo | | | **1,92×** il modello (§46 a δ−0,10 su BTC_USDC: 1,27 — punto NUOVO e piu' alto, ETH inverse) |
|
||||||
|
| put δ−0,10 a scadenza (ITM di $4: K 1700, ST 1694) | −$6 | ≈ $0 | 0,96 sul solo sottoinsieme con payoff modellato ≥ $5 |
|
||||||
|
| put δ−0,10 al picco del percorso (look-ahead, limite sup.) | +$163 | +$173 | 0,94 |
|
||||||
|
| debit spread −0,28/−0,10 a scadenza | +$169 | +$163 | 1,04 (stessa tautologia, specchio) |
|
||||||
|
| ingressi DURANTE il crollo (02-04/06, 283 snapshot) | mediana +$5, **perde nel 46%**, p10 −$133 / p90 +$26 | | bimodale: la mediana nasconde le perdite |
|
||||||
|
|
||||||
|
BTC: **non misurabile pre-crollo** — la griglia di strike dell'archivio il 30/05-01/06 non
|
||||||
|
contiene la δ−0,10 (78 snapshot fuori geometria, 0 in geometria prima del 02/06). Durante:
|
||||||
|
247 snapshot, f_net 0,79, perde nel 37% (p10 −$1.718 / p90 +$818 su larghezza 5000).
|
||||||
|
|
||||||
|
🚨 **Tre correzioni della revisione, applicate:**
|
||||||
|
1. *"Il percorso costa 2,6× il modello (f_mtm 2,58), la IV della gamba corta e' esplosa a 108 per lo
|
||||||
|
skew"* — **artefatto di quote.** Il costo di chiusura `(ask corta − bid lunga)` superava la
|
||||||
|
**larghezza** dello spread (una quota 0,208/0,376 ETH su un intrinseco di $296: 158% di spread
|
||||||
|
relativo, a T→0) e `min()` pescava li'; uno spread a rischio definito non costa mai piu' della
|
||||||
|
larghezza per chiudere. Tagliato: **f_mtm 1,08**. E la IV 108 e' del **18/06 alle 17:00** (deep
|
||||||
|
ITM, T→0), non del crollo: all'ora del peggior MTM la IV e' 68 = DVOL.
|
||||||
|
2. *"f a scadenza 1,04: il modello prezza bene la perdita"* — **tautologia**: con entrambe le gambe
|
||||||
|
ITM il payoff e' la larghezza per entrambi; f_exp e' una funzione di f_net. E la "banda su 286
|
||||||
|
snapshot" sono 4 strutture (N_eff = 1 crollo × 4).
|
||||||
|
3. `S.asof(ts)` sul feed 1h **guardava un'ora avanti**: la barra e' etichettata all'apertura (la
|
||||||
|
20:00 chiude col 5m delle 20:55). Corretto con `spot_causale` (indice +1h). ⚠️ **`cblib.spot_series`
|
||||||
|
ha lo stesso difetto** → il f 0,714 di §11 e ogni numero di `cblib` con `asof` sullo spot; qui
|
||||||
|
innocuo (ambo le gambe ITM per ogni ST < 1700) e nel MTM giocava contro l'autore. **Debito nuovo,
|
||||||
|
registrato in CLAUDE.md §5.** Non toccato: cambierebbe numeri pubblicati.
|
||||||
|
|
||||||
|
📌 **Cosa dice davvero il crollo catturato (n = 1):** il **rapporto di credito reale/modello non
|
||||||
|
cambia col regime** (0,74 nel crollo contro 0,714 nel rally); la put comprata paga nel crollo
|
||||||
|
cio' che il modello dice (0,94-1,04) ma **costa 1,9× il modello all'ingresso** — il bleed e' dove
|
||||||
|
il modello sbaglia, il payoff no (e' il motivo di §46/§72 visto dal lato del sinistro); il gate di
|
||||||
|
VRP01 lo ha tenuto fuori. **La condizione di riapertura di §3 e' soddisfatta nella forma** (un
|
||||||
|
archivio bite con bid/ask su un crollo) **e non nella sostanza**: crollo a vol bassa, sotto il gate,
|
||||||
|
tenor 14-21, famiglia inverse, e l'unico f non tautologico (MTM) vale 1,08 su 4 strutture. **Non si
|
||||||
|
riapre.** Primo punto di una distribuzione che `collect_chain` riempira' da solo; il secondo punto
|
||||||
|
utile e' un crollo **con IV-rank > 0,30**.
|
||||||
|
|
||||||
|
## 3. Piu' monete, sempre in Deribit — `r0909_deribit_universo.py` · `r0909_xrp_terza_gamba.py`
|
||||||
|
|
||||||
|
**Il venue letto oggi (15:05Z):** 32 perpetual USDC-lineari aperti (+6 non aperti, fra cui **APT
|
||||||
|
inactive**: e' il mancante rispetto al 23/08). Volume 24h ≥ $10M solo per **XRP ($38M, il piu'
|
||||||
|
scambiato del venue, spread 4 bps, listato 2022-03-16), ETH, BTC, SOL ($13M)**; tutto il resto fra
|
||||||
|
$0,01M e $7M (TAO, HYPE, ADA, AVAX, NEAR…) con spread 2-20 bps. XS01: **13/19** gambe — mancano
|
||||||
|
ARB, OP, APT, INJ, TIA, SEI → §50 resta: morto per ampiezza. Opzioni USDC-lineari su 7 sottostanti
|
||||||
|
(SOL 676, HYPE 442, XRP 442, TRX, AVAX) con la liquidita' misurata il 22/08 (SOL: 9% delle put a
|
||||||
|
due lati). Trovati dalla revisione, registrati perche' il controllo costa €0 e nessuno li riapra:
|
||||||
|
**`BTCDVOL_USDC-30SEP26`** (l'unico strumento long-vol diretto del venue: volume 0, OI 132, bid/ask
|
||||||
|
39,1/43,9 = spread 12% → non negoziabile, e in contango sanguina come la put) e **PAXG perpetual**
|
||||||
|
(oro, dal 2024-12: $0,18M/g, OI $8,3M — non misurato, non morto, stessi muri di SOL/XRP: 9 mesi di
|
||||||
|
storia contro i 3 anni della decisione 22/08).
|
||||||
|
|
||||||
|
⇒ **L'unica "altra moneta" liquida su Deribit mai misurata come gamba direzionale e' XRP.** Misurata
|
||||||
|
col harness di `r0822_sol_leg` (TP01 e SKH01 **congelati**, 24 ancore, differenze appaiate,
|
||||||
|
de-levering, per anno), ipotesi a priori dichiarata: diluisce.
|
||||||
|
|
||||||
|
Certificazione (Coinbase dal rilisting 2023-07, Bitstamp prima — Coinbase aveva delistato XRP per
|
||||||
|
la causa SEC): >1% dal riferimento 0,06% (2022) · **0,57% (2023)** · 0,01-0,02% (2024+); mediana
|
||||||
|
5-6 bps. ⇒ **L-PULITA dal 2024** (stessa soglia e stesso anno sporco di SOL). ⚠️ Barre 5m flat
|
||||||
|
**27-61%** (BTC/ETH 2021+: 0,1-2%; SOL 21%): il 5m su cui gira SKH01 e' 20-100× piu' flat.
|
||||||
|
|
||||||
|
| | L-FULL (2022-03+) | L-PULITA (2024+) |
|
||||||
|
|---|---|---|
|
||||||
|
| dSharpe FULL (mediana, 24 ancore) | **−0,206** [−0,26, −0,16], >0 in 0% | −0,066, >0 in 12% |
|
||||||
|
| dSharpe hold-out | **−0,169** [−0,32, −0,15], **>0 in 0%** | −0,169, 0% |
|
||||||
|
| dCAGR | −2,27 pp, 0% | −1,32 pp, 4% |
|
||||||
|
| null del de-levering | Sh 3g 1,23 vs 2g×0,974 **1,44** → de-levering | 1,54 vs 1,53 → pari |
|
||||||
|
| per anno (dSh) | 2022 −0,55 · 2023 −0,66 · **2024 +0,54** · 2025 −0,44 · 2026 −0,25 → **1/5** | |
|
||||||
|
| corr(gamba XRP, book) | +0,35 / +0,37 (SOL 0,40-0,56) | |
|
||||||
|
|
||||||
|
**XRP DILUISCE** — lo stesso −0,17 di hold-out di SOL, in 0/24 ancore, con **un anno buono** (2024).
|
||||||
|
TP01 XRP da solo: Sharpe −0,01, hold-out −0,49; SKH01 XRP maxDD 40,1% (il criterio di selezione di
|
||||||
|
SKH01-V2-DD, maxDD<30%, fallisce anche qui, come SOL). Eseguibilita' non e' il vincolo (min 1 XRP =
|
||||||
|
$1,42). Non misurato: **SKH01 solo-short su una terza moneta** (la domanda riguarda i crolli, dove
|
||||||
|
paga lo short) — ma il filtro di direzione e' una famiglia nuova (M20) e il 5m di XRP e' peggio di
|
||||||
|
SOL: il muro e' lo stesso. **Nessuna proposta.** Il dato vive in `data/raw/alt_xrp_*` (namespace di
|
||||||
|
ricerca, non rinfrescato dal cron, `load_data("XRP")` fallisce; `tests/test_sol_leg.py` lo presidia
|
||||||
|
via `DERIBIT_INSTR`). La cache del riferimento in `data/_cache` e' gitignored e fuori backup: la
|
||||||
|
certificazione si riproduce **con rete** (N11 vale con rete).
|
||||||
|
|
||||||
|
## 4. Cosa cambia una decisione — niente; cosa si registra
|
||||||
|
|
||||||
|
| voce | prima | oggi |
|
||||||
|
|---|---|---|
|
||||||
|
| il libro nei crolli | "immune, beta 0,076" | perde nel GIORNO (−0,48%/g, 18% positivi), immune-o-positivo per finestra, **> 0 in 8/8 dal 2022** per SKH01 short marcato all'uscita; beta 0,0769 riprodotto |
|
||||||
|
| f di stress delle opzioni | non misurato ("serve un crash catturato") | crollo catturato **a vol bassa, sotto il gate, inverse**: f_net 0,74 = rally; f_mtm 1,08; ali 1,92×. **Condizione non soddisfatta nella sostanza** |
|
||||||
|
| peso SKH01 | gate fallisce (26/07) | invariato: la condizione che fallisce e' in-sample; cache segnali ferma al 25/07 |
|
||||||
|
| XRP | mai misurato | diluisce come SOL (−0,169 hold-out, 0/24) |
|
||||||
|
| universo Deribit | 14/19 (23/08) | 13/19 (APT inactive); liquidi solo BTC/ETH/XRP/SOL; BTCDVOL future non negoziabile; PAXG non misurato |
|
||||||
|
| debito nuovo | — | `cblib.spot_series` + `asof` guarda un'ora avanti (feed 1h etichettato all'apertura) |
|
||||||
|
|
||||||
|
**Cosa riaprirebbe qualcosa:** (a) un secondo crollo catturato **a IV-rank > 0,30** (VRP01 a
|
||||||
|
mercato), sulla famiglia USDC-lineare, per il f di stress; (b) `weights_tilt_null` che passa — si
|
||||||
|
rigenera `skh_sigtab` e si rilancia `r0726_reeval_live_weight`, non si ridiscute; (c) per le
|
||||||
|
"altre monete", un meccanismo che non sia TP01/SKH01 congelati, o 3 anni di dato 5m liquido.
|
||||||
|
|
||||||
|
## 5. Errori catturati in sessione (prima della revisione)
|
||||||
|
|
||||||
|
- Percorso ora per ora in un ciclo `groupby` per ancora: >10 minuti; riscritto su una tabella
|
||||||
|
ora×strumento (`Quote`), ~3 minuti.
|
||||||
|
- BTC "in geometria" senza guardia: la griglia rada faceva scegliere a `pick_legs` una δ−0,02 al
|
||||||
|
posto della δ−0,10 (K72000/55000). Guardia `TOL_D` (±0,08/±0,06), scritta dopo aver visto BTC ma
|
||||||
|
che per ETH scarta 2/288 e non cambia nessun verdetto (verificato dal revisore).
|
||||||
|
- Coinbase restituiva un batch vuoto per XRP 2022 e il ciclo si fermava: certificazione vuota.
|
||||||
|
Riscritto con avanzamento su batch vuoto e riferimento Bitstamp dove Coinbase manca.
|
||||||
|
|
||||||
|
## 6. Revisione (`fable`, agente fresco) — 21 segnalazioni, tutte verificate
|
||||||
|
|
||||||
|
Applicate: #1 artefatto del MTM (taglio alla larghezza; IV 108 e' a T→0) · #2 f_exp tautologico
|
||||||
|
(quota stampata, N_eff = strutture) · #3 "101%" = perdita massima + fee · #4 `asof` un'ora avanti
|
||||||
|
(`spot_causale`; debito su `cblib`) · #5 famiglia inverse dichiarata · #8 "durante" bimodale (p10/p90,
|
||||||
|
quota che perde) · #9 ordine delle classi (immune → POSITIVO → PERDE, tre affermazioni nel verdetto)
|
||||||
|
· #10 peggior giorno = crollo con TP01 0,505, docstring (c) corretto · #11 beta sulla finestra di §46
|
||||||
|
(0,0769, tolleranza 0,01) · #12 scomposizione esatta con assert · #13 lente "marcata all'uscita"
|
||||||
|
dichiarata · #14 confronto bit-exact con `_skyhook_returns` · #15 cache `skh_sigtab` ferma al 25/07
|
||||||
|
· #16 1,92 e' un punto nuovo, non "gia' noto" · #17 il crollo e' sotto la mediana del DVOL: verdetto
|
||||||
|
riscritto · #18-19 BTCDVOL future e PAXG registrati · #21 APT inactive, cache con rete.
|
||||||
|
Non applicate: nessuna. Il revisore ha trovato **due conclusioni che non reggevano** (il 2,6× e
|
||||||
|
l'"8/12 guadagna") e le ha smontate con numeri riprodotti: la regola *"chi rivede non e' chi ha
|
||||||
|
scritto"* ha pagato in una sessione.
|
||||||
|
|
||||||
|
**Test (Opus, agente separato): `tests/test_r0909_*.py`, 92 test, suite intera 1008/1008 verdi**
|
||||||
|
(erano 916). Coprono: ordine delle classi e riproduzione verbatim dei verdetti, `spot_causale` col
|
||||||
|
controllo positivo sul difetto che ripara, taglio alla larghezza, geometria `in_geom` derivata da
|
||||||
|
`VRP_CFG`, regola L-PULITA (anno pulito isolato ignorato), `_fetch_1h` su batch vuoti, P1 per
|
||||||
|
identita' (`W_TP01`, `XS_UNIVERSE`), zero rete e zero scritture in `data/`. **Due difetti trovati da
|
||||||
|
Opus nel verdetto di XRP, corretti:** un campione vuoto stampava "DILUISCE" (ora "NON MISURABILE",
|
||||||
|
P5) e il criterio per anno era cablato al 2024 invece che derivato dall'anno della lente (P1).
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
# 2026-09-09b — Debito §5.18 riparato: `cblib` causale (spot +1h, DVOL +1 giorno) e §11 rimisurato
|
||||||
|
|
||||||
|
**Richiesta dell'operatore:** *"ripara il debito §5.18 su cblib e rimisura §11"*. Revisione col modello
|
||||||
|
`fable` (agente fresco), come da regola.
|
||||||
|
|
||||||
|
**Esito in una riga:** riparato in `cblib` — spot e DVOL escono **gia' causali** — e §11 rimisurato allo
|
||||||
|
stesso taglio: **0,714 [0,690-0,779] → 0,712 [0,664-0,732]** (n=19). Il numero non si muove perche' le due
|
||||||
|
correzioni hanno **segno opposto**; **nessun verdetto cambia**; riparando la prima serie ne e' uscita una
|
||||||
|
**seconda con lo stesso difetto** (il DVOL giornaliero, fino a 24 ore avanti); il DVOL orario resta un
|
||||||
|
passo aperto **con costo misurato**. Libro, config, cron: invariati. Nessun ordine.
|
||||||
|
|
||||||
|
## 0. Il difetto, verificato prima di ripararlo (D6: si controlla col lag, non si assume)
|
||||||
|
|
||||||
|
| serie | come e' etichettata | prova | look-ahead di `asof(ts)` |
|
||||||
|
|---|---|---|---|
|
||||||
|
| feed 1h certificato | all'**apertura**: la barra T contiene la chiusura di T+1h | chiusura 1h a T == chiusura 5m a **T+55** nel **100%** delle barre (BTC ed ETH, 1-5/06); a T−5 nell'1,7% / 0% | **+1 ora** |
|
||||||
|
| `dvol_{asset}.parquet` (giornaliero, `fetch_dvol`, `resolution=86400`) | al giorno D contiene la **chiusura delle 23:00 di D** | API pubblica a risoluzione 1h, 1-4/06: `close` 1D del giorno D == `close` 1h delle 23:00 di D, e `open` 1D di D+1 == `close` di D | **fino a +24 ore** |
|
||||||
|
|
||||||
|
La seconda riga non era nel debito: e' uscita controllando la convenzione dell'altra serie di
|
||||||
|
contesto che `cblib` legge con lo stesso `asof`. Il sleeve (`_vrp_weekly_asset`) **non ha il difetto**:
|
||||||
|
lavora a cadenza giornaliera e accosta la chiusura del giorno D al DVOL del giorno D, che a quella
|
||||||
|
cadenza e' coerente. Il difetto e' solo di chi entra **a un'ora** con una serie giornaliera.
|
||||||
|
|
||||||
|
## 1. La riparazione
|
||||||
|
|
||||||
|
`cblib.causale(s, cadenza)` sposta le **etichette** (mai i valori) dall'apertura all'istante in cui la
|
||||||
|
chiusura e' nota. `spot_series` → `causale(·, "1h")`, `dvol_series` → `causale(·, "1D")`. Conseguenze:
|
||||||
|
|
||||||
|
- `asof(ts)` = ultima chiusura **conosciuta** a ts (prima della prima chiusura nota: NaN, non un valore).
|
||||||
|
- il regolamento `ST = S.asof(exp)` alle 08:00 e' ora la chiusura della barra 07:00, cioe' **il prezzo
|
||||||
|
delle 08:00** — prima era la chiusura della barra 08:00, cioe' il prezzo delle **09:00**, un'ora dopo la
|
||||||
|
scadenza. Deribit regola su una TWAP dei 30 minuti prima delle 08:00: l'approssimazione e' ora dalla
|
||||||
|
parte giusta dell'ora.
|
||||||
|
- `r0909_crash_catturato` **non ri-sposta** (lo `spot_causale` locale della mattina e' rimosso: applicato
|
||||||
|
due volte darebbe la chiusura di **due** ore prima — lo stesso errore col segno opposto). Test di
|
||||||
|
non-regressione sul sorgente.
|
||||||
|
- chi usa `cblib`: `r0822_vrp_real_quotes` (§11), `r0822_skew` (§5), `r0909` (§75), **`vrp_f_watch`**
|
||||||
|
(cron giornaliero, `data/vrp_f_watch/state.json`), e — trovati dal revisore, non da me —
|
||||||
|
**`r0730_vrp_real_quotes`** (il f 0,73 [0,706-0,805] di CLAUDE.md §2: rigirato stampa **0,71
|
||||||
|
[0,680-0,756]**, n=27, 0/27 ≥ 1, f gamba lunga 1,66 → 1,49; verdetto invariato, numero dichiarato in §2)
|
||||||
|
e **`r0901_btc_collar`** (join del DVOL **per giorno**: con la serie causale avrebbe accostato l'IV di D
|
||||||
|
al DVOL di D−1 — riallineato con `causale(·, "-1D")`, bit-exact a prima, §71 non si muove).
|
||||||
|
`options_vrp_calibrate.py` ha una **copia propria** di `spot_series` (numeri del 20/06): **non toccato**,
|
||||||
|
dichiarato.
|
||||||
|
- la sezione di REGIME di `r0822` (date di calendario: «ultimo giorno aperto», IV-rank per anno) e la
|
||||||
|
tabella di contesto di `r0909` usano `causale(V, "-1D")`: li' serve il DVOL **del** giorno, non
|
||||||
|
l'etichetta causale — altrimenti ogni data stampata slitta di un giorno (trovato dal revisore).
|
||||||
|
- `r0822_vrp_real_quotes` ha ora il flag `--al <data>` (taglio delle scadenze) per riprodurre una misura
|
||||||
|
a una data: senza, "riprodurre il 22/08" era impossibile perche' la catena cresce ogni ora.
|
||||||
|
|
||||||
|
## 2. Prima di pubblicare il numero nuovo, la macchina ha riprodotto quello vecchio (M23)
|
||||||
|
|
||||||
|
Worktree di HEAD (`git worktree add`, `data/raw` in symlink), stesso script con `--al 2026-08-22`:
|
||||||
|
**pooled 0,714 (IC95 [0,690, 0,779], n=19)** — al millesimo, IC compreso. Poi lo stesso comando sul
|
||||||
|
codice riparato.
|
||||||
|
|
||||||
|
## 3. Scomposizione: due correzioni di segno opposto (M26) e un controllo orario
|
||||||
|
|
||||||
|
| taglio | spot | DVOL | pooled f | IC95 | n |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 22/08 | grezzo | grezzo (com'era) | **0,714** | [0,690, 0,779] | 19 |
|
||||||
|
| 22/08 | causale | grezzo | 0,721 | [0,685, 0,758] | 19 |
|
||||||
|
| 22/08 | grezzo | causale (giornaliero) | 0,693 | [0,670, 0,748] | 19 |
|
||||||
|
| 22/08 | causale | causale (giornaliero) = **`cblib` oggi** | **0,712** | [0,664, 0,732] | 19 |
|
||||||
|
| 22/08 | causale | causale **orario** (controllo, API pubblica) | 0,717 | [0,667, 0,732] | 19 |
|
||||||
|
| 09/09 | grezzo | grezzo | 0,720 | [0,717, 0,952] | 23 |
|
||||||
|
| 09/09 | causale | causale (giornaliero) | **0,712** | [0,677, 0,741] | 23 |
|
||||||
|
| 09/09 | causale | causale orario | 0,726 | [0,680, 0,738] | 23 |
|
||||||
|
|
||||||
|
Il DVOL di fine giornata **alzava** f: nel rally la vol scende dentro il giorno, la chiusura di D sta
|
||||||
|
**sotto** quella di D−1 (+1,05 pt di vol mediana su 19/19 trade, misura del revisore) → denominatore piu'
|
||||||
|
basso → f piu' alto; causale: −0,021. Lo spot di un'ora dopo, invece, **non e' un meccanismo ma rumore**:
|
||||||
|
lo scarto per trade ha segno misto (mediana −0,02% BTC / −0,11% ETH, banda −0,6…+0,9%) e sposta f fino a
|
||||||
|
**±0,15 per trade** (lo spread δ−0,28/−0,10 ha delta netto ~0,18: 0,5% di spot vale 15-20% del credito
|
||||||
|
modellato); il +0,007 netto e' la somma di 19 segni. ⚠️ Corollario: lo spot causale resta **stantio fino
|
||||||
|
a 25 minuti** (snapshot al :25, chiusura nota al :00), cioe' ±0,05-0,10 di f per osservazione; il feed
|
||||||
|
**5m certificato** lo porterebbe a ≤5 minuti. Non fatto: dichiarato in §6. Il DVOL giornaliero causale e' **stantio fino a
|
||||||
|
23 ore** (agli ingressi delle 08:00: 8 ore): scarto mediano dall'orario **0,2 punti di vol**, max 1,1
|
||||||
|
(22/08) / 3,0 (oggi). Su §11 vale 0,005 di f.
|
||||||
|
|
||||||
|
## 4. Cosa cambia in §11 (taglio 22/08, stesso campione)
|
||||||
|
|
||||||
|
| grandezza | prima | dopo | perche' |
|
||||||
|
|---|---|---|---|
|
||||||
|
| f pooled | 0,714 [0,690-0,779] | **0,712 [0,664-0,732]** | §3 |
|
||||||
|
| f BTC / ETH | 0,706 / 0,715 | 0,720 / 0,692 | |
|
||||||
|
| Sharpe canonico BTC (titolo, non citabile) | 32,58 | 29,61 | regolamento alle 08:00 |
|
||||||
|
| **mediana onesta** BTC (9 ancore) | 1,45 | **2,12** | banda [−0,19, 29,61] (era [−0,35, 32,58]) |
|
||||||
|
| Sharpe canonico ETH → mediana onesta | 3,90 → 2,41 | 7,64 → 3,35 | una settimana ETH cambia taglia: 26/06 −27,38 → −12,84 $ (BTC: 31/07 19,88 → 31,69, 14/08 12,94 → 8,68) |
|
||||||
|
| media BTC fra le ancore | −1,35% → +11,22% | −0,66% → +11,44% | **cambia ancora segno**; sd 11,1× → 9,2× |
|
||||||
|
| forfait fee vs listino | 1,44× / 1,80× | 1,51× / 1,87× | credito modellato a DVOL causale |
|
||||||
|
| peggior mark: mediana / minimo / minimo fra le vincenti | −6,4% / −91,0% / −56,9% | −6,4% / −91,5% / −55,2% | |
|
||||||
|
| P&L somma 19 settimane | $2.733 | $2.822 (+3,3%) | tutto dal regolamento |
|
||||||
|
| sottostante nel campione | +25,1% / +44,2% | +21,1% / +40,1% | il vecchio regolamento del 21/08 era il prezzo delle **09:00**, che sta **+3,9%** sopra quello delle 08:00 (76.311 → 79.316): un numero a due estremi pesa un'ora intera. (La prima stesura diceva «−1,8%»: avevo letto l'ora dopo, 09:00 → 10:00. Corretto dal revisore) |
|
||||||
|
|
||||||
|
**Verdetto di §11: SCARTATO, per le stesse quattro ragioni** (il campione non contiene la strategia; il
|
||||||
|
titolo non sopravvive all'ora d'ingresso; la famiglia con storia non e' quella eseguibile; lo spread e'
|
||||||
|
il costo dominante). Nessuna delle quattro dipende da un'ora di spot. Il testo del verdetto nello script
|
||||||
|
riporta i numeri nuovi con i vecchi accanto.
|
||||||
|
|
||||||
|
## 5. Effetti collaterali, misurati (tutto cio' che legge `cblib`)
|
||||||
|
|
||||||
|
**§75, crollo catturato** (`r0909`, rigirato): ETH pre-crollo f_net **0,74 [0,65-0,82] → 0,76
|
||||||
|
[0,71-0,86]**; peggior MTM 1,08 [1,07-1,31] → 1,08 [1,06-1,27]; put δ−0,10 1,92× → 1,98×; al picco 0,94 →
|
||||||
|
0,99; BTC durante **0,79 → 0,89 [0,74-1,05]**, ETH durante 0,71 → 0,78. Con il DVOL **orario** BTC durante fa **0,83**: nel crollo
|
||||||
|
il DVOL sale di 5-6 punti al giorno, quindi il giornaliero di fine giornata (com'era) sottostima f e
|
||||||
|
quello del giorno prima (causale) lo sovrastima — **−0,04/+0,06 di f e' il prezzo della stantiezza in un
|
||||||
|
crollo**, ed e' l'argomento per il feed orario. DVOL "pre" 37,3 → **36,4** (BTC): il 37,3 era la chiusura
|
||||||
|
del **primo giorno di crollo** (01/06, −3,5%), non del giorno prima — IV-rank pre 0,05 → 0,03. Picco e
|
||||||
|
percentili **invariati** (49,1 / 69,2; 29° / 46°) dopo aver corretto la tabella di contesto (§7).
|
||||||
|
Verdetto invariato.
|
||||||
|
|
||||||
|
**§5, skew** (`r0822_skew`, rigirato, 66 s): la scomposizione sul panel si muove di **≤0,005** per fattore
|
||||||
|
(term 0,880 · skew 0,917 · spread 0,910 · fit 0,995 · f_tot 0,729); Q1/Q2 (lag prezzo/skew) **non si
|
||||||
|
toccano** (serie proprie ancorate a `ts_max`). Agli ingressi il controllo `f_markfit` passa da 1,045 a
|
||||||
|
**0,991** e le osservazioni che lo superano da 22/28 a **25/28**: l'ora di spot in piu' stava **nel
|
||||||
|
controllo**, non nell'effetto — riparare ha reso il controllo piu' pulito, non il risultato diverso.
|
||||||
|
|
||||||
|
**`vrp_f_watch`** (sorvegliante in `cron_daily`, criteri pre-registrati il 30/07 e **non toccati**):
|
||||||
|
|
||||||
|
| | prima (state.json del 09/09 mattina) | dopo |
|
||||||
|
|---|---|---|
|
||||||
|
| coppie | 28 | 28 |
|
||||||
|
| f canonico | 0,732 [0,702-0,784] | **0,706 [0,678-0,745]** |
|
||||||
|
| f candidato | 0,803 [0,741-0,842] | 0,805 [0,771-0,840] |
|
||||||
|
| differenza | +0,054 [−0,027, +0,105], 18/28 > 0 | **+0,097 [+0,042, +0,140]**, 21/28 > 0 |
|
||||||
|
| ampiezza IC95 (b ≤ 0,12) | 0,131 — no | **0,098 — OK** |
|
||||||
|
| coppie (a ≥ 40) | no | no |
|
||||||
|
| `ready` | False | False |
|
||||||
|
|
||||||
|
Lo `state.json` e' stato **riscritto** dal mio lancio (21:14Z, senza `--quiet`; nessun Telegram: notifica
|
||||||
|
solo a `ready`); il «prima» completo e' la colonna qui sopra (n_positive 18, `esclude_zero` False), e il
|
||||||
|
cron lo rigenera domani. La differenza ora **esclude lo zero**. Non decide niente: (a) non e' soddisfatto e il candidato resta
|
||||||
|
bocciato sul deflated-Sharpe, che non dipende da f. Si registra perche' fra un mese qualcuno leggera'
|
||||||
|
"esclude lo zero" e deve sapere da quando e perche'.
|
||||||
|
|
||||||
|
## 6. Cosa resta aperto, col suo costo
|
||||||
|
|
||||||
|
| voce | costo di non farlo | costo di farlo |
|
||||||
|
|---|---|---|
|
||||||
|
| **DVOL orario** nel feed (`fetch_dvol --res 3600`, riga in `cron_daily`, `cblib` che lo legge) | su §11 0,005 di f; **su un crollo ±0,05**; ogni misura oraria su opzioni a DVOL giornaliero eredita lo scarto | un fetch pubblico gratuito (3.163 barre da maggio in 2 chiamate), una riga di cron, e una decisione: e' un feed nuovo dentro il perimetro (D1, certificazione, backup) |
|
||||||
|
| **spot causale stantio ≤25 min** (snapshot :25, chiusura :00): ±0,05-0,10 di f per osservazione | `cblib.spot_series` sul feed **5m** certificato (`causale(·, "5m")`): cambia di nuovo ogni numero di `cblib`, quindi con rimisura dichiarata |
|
||||||
|
| `options_vrp_calibrate.spot_series` (copia, 20/06) | i numeri del 20/06 (f 0,73) restano con l'ora in piu'; §11 li ha gia' sostituiti | riscriverlo su `cblib` |
|
||||||
|
| la tabella pubblicata di §5 (x0,869 / x0,904 / x0,920) | ≤0,005 per fattore: sotto la risoluzione del campione (14+14 scadenze) | nulla: nota aggiunta |
|
||||||
|
|
||||||
|
## 7. Errori dell'autore in sessione
|
||||||
|
|
||||||
|
- **`asi8` in microsecondi** scrivendo il test della causalita' (pandas 3): i timestamp sintetici finivano
|
||||||
|
nel 1970 e il test falliva per la fixture, non per il codice. E' la trappola del debito 1, ripagata
|
||||||
|
scrivendo un test *sul* debito 18. Corretto con `(idx − epoch) // Timedelta(ms)`, la convenzione di
|
||||||
|
`load_tf`.
|
||||||
|
- **La tabella di contesto di `r0909` si e' spostata di un giorno** dopo la riparazione: mostrava il DVOL
|
||||||
|
del giorno D−1 accanto al prezzo del giorno D, e il picco del crollo cambiava (69,2 → 68,3, 46° → 43°)
|
||||||
|
non per una misura ma per un'etichetta. Trovato **col diff degli output**, non da un test: una tabella
|
||||||
|
per umani non ha test. Ora il contesto usa la chiusura *del* giorno (`causale(V, "-1D")`, dichiarato
|
||||||
|
nel commento) e `dvol_pre` / `ivr_pre` restano causali.
|
||||||
|
- Il verdetto §7 di `r0822_vrp_real_quotes` era **prosa con numeri cablati**: sarebbe rimasto a 0,714 per
|
||||||
|
sempre (N11 al contrario). Aggiornato con i vecchi accanto; il difetto di forma resta (i numeri del
|
||||||
|
verdetto andrebbero calcolati, non scritti).
|
||||||
|
|
||||||
|
## 8. Revisione (`fable`, agente fresco) — 16 segnalazioni, verdetto «correggi e committa»
|
||||||
|
|
||||||
|
Ha **riverificato in proprio** la convenzione delle due serie (1h vs 5m su 120 barre: 100%; DVOL contro
|
||||||
|
l'API oraria) e riprodotto ogni numero dei documenti lanciando gli script. Applicate: **#2** «−1,8% in
|
||||||
|
un'ora» era falso (avevo letto 09:00 → 10:00; il fatto e' +3,9% 08:00 → 09:00) · **#3** due delle tre
|
||||||
|
settimane «ETH» erano BTC · **#4** la parentesi sul DVOL era invertita (la chiusura di D e' piu' BASSA,
|
||||||
|
non piu' alta) · **#5** lo spot non e' un meccanismo ma rumore, e resta stantio ≤25 min · **#6/#7** due
|
||||||
|
lettori di `cblib` non dichiarati: `r0730` (0,73 → 0,71, ora in CLAUDE.md §2) e `r0901` (riallineato) ·
|
||||||
|
**#8** le date di calendario di `r0822` §1 slittavano di un giorno · **#9** il test anti-doppio-shift non
|
||||||
|
catturava `causale(spot_series, "1h")`: ora conta i `causale(` · **#10** ancora 0,74 → 0,76 nel test
|
||||||
|
d'integrazione · **#11** teste di §75 e memoria 20 §5 con i numeri vecchi come correnti (M28) · **#12**
|
||||||
|
«±0,05» → «−0,04/+0,06» · **#13** `state.json` riscritto: dichiarato · **#15** lo skew agli ingressi cambia
|
||||||
|
piu' del panel (correlazioni che cambiano segno): dichiarato in §5. Non applicate: nessuna. Confermati
|
||||||
|
senza rilievo (#1, #14, #16): verso e taglia degli shift, nessun doppio spostamento, `ST` alle 08:00
|
||||||
|
coerente con la TWAP 07:30-08:00, tutti i numeri di §3-§5.
|
||||||
|
|
||||||
|
## 9. Test
|
||||||
|
|
||||||
|
`tests/test_cb_chain_vrp.py`: +3 (causale; `spot_series` causale sul feed 1h con controllo positivo sulla
|
||||||
|
serie grezza; `dvol_series` causale sul giornaliero). `tests/test_r0909_crash_catturato.py`: il test dello
|
||||||
|
shift locale diventa "lo script usa le serie di `cblib` senza ri-spostarle" (sorgente + controllo positivo
|
||||||
|
su `cblib.causale`). Suite intera: **1011 passati, 0 falliti** (erano 1008), rilanciata dopo le correzioni della revisione: 1011/1011.
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# 2026-09-09c — Revisione settimanale in sola lettura: cosa si costruisce e cosa no
|
||||||
|
|
||||||
|
**Richiesta dell'operatore:** *«schedulare ogni x tempo un processo di auto revisione dei dati e della
|
||||||
|
configurazione del sistema in modo che migliori con il tempo … autoregolazione e autoaggiornamento dei
|
||||||
|
sistemi o anche creare nuovi sistemi … valutando dati macroeconomici … e poi informa me tramite
|
||||||
|
Telegram»*. Dopo la discussione: *«fai»*.
|
||||||
|
|
||||||
|
**Esito in una riga:** costruita la **rilettura settimanale in sola lettura** con revisore diverso
|
||||||
|
dall'autore (`claude-fable-5-1`), rapporto firmato in `docs/revisioni/` e Sintesi su Telegram, lunedi'
|
||||||
|
06:15 UTC; **scartati** — con la memoria del progetto, non per prudenza generica — autoregolazione dei
|
||||||
|
parametri, generazione automatica di strategie e macro da internet. Libro, config, pesi: invariati.
|
||||||
|
|
||||||
|
## 1. Perche' tre pezzi su quattro sono no
|
||||||
|
|
||||||
|
| pezzo | cosa dice la memoria | esito |
|
||||||
|
|---|---|---|
|
||||||
|
| auto-revisione periodica | nove sorveglianti, ognuno sulla sua grandezza; nessuno rilegge l'insieme; la revisione col secondo modello ha trovato oggi due conclusioni false e un look-ahead | **si'** |
|
||||||
|
| autoregolazione dei parametri | TP01/SKH01 congelati (M20), 75/25 confermato 3 volte, `weights_tilt_null` fallisce; `scale_watch` esiste per rendere visibile ogni cambio di config; A8: una riga di config + una di giornale, decisione dell'operatore | **no** |
|
||||||
|
| generazione e test di strategie nuove | 69 filoni in 6 ondate; ogni candidato e' un trial (M3) e deflaziona tutto; la ricerca non e' il vincolo binding dal 26/07 (+0,046 €/g il miglior lead contro +4,07%/anno per €100/mese) | **no, salvo freno**: prima si apre la memoria e si batte il motivo della morte; i candidati vanno in forward-monitor con gate e data, mai nel book |
|
||||||
|
| dati macro da internet | gate macro / DVOL direzionale / skew de-risk: HEDGE o ridondanti (TP01 gia' flat nei crolli); un dato dal web non e' certificato (D1-D4; la APR USDC 3,40% pubblicata e mai incassata) | **no** |
|
||||||
|
|
||||||
|
Il centro dell'argomento: **il progetto ha gia' misurato che piu' ricerca non sposta il traguardo, i
|
||||||
|
bonifici si'**. Un ciclo automatico che cerca strategie ottimizza la leva sbagliata e paga in trial bruciati.
|
||||||
|
|
||||||
|
## 2. Cosa gira
|
||||||
|
|
||||||
|
`scripts/cron_review.sh` (lunedi' **06:15 UTC**: fuori dal :00, fuori dal martedi' 09:00, dopo il
|
||||||
|
`cron_daily` delle 00:30 cosi' il giornale di domenica e' chiuso) → `scripts/live/revisione.py --quiet`
|
||||||
|
→ `src/live/revisione.scrivi`. Riga installata in crontab (`15 6 * * 1`), letta dal test.
|
||||||
|
|
||||||
|
**Materiale** (sola lettura, ~195k caratteri, 27 voci): CLAUDE.md intero · `config/live.json` ·
|
||||||
|
`crontab -l` · `git log` 14 g + `git status` · `monitor_health` · stato dei sorveglianti (scale, vrp_f,
|
||||||
|
usde, balance, venue_news, movimenti dichiarati, watermark) · 7 pagine di giornale · diari della
|
||||||
|
settimana. Gli stati stanno **prima** delle voci grandi: alla prima stesura il tetto totale (320k) aveva
|
||||||
|
tagliato `usde_convert.jsonl` a 2 caratteri — un sorvegliante invisibile per un limite di lunghezza.
|
||||||
|
|
||||||
|
**Prompt**: dieci regole vincolanti (sola lettura · ogni segnalazione cita la fonte · solo numeri del
|
||||||
|
materiale · le decisioni di §3 non si ripropongono se la colonna «cosa la riapre» non e' soddisfatta ·
|
||||||
|
un'idea nuova solo citando la memoria che ha ucciso la simile · i gate con data e criterio, senza
|
||||||
|
anticipare · misurato ≠ dedotto · niente previsioni · URGENTE per cio' che serve all'operatore subito ·
|
||||||
|
cinque titoli fissi, ~1.000 parole). L'ultimo titolo, «Cosa ho letto e cosa mi mancava», e' l'elenco da
|
||||||
|
cui il materiale della settimana dopo migliora: e' l'unico «miglioramento col tempo» che questo sistema
|
||||||
|
si concede, e passa da una modifica al codice, cioe' da una revisione.
|
||||||
|
|
||||||
|
**Uscita**: `docs/revisioni/<data>.md` firmato (modello, ora, «SOLA LETTURA», «lettore fallibile P13»,
|
||||||
|
guardia sui numeri come `analista` con soglia 12, tabella del materiale letto). Telegram: la sola
|
||||||
|
Sintesi, taglio dichiarato sotto i 4.096. Modello muto o risposta senza i titoli → rapporto «NON
|
||||||
|
eseguita» col motivo e 🚨: il silenzio non e' una revisione (P5).
|
||||||
|
|
||||||
|
**Cosa non puo' fare, per costruzione**: scrivere altro che il rapporto (test sull'albero dei file:
|
||||||
|
`--secco` non scrive niente, un giro scrive UN file), eseguire proposte, cambiare config. Il modello e'
|
||||||
|
`claude-fable-5-1` per costante, e un test verifica che sia diverso da `analista.MODELLO_DEFAULT`.
|
||||||
|
|
||||||
|
## 3. Test — `tests/test_revisione.py` (16)
|
||||||
|
|
||||||
|
Finestra del materiale e file assenti dichiarati · ordine stati-prima-delle-voci-grandi · troncamento
|
||||||
|
dichiarato · regole nel prompt · guardia (vuoto, titoli mancanti, numeri fuori con controllo positivo) ·
|
||||||
|
rapporto firmato · modello muto → rapporto + 🚨 · Telegram con HTML neutralizzato e taglio dichiarato ·
|
||||||
|
`--secco` non chiama e non scrive · un giro scrive un solo file · `--no-telegram` · revisore ≠ autore ·
|
||||||
|
cadenza: il `.sh` dichiara «lunedi' HH:MM UTC», minuto ≠ 0, fuori dallo slot di release, e la crontab
|
||||||
|
installata ha gli stessi cinque campi (skip se illeggibile). `test_cli_flag` ha preso lo script da solo
|
||||||
|
(guardia `valida`, USO coi flag, flag del cron accettati).
|
||||||
|
|
||||||
|
## 4. Errori in sessione
|
||||||
|
|
||||||
|
- `_tronca(testo, n=MAX_CHARS_VOCE)`: il default e' catturato alla definizione, il test che patchava la
|
||||||
|
costante non vedeva niente — la stessa trappola del debito 12 (`connect`). Letto a runtime.
|
||||||
|
- Il tetto totale tagliava in coda: i sorveglianti erano gli ultimi e i primi a sparire. Riordinato, e
|
||||||
|
il test lo presidia.
|
||||||
|
|
||||||
|
## 5. Primo giro reale (22:03Z) — e i tre difetti che solo il giro reale poteva trovare
|
||||||
|
|
||||||
|
Il primo lancio e' **morto prima di chiamare il modello**: `OSError: Argument list too long` — 195k
|
||||||
|
caratteri di prompt passati come argomento alla CLI superano il limite del sistema, e `analista.interroga`
|
||||||
|
non lo catturava: zero rapporto, zero Telegram, **il silenzio che il modulo esiste per vietare**. Nessun
|
||||||
|
test l'avrebbe trovato (i test iniettano `interroga`): e' un difetto di trasporto, e il trasporto si valida
|
||||||
|
sul trasporto (P2). Tre correzioni: (1) `revisione.interroga` proprio, prompt su **stdin**, ogni eccezione
|
||||||
|
restituita come errore; (2) `scrivi` incapsula chiamata e invio: un'eccezione diventa un rapporto «NON
|
||||||
|
eseguita» + 🚨; (3) `_cmd` (crontab, git) dichiara qualunque eccezione invece di propagarla. Test con
|
||||||
|
controllo positivo su `OSError(7)`. Terzo difetto, trovato dal test: `analista.pulisci` **toglie le righe
|
||||||
|
che iniziano con `#`**, cioe' i cinque titoli che la guardia richiede — usarlo avrebbe reso ogni revisione
|
||||||
|
«rifiutata: titoli mancanti». Sostituito con `strip()`.
|
||||||
|
|
||||||
|
Secondo lancio: **rapporto scritto, stato `sospetta`**, 1.435 parole, `docs/revisioni/2026-09-09.md`.
|
||||||
|
La guardia ha segnato 11 numeri «non nel materiale»: sono cifre in **formato italiano** («4.477,46»,
|
||||||
|
«1.341») contro il materiale in formato USA («$4,477.46») — la normalizzazione di `analista` conosce una
|
||||||
|
convenzione sola. E' un limite dichiarato, non un errore del revisore: si aggiusta quando si vede quanto
|
||||||
|
spesso scatta. Contenuto: **nessun URGENTE**; tre verifiche in cima (una possibile attribuzione a reward di
|
||||||
|
400 USDE che erano un acquisto in `usde_watch` del 07/09 — `n_trades` fermo a 50 contro 54 ordini nel
|
||||||
|
registro; il cuscino USDC del debito §5.17 senza definizione balance/equity; il versamento del 04/09 non
|
||||||
|
dichiarato in `movimenti_dichiarati.jsonl`); una contraddizione in CLAUDE.md §1 sull'haircut (M28); la data
|
||||||
|
di arming che differisce fra §0, config e TWR; quattro sorveglianti senza timestamp o fuori da
|
||||||
|
`monitor_health`; e, come richiesto dalla regola 5, **zero idee di strategia**. «Cosa mi mancava»: stati
|
||||||
|
con ora di `edge/fee/venue/scale_watch`, l'output di `usde_watch.rendimento()`, i paper a oggi,
|
||||||
|
`cron_daily.sh`, il registro fill della settimana — l'elenco da cui il materiale della prossima settimana
|
||||||
|
migliora. Le segnalazioni sono dell'operatore e della prossima sessione: **nessuna e' stata applicata qui**
|
||||||
|
(sarebbe l'agente che esegue le proposte del revisore nello stesso giro, cioe' il ciclo che si e' scartato).
|
||||||
|
Costo: una chiamata, ~4 minuti.
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
# I cinque punti della revisione del 09/09 — decisi e fatti
|
||||||
|
|
||||||
|
**Data:** 2026-09-10 (sessione 12:08Z → 13:10Z; i timestamp qui dentro sono letti, non ricordati)
|
||||||
|
**Origine:** la prima revisione settimanale (`docs/revisioni/2026-09-09.md`, revisore `fable`) e il
|
||||||
|
giornale del 09/09. Ogni punto e' stato spiegato all'operatore con pro e contro e **deciso da lui**
|
||||||
|
(§3-style: decisioni con l'informazione completa). Issue sul Gitea di casa #2-#6, chiuse con la
|
||||||
|
soluzione scritta (regola globale del 10/09).
|
||||||
|
|
||||||
|
## Le decisioni, in una tabella
|
||||||
|
|
||||||
|
| # | punto | decisione dell'operatore | issue |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | `usde_watch` contava 400 USDE come reward (07/09) | ripara ora + issue | #2 bug |
|
||||||
|
| 2 | cuscino USDC senza sorvegliante (§5.17) | **equity USDC, riconverte da solo** | #3 sviluppo |
|
||||||
|
| 3 | versamento 04/09 solo rilevato; piano €5.000 «in attesa» | **e' l'INTERO versamento previsto**: dichiarato, piano chiuso | #6 sviluppo |
|
||||||
|
| 4 | SCALA-01 A2: da quando contano i 180 giorni | **dal 01/09** (sorvegliante attivo) → gate non prima del **2027-02-28** | #5 bug |
|
||||||
|
| 5 | PREVDAY-01 «senza data» | **tieni 21/06/2027, cabla kill e veto, correggi §4** | #4 bug |
|
||||||
|
|
||||||
|
Sul punto 5 la domanda giusta e' stata «ricorda dove dobbiamo arrivare (50 €/g)»: PREVDAY
|
||||||
|
de-luckato al 15% vale +0,10 di Sharpe sul libro e la somma di *tutti* i lead dell'ondata di agosto
|
||||||
|
vale +0,036 €/giorno. Verso i 50 €/g il candidato vale centesimi in qualunque esito del gate:
|
||||||
|
non si spende una sessione di ricerca per anticipare (a)+(b), e non si butta l'unica gamba short
|
||||||
|
scorrelata oltre a SKH01 per risparmiare un cron. Costo scelto: mezza giornata, una volta.
|
||||||
|
|
||||||
|
## Punto 1 — il lettore dei trade spot aveva DUE limiti, non uno
|
||||||
|
|
||||||
|
Il primo era noto dalla revisione: `trade_history(limit=50)` con 54 ordini. Il secondo l'ho
|
||||||
|
trovato sondando il gateway per ripararlo: `get_user_trades_by_instrument` chiamato senza
|
||||||
|
`historical` restituisce **solo le ultime ~24 ore** — il fill BTC dell'08/09 06:47Z e' invisibile
|
||||||
|
alle 12:55Z del 10/09, quello del 09/09 21:47Z si vede. Il cron gira ogni 24h alle 12:35: sta
|
||||||
|
dentro la finestra per pochi secondi. Un giro saltato avrebbe reso invisibili i trade fra 48h e
|
||||||
|
24h fa, e il delta sarebbe stato attribuito al reward *esattamente come il 07/09*, con un
|
||||||
|
meccanismo diverso.
|
||||||
|
|
||||||
|
Ora `trade_copertura()` dichiara **non leggibile** (somma `None`, e `analizza` non attribuisce:
|
||||||
|
P12) in entrambi i casi — lista lunga quanto il limite (troncata) o ultima lettura oltre 24h+5min —
|
||||||
|
e il record porta `trades_motivo` (P4: il perche'). `TRADE_LIMIT = 1000` e' il `count` massimo di
|
||||||
|
Deribit. Controllo positivo nei test: 54 ordini si sommano tutti.
|
||||||
|
|
||||||
|
La riga del 07/09 in `data/live/usde_watch.jsonl` e' **corretta in loco** con campo `correzione`
|
||||||
|
(originale conservato dentro, copia del file in `usde_watch.jsonl.pre_fix_20260910`): 54 ordini
|
||||||
|
= 8 + 30 + 16 dal registro `usde_convert.jsonl`, 2.434 USDE, reward 0,080934. Il tasso non
|
||||||
|
cambia — la riga era gia' esclusa per `con_trade` — e `rendimento()` oggi da' **3,94% su 14
|
||||||
|
finestre, 12 pagate**.
|
||||||
|
|
||||||
|
## Punto 2 — `cuscino_watch`: la grandezza che nessuno guardava
|
||||||
|
|
||||||
|
Il cuscino richiesto e' `2 × 0,5 × 0,30 × equity_tot` = 30% dell'equity, **derivato** da
|
||||||
|
config e `book.WEIGHT`. La formula e' passata da `usde_convert.py` a `src/live/usde.py`, e
|
||||||
|
`usde_convert` la importa da li': due lettori, una formula (P1 — cinque occorrenze nel
|
||||||
|
progetto di sorveglianti puntati su un bersaglio ridichiarato).
|
||||||
|
|
||||||
|
| stato | criterio | azione |
|
||||||
|
|---|---|---|
|
||||||
|
| OK | slack ≥ 10% del cuscino | niente |
|
||||||
|
| PREAVVISO | 0 ≤ slack < 10% | ⚠️ alla transizione |
|
||||||
|
| SCOPERTO | slack < 0 | 🚨 + `usde_convert --quota 0,64 --esegui` (cuscino + 20% di margine, alzato dal 10% in revisione) |
|
||||||
|
| BLIND | conto non leggibile | ⚠️ alla transizione |
|
||||||
|
|
||||||
|
Guardie della riconversione: `execution_enabled` del libro (un solo interruttore), niente vendita
|
||||||
|
sotto `depeg_warn` (li' decide l'operatore, e l'allarme lo dice), un tentativo ogni 6h, le guardie
|
||||||
|
proprie di `usde_convert` (banda di prezzo, book leggibile, tetto HARD) ereditate lanciando lo
|
||||||
|
script vero (P15). Il marcatore «gia' detto» e' l'esito di `notify`: un invio fallito non consuma
|
||||||
|
la transizione (lezione del debito §5.2). Cron `:53` = dopo il giro del book (`:47`), fuori dal
|
||||||
|
minuto tondo. `monitor_health` lo sorveglia (max 3h).
|
||||||
|
|
||||||
|
📌 **Primo giro a secco (13:00Z): gia' PREAVVISO.** USDC equity $1.361,23 contro $1.335,26
|
||||||
|
richiesti: **slack +$25,97**. Il 09/09 il revisore aveva stimato +$41/+$59; la marcatura del
|
||||||
|
libro dal picco del 06/09 ne aveva consumati altri 15-30 senza che nessuno lo vedesse. E' il
|
||||||
|
caso d'uso del sorvegliante, arrivato prima del sorvegliante.
|
||||||
|
|
||||||
|
## Punto 3 — il versamento del 04/09, dal balance e non dall'equity
|
||||||
|
|
||||||
|
Il salto di equity 13:47→14:47Z diceva +$2.410,14 col mercato dell'ora dentro. Il **balance
|
||||||
|
USDC** di `balance_watch` (cambia solo per P&L realizzato, fee, funding e movimenti) dice
|
||||||
|
1.417,73455581 → 3.832,41432481 = **+$2.414,68**, nessun fill nella finestra, funding su $541
|
||||||
|
di nozionale ~$0,01. Dichiarato con banda $0,05. Il report ora lo mostra come dichiarato e
|
||||||
|
il TWR passa a **+6,66%** (era +7,22% stamattina, +7,83% il 06/09): la coda −0,63% dal 04/09 e'
|
||||||
|
marcatura delle due long, trading da arming **+$13,82**.
|
||||||
|
|
||||||
|
L'operatore ha detto che era **l'intero versamento previsto**: la voce «in attesa di importo e
|
||||||
|
data» di §1 e' chiusa. Resta la lezione di §2: per sei giorni il movimento e' stato solo
|
||||||
|
rilevato, la regola diceva il giorno stesso.
|
||||||
|
|
||||||
|
## Punto 4 — SCALA-01: la data era il punto 5 della SPEC scambiato per il gate
|
||||||
|
|
||||||
|
«Non prima del 2026-10-01» erano i 30 giorni di sorvegliante del punto 5 (§7 della SPEC); A2
|
||||||
|
chiede **≥180 giorni** col criterio passato ogni giorno, e il criterio lo misura solo
|
||||||
|
`scale_watch`, attivo dal 01/09. Le tre letture possibili, con pro e contro, sono nel diario di
|
||||||
|
sessione; l'operatore ha scelto **dal 01/09 → 2027-02-28**. Contare dall'arming avrebbe dato
|
||||||
|
un criterio ricostruito a posteriori, che non prova nulla sul sorvegliante; ridurre A2 a 30 giorni
|
||||||
|
avrebbe allentato un gate senza un costo d'attesa che lo giustifichi (il gradino 1,25x vale meno
|
||||||
|
di €100/mese di versamento).
|
||||||
|
|
||||||
|
Riconciliata anche la data d'arming: **20/06 = TP01 da solo** (commit `4650aa7`), **23/06 = il
|
||||||
|
BOOK** (commit `db738bc`, prima lettura di equity 23/06 22:00Z). CLAUDE.md §0 diceva solo la
|
||||||
|
prima, `config/live.json` e il TWR usano la seconda: erano due eventi, non un errore.
|
||||||
|
|
||||||
|
## Punto 5 — PREVDAY-01 aveva la data; non l'aveva la tabella
|
||||||
|
|
||||||
|
`RESULTS-0822` §54: decisione **2027-06-21**, kill, veto. La tabella §4 diceva «scritto 23/08 ·
|
||||||
|
10 condizioni», e il revisore ha letto quella. Ora la riga ha data, kill, veto e stato (5/7:
|
||||||
|
(a) e (b) sono strutturali, nessun forward le cambia).
|
||||||
|
|
||||||
|
Cablato in `paper_prevday.py` (prima esisteva solo nel testo, a differenza di `paper_dvolspread`):
|
||||||
|
|
||||||
|
| misura | valore al 10/09 |
|
||||||
|
|---|---|
|
||||||
|
| Sharpe forward **giornaliero** (lente del kill; l'oraria +1,19 non si cita, §54) | **+0,95** su 81 giorni, 81 attivi |
|
||||||
|
| kill (< −0,50 su ≥180 g attivi) | **NON MATURO**, leggibile dal ~18/12/2026 |
|
||||||
|
| barre ricostruibili dal feed di oggi | **1.942/1.943** (99,9%); l'unica divergente e' il 26/08 00:00, il giorno del fix di `advance()` |
|
||||||
|
| veto | ok |
|
||||||
|
|
||||||
|
⚠️ **Errore mio, catturato prima di committare:** la prima stesura del veto leggeva «divergenze
|
||||||
|
non crescenti» come `tasso_recente > tasso_totale`, e con UNA barra divergente negli ultimi 30
|
||||||
|
giorni (0,03/g contro 0,01/g) dichiarava VETO. Una guardia piu' stretta del contratto produce
|
||||||
|
allarmi che si impara a ignorare (P14): ora la crescita richiede **≥5 barre recenti E tasso
|
||||||
|
doppio**, dichiarato nel codice e provato nel test con i numeri della serie vera.
|
||||||
|
|
||||||
|
La ricostruzione al 99,9% contro il 95,6% di §54: la misura del 23/08 era stata fatta *prima*
|
||||||
|
del fix di `advance()` del 26/08 (le 67 barre divergenti erano «all'ora del cron», cioe' barre
|
||||||
|
parziali); oggi il feed rivisto e la serie coincidono tranne quella barra.
|
||||||
|
|
||||||
|
## Cose trovate per strada
|
||||||
|
|
||||||
|
- Due timestamp «di comodo» scritti a mano (13:05Z e 13:07Z) invece dell'ora letta (12:57:43Z e
|
||||||
|
13:04:09Z): corretti subito. E' esattamente la regola globale «la data si LEGGE» — vale anche per
|
||||||
|
chi la scrive nel campo `correzione`.
|
||||||
|
- `issue --json` va **prima** del sottocomando; il percorso nella regola globale era
|
||||||
|
`/opt/docker/AI-OS` invece di `/opt/AI-OS` (corretto la mattina, nel test dello strumento).
|
||||||
|
|
||||||
|
## Verifica
|
||||||
|
|
||||||
|
`uv run pytest`: **1060 passati** (erano 1031). Nuovi: `test_cuscino_watch.py` (14),
|
||||||
|
`test_paper_prevday_gate.py` (8), +5 in `test_usde_watch.py`. Revisione del diff affidata a
|
||||||
|
`fable` (agente fresco): esito e correzioni applicate in coda a questo diario.
|
||||||
|
|
||||||
|
## Revisione `fable` (13:10-13:18Z): 15 segnalazioni, 4 medie — tutte verificate, 12 applicate
|
||||||
|
|
||||||
|
| # | segnalazione | verifica | esito |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | `puo_riconvertire` contava anche i NON tentativi (`tentata=False`): un giro `--secco` o un prezzo illeggibile bloccava la riconversione vera per 6h | riprodotta | filtro su `tentata` + test |
|
||||||
|
| 2 | il tetto del venue in `usde_convert.piano` bloccava anche la **vendita**: con `venue_cap_frac` rimesso in config la riconversione automatica sarebbe stata morta | riprodotta (`side=sell`, `ok=False`) | vale solo in acquisto |
|
||||||
|
| 3 | `MARGINE_RIPRISTINO` 10% == `PREAVVISO_FRAC` 10%: dopo ogni 🚨 un ⚠️ per costruzione (P14) | riprodotta col floor di 1 USDE | margine **20%** → quota **0,64**, test |
|
||||||
|
| 4 | `--secco` scriveva nel registro vivo (veicolo del punto 1, e maschera un cron fermo) | riprodotta (riga 13:00:15Z) | non scrive; riga tolta |
|
||||||
|
| 5 | due bersagli di quota: `quota_target` 0,70 in config contro 0,64 del ripristino; il ripristino e' un **ratchet verso il basso** | letto | dichiarato in CLAUDE.md §5.17 e nel docstring: decisione dell'operatore (N9) |
|
||||||
|
| 7 | buco di ≤5 min dentro la tolleranza della finestra 24h | letto | dichiarato (D5) + fonte Deribit documentata |
|
||||||
|
| 8 | `giorni_attivi` legge posizioni REAL, il kill e' MODELED | letto | dichiarato nel docstring (81/81 oggi) |
|
||||||
|
| 10 | il test del cron leggeva un commento nello `.sh`, non la crontab | letto | legge `crontab -l` come `test_book_cadenza` |
|
||||||
|
| 11 | `prevday_target` patchato senza `monkeypatch` | letto | `monkeypatch` |
|
||||||
|
| 13 | «QUATTRO movimenti» in §2: sono TRE, in quattro tratti | letto | corretto |
|
||||||
|
| 14 | «5/7» contro «8/10» di §54: base diversa non dichiarata | letto | dichiarata |
|
||||||
|
| 6, 9, 12, 15 | verifiche che reggono (`sys.executable`, `cwd`, `piano()` in SCOPERTO, ricostruzione ≡ `advance`) | — | — |
|
||||||
|
|
||||||
|
Non applicato: il lock fra cron e conversione a mano (DUBBIO 2) — dichiarato come limite nel
|
||||||
|
docstring. Il DUBBIO 1 (ratchet) e' la domanda aperta per l'operatore.
|
||||||
|
|
||||||
|
## Coda (13:55Z →): il riacquisto automatico — issue #7
|
||||||
|
|
||||||
|
Alla domanda aperta (ratchet verso il basso) l'operatore ha risposto «crea una strategia per
|
||||||
|
riportarla a quota in autonomia». La risposta ingenua — ricomprare fino a `quota_target` 0,70 —
|
||||||
|
e' sbagliata per costruzione: a 0,70 lo slack e' zero, la prima ora in perdita vende, il
|
||||||
|
riacquisto ricompra, e cosi' via a 6 bps al giro. Serve **isteresi con un bersaglio unico**:
|
||||||
|
|
||||||
|
| slack / cuscino | stato | azione |
|
||||||
|
|---|---|---|
|
||||||
|
| < 0 | SCOPERTO | 🚨 vende USDE → 0,20 |
|
||||||
|
| 0 – 0,10 | PREAVVISO | ⚠️ |
|
||||||
|
| 0,10 – 0,40 | OK | — |
|
||||||
|
| > 0,40 | **ECCEDENTE** | 📌 compra USDE → 0,20 |
|
||||||
|
|
||||||
|
Bersaglio 0,20 ⇒ quota **0,64**. Per oscillare lo slack deve muoversi di ±0,20×cuscino = ±6%
|
||||||
|
dell'equity, cioe' **±8,6% di equity USDC (~$380)** perche' slack = 0,7·USDC − 0,3·USDE (la prima
|
||||||
|
stesura diceva ±6%/$270: corretta in revisione): a leva 0,26x sono ~33% di mercato. Un bonifico alza lo slack di
|
||||||
|
0,7×importo: sopra ~9% dell'equity (~$380) il riacquisto scatta da solo — la regola di §1
|
||||||
|
«dopo un bonifico si rilancia `usde_convert`» non dipende piu' da qualcuno che se ne ricordi.
|
||||||
|
Le tre frazioni vivono in `config/live.json` (`usde.bande_cuscino` verifica 0 < preavviso <
|
||||||
|
margine < riacquisto), `quota_target` 0,70 e' stato tolto. Soglie dichiarate, non ottimizzate
|
||||||
|
(M8): il costo di un giro e' ~$0,2, la frequenza attesa qualche giro al mese nei tratti mossi.
|
||||||
|
|
||||||
|
📌 **Trovato al primo giro vero del cron (13:53Z): l'allerta PREAVVISO non e' partita.** Il token
|
||||||
|
era valido (`getMe` ok); il testo conteneva «(< 10% del cuscino)» e Telegram in `parse_mode=HTML`
|
||||||
|
rifiuta un `<` nudo. Testi riscritti senza `<`, `allerta_errore` registrato nel record (P3),
|
||||||
|
test che legge il sorgente delle `notify`. Un sorvegliante nuovo che non riesce a parlare al
|
||||||
|
primo giro e' il caso di P2: si valida sul segnale E sul trasporto.
|
||||||
|
|
||||||
|
Stato al giro a secco delle 14:0xZ: quota 69,3%, slack +$30 = PREAVVISO. Il piano a 0,64 e'
|
||||||
|
valido (SELL 237 USDE, slack dopo +$267): il sorvegliante lo esegue alla prima ora sotto zero;
|
||||||
|
allinearlo subito e' una riga a mano.
|
||||||
|
|
||||||
|
### Seconda revisione `fable` (14:04Z): 10 segnalazioni, 1 ALTA, 3 MEDIE — applicate 9
|
||||||
|
|
||||||
|
| # | segnalazione | esito |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 ALTA | il `<` arrivava ancora a Telegram dal campo `motivo` («(< 6h)») dentro l'allerta 🚨: il 🚨 piu' importante perso fino a 4h | escape nel **sink** `notifier.notify` (`html.escape` su titolo e valori; nessuno dei 45 chiamanti passa tag: grep) + motivi senza `<` |
|
||||||
|
| 2 MEDIA | il test sul sorgente era parziale (codice morto, cieco ai valori interpolati) | sostituito: cattura la POST e verifica `<` |
|
||||||
|
| 3 MEDIA | ECCEDENTE con guasto persistente = «acquisto FALLITA» ogni 6h per sempre | da ECCEDENTE dopo un fallimento si riprova ogni 24h; da SCOPERTO resta 6h |
|
||||||
|
| 4 MEDIA | bande fuori ordine ⇒ traceback ingoiato dal cron, nessun record, «fermo» invece di «rotto» | `giudica` ⇒ BLIND con motivo; record e allerta scritti |
|
||||||
|
| 5-7 | CLAUDE.md §1 diceva ancora «nessuno sorveglia»; §5.17 «tre stati»; `_nota_usde` con `quota_target` | corretti |
|
||||||
|
| 8 | «±6% di equity USDC» sbagliato di 0,7: slack = 0,7·USDC − 0,3·USDE ⇒ ±8,6% (~$380) | corretto in 4 posti |
|
||||||
|
| 9 | `TOLL` ridichiarato nel test | importato da `usde_convert` |
|
||||||
|
| 10 | esempi `--quota 0.70` nel docstring di `usde_convert` | 0,64 |
|
||||||
|
|
||||||
|
Verifiche numeriche del revisore (loop su `piano()` con passo 1 USDE, TOLL, min order): bonifico
|
||||||
|
+$2.000 → 11 acquisti → OK, slack +$387; perdita −$50 → 3 vendite → OK, +$264; conto tutto USDC →
|
||||||
|
20 acquisti → OK. **Nessuna sequenza vendita→acquisto→vendita.** Non applicato: `min(q*, cap)` con
|
||||||
|
il tetto del venue (dubbio 3: un conto al tetto resta ECCEDENTE e ora allerta una volta al giorno).
|
||||||
|
Dubbio 4, che vale una riga in §5.17: **0,64 e' una derivazione dell'autore, non la quota decisa
|
||||||
|
dall'operatore il 30/08 (0,70)**; ~$10/anno di resa di differenza; da confermare.
|
||||||
|
|
||||||
|
### Allineamento eseguito (14:16:38Z, ordine dell'operatore «allinea subito a 0.64, esegui»)
|
||||||
|
|
||||||
|
`usde_convert --quota 0.64 --esegui`: SELL 100 + 100 + 38 USDE a 1,0000 medio (limite 0,9995,
|
||||||
|
fill al book), quota 69,4% → **64,02%**, conto USDC $1.602,79 + USDE 2.851,95 = $4.454,74.
|
||||||
|
Slack dopo **+$266** su $1.336 di cuscino: `cuscino_watch --secco` dice **OK** (era PREAVVISO a
|
||||||
|
+$29). Costo: ~$0,1 di spread, fee zero. Registro in `data/live/usde_convert.jsonl`.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Risk-off ±2h intorno a FOMC e CPI su TP01 — REFUTED, e il segno e' l'opposto
|
||||||
|
|
||||||
|
**Data:** 2026-09-10 14:52-14:55Z · **Script:** `scripts/research/r0910_macro_riskoff.py` (N11: il
|
||||||
|
verdetto lo calcola lo script) · **Issue:** #9 · **Origine:** domanda dell'operatore da un articolo su
|
||||||
|
EA forex a griglia con «agente fondamentale» che approva/rifiuta i segnali intorno ai dati macro.
|
||||||
|
|
||||||
|
## Cosa si e' misurato
|
||||||
|
|
||||||
|
TP01 canonico (posizione decisa alla chiusura giornaliera, tenuta 24 barre 1h), 2019-03 → 2026-09,
|
||||||
|
65.676 ore. Variante RISK-OFF: esposizione zero nelle barre che intersecano [t−2h, t+2h] intorno a
|
||||||
|
**61 dichiarazioni FOMC** (14:00 ET, piu' le due d'emergenza di marzo 2020, notation vote esclusi) e
|
||||||
|
**88 release CPI** (08:30 ET), con fee di chiusura e riapertura. Calendario **letto dal web** (Fed e
|
||||||
|
BLS), non ricordato; l'archivio BLS raggruppa per anno di riferimento e le release di gennaio/febbraio
|
||||||
|
vanno spostate di +1 anno (verificato sulla schedule 2026). 684 ore in finestra = **1,04%** del tempo.
|
||||||
|
|
||||||
|
Verdetto pre-registrato prima del primo numero: LEAD solo se (a) ΔSharpe con fee > 0, (b) Δ al ≥95°
|
||||||
|
percentile del null location-matched (stessi conteggi per anno, stesse ore del giorno, giorni a caso,
|
||||||
|
1.000 estrazioni — M18), (c) Δ > 0 in ≥70% degli anni (M9).
|
||||||
|
|
||||||
|
## Numeri
|
||||||
|
|
||||||
|
| | Sharpe giornaliero | drift/anno |
|
||||||
|
|---|---|---|
|
||||||
|
| TP01 base | 1,302 | +15,73% |
|
||||||
|
| risk-off con fee | 1,205 | +14,49% |
|
||||||
|
| risk-off senza fee | 1,228 | +14,77% |
|
||||||
|
| **Δ** | **−0,097** | **−1,24%** ≈ **−0,14 €/giorno** a $4.455 |
|
||||||
|
|
||||||
|
Per tipo: FOMC ΔSh −0,012 (P&L rinunciato +0,79%), **CPI −0,084 (+6,36%)**. Per anno: Δ negativo in
|
||||||
|
6/8, positivo solo 2024 e 2026 (+0,02/+0,03).
|
||||||
|
|
||||||
|
Null location-matched: ΔSh mediana −0,033 [p05 −0,095, p95 +0,027]. **Il Δ vero sta al 4,2°
|
||||||
|
percentile**: le finestre vere sono fra le PEGGIORI da appiattire, cioe' fra le migliori in cui
|
||||||
|
essere esposti. Dentro la finestra TP01 fa **+1,05 bp/ora contro +0,17 fuori** (t = 1,57 su 684 ore).
|
||||||
|
|
||||||
|
(a) FAIL · (b) FAIL · (c) 2/8 FAIL ⇒ **REFUTED**.
|
||||||
|
|
||||||
|
## Lettura
|
||||||
|
|
||||||
|
1. **Il risk-off toglie P&L, non rischio.** TP01 e' long-flat vol-targeted: nelle ore dei dati macro
|
||||||
|
il trend che sta cavalcando accelera piu' spesso di quanto si rovesci, e l'1% del tempo in finestra
|
||||||
|
porta il 7% del guadagno cumulato. E' la stessa lezione del weekend (§3, 17/07: il 38% del gross
|
||||||
|
nel 31% del tempo): **la coda del trend vive nelle ore «pericolose»**.
|
||||||
|
2. **Il segno opposto non e' un lead.** «Esporsi DI PIU' intorno ai dati» sarebbe un'ipotesi nuova
|
||||||
|
(M12), con t = 1,57 e 684 ore: sotto qualunque MDE, e la selezione sull'esito la vieta. Non si apre.
|
||||||
|
3. Il gate «fondamentale» dell'articolo, applicato al nostro libro, costerebbe **−1,24%/anno di drift**
|
||||||
|
— la stessa taglia del funding non modellato (−2,16%), ma con segno scelto da chi lo propone.
|
||||||
|
4. Coerente con `macro-regime-gate` (29/06, ridondante col trend) e con la conclusione di §3: **ogni
|
||||||
|
risk-off parte REFUTED** e questo lo e' rimasto.
|
||||||
|
|
||||||
|
Non modellato (dichiarato): funding (identico nelle due varianti salvo l'1% delle ore); slippage
|
||||||
|
della finestra (rende il risk-off *piu'* costoso, non meno). Costo: 3 minuti di calcolo, 0 ordini.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Giornale di bordo — 2026-09-02
|
||||||
|
|
||||||
|
*Scritto 2026-09-03T00:36:49+00:00. Numeri misurati; nessuna interpretazione automatica.*
|
||||||
|
|
||||||
|
## Mercato
|
||||||
|
|
||||||
|
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **BTC** | $77,312.50 | -0.15% | -2.17% | +21.78% | 42.9% | ↑ ↑ ↑ (3/3 su) | 37.2 (16° pctl 1a) |
|
||||||
|
| **ETH** | $2,391.55 | -1.11% | -4.63% | +28.63% | 68.5% | ↑ ↑ ↑ (3/3 su) | 50.9 (17° pctl 1a) |
|
||||||
|
|
||||||
|
## Libro
|
||||||
|
|
||||||
|
Equity **$2,045.82** · nozionale lordo $560 · leva lorda 0.27x · barra dati 2026-09-02 · 24 giri
|
||||||
|
|
||||||
|
| | TP01 | SKH01 | target | posizione | azione |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **BTC** | +0.454 | flat | $+348 | $+347 | HOLD (a target) |
|
||||||
|
| **ETH** | +0.276 | flat | $+212 | $+213 | HOLD (a target) |
|
||||||
|
|
||||||
|
## P&L
|
||||||
|
|
||||||
|
- giorno: **$-4.95** di equity (24 letture)
|
||||||
|
- realizzato: -0.35 netto su 1 round-trip chiusi · 2 fill · fee 0.0045
|
||||||
|
- cumulato dall'arming: **$+1,447.76** di equity — di cui $+1,399.39 versati/prelevati -> trading **$+48.37** · TWR **+10.47%**
|
||||||
|
|
||||||
|
## Salute
|
||||||
|
|
||||||
|
- giri di `book_execute`: 24/24
|
||||||
|
- eta' feed SKH all'ultimo giro: 0 min
|
||||||
|
|
||||||
|
## Lettura
|
||||||
|
|
||||||
|
*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.*
|
||||||
|
|
||||||
|
- `[stato]` A mercato su BTC $+347 (TP01 +0.454, SKH01 flat) e ETH $+213 (TP01 +0.276, SKH01 flat).
|
||||||
|
- `[leva]` Leva lorda **0.27x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.73x.
|
||||||
|
- `[pnl]` Equity $-4.95 con 2 fill e 1 round-trip chiusi (realizzato $-0.35 netto).
|
||||||
|
- `[drawdown]` 📌 Equity **-1.34%** sotto il picco ($2,073.59). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
|
||||||
|
- `[vol]` BTC: implicita 37.2 contro realizzata 30g 42.9 (sotto); DVOL al 16° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
|
||||||
|
- `[vol]` ETH: implicita 50.9 contro realizzata 30g 68.5 (sotto); DVOL al 17° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
|
||||||
|
- `[evidenza]` 📌 Campione a oggi: **71 giorni, 30 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi.
|
||||||
|
|
||||||
|
## Analisi (agente)
|
||||||
|
|
||||||
|
*Scritta da `claude-opus-5` il 2026-09-03T00:37:49+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
|
||||||
|
|
||||||
|
Il fatto nuovo e' l'equity a $2,045.82: sotto ogni chiusura registrata dopo il versamento del 25/08, dove il minimo era $2,047.39 del 30/08. Non viene dal trading — realizzato $-0.35 su un round-trip, fee 0.0045, due fill — ma dal prezzo su due posizioni tenute a target per tutte le 24 letture.
|
||||||
|
|
||||||
|
Il picco da cui si misura il -1.34% ($2,073.59) non compare fra le chiusure giornaliere: e' una lettura oraria, quindi quel drawdown vive su una griglia piu' fine della tabella dei giorni.
|
||||||
|
|
||||||
|
La tensione del giorno e' fra le scale: TSMOM 3/3 su su entrambi gli asset, ma 1g e 7g negativi (BTC -2.17%, ETH -4.63% a 7g) contro 30g ancora +21.78% e +28.63%. Il libro resta long perche' la gamba lunga tiene; una lettura altrettanto compatibile e' che i 30g stiano semplicemente restituendo il rialzo accumulato. Quale delle due, non e' misurabile qui.
|
||||||
|
|
||||||
|
Insolito il pacchetto volatilita': implicita sotto la realizzata su entrambi (37.2 contro 42.9, 50.9 contro 68.5) con DVOL al 16° e 17° percentile dell'anno, mentre il gate di VRP01 legge l'IV-rank (0.06 e 0.12) e resta fermo — guarda una cosa diversa da quello scarto.
|
||||||
|
|
||||||
|
Meriterebbe un occhio umano il perche' il $-4.95 arrivi con salute perfetta, 24/24 giri e feed a 0 min. Il campione resta 71 giorni e 30 round-trip, con $+48.37 di trading sui $+1,447.76 cumulati.
|
||||||
|
|
||||||
|
## Nota
|
||||||
|
|
||||||
|
*(vuota — campo dell'operatore)*
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Giornale di bordo — 2026-09-03
|
||||||
|
|
||||||
|
*Scritto 2026-09-04T00:37:16+00:00. Numeri misurati; nessuna interpretazione automatica.*
|
||||||
|
|
||||||
|
## Mercato
|
||||||
|
|
||||||
|
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **BTC** | $81,286.00 | +5.14% | +1.27% | +26.90% | 45.5% | ↑ ↑ ↑ (3/3 su) | 39.8 (38° pctl 1a) |
|
||||||
|
| **ETH** | $2,507.90 | +4.87% | -0.15% | +34.20% | 69.8% | ↑ ↑ ↑ (3/3 su) | 53.3 (27° pctl 1a) |
|
||||||
|
|
||||||
|
## Libro
|
||||||
|
|
||||||
|
Equity **$2,074.42** · nozionale lordo $562 · leva lorda 0.27x · barra dati 2026-09-03 · 24 giri
|
||||||
|
|
||||||
|
| | TP01 | SKH01 | target | posizione | azione |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **BTC** | +0.453 | flat | $+352 | $+349 | HOLD (a target) |
|
||||||
|
| **ETH** | +0.274 | flat | $+213 | $+213 | HOLD (a target) |
|
||||||
|
|
||||||
|
## P&L
|
||||||
|
|
||||||
|
- giorno: **$+28.60** di equity (24 letture)
|
||||||
|
- realizzato: -0.14 netto su 4 round-trip chiusi · 4 fill · fee 0.0095
|
||||||
|
- cumulato dall'arming: **$+1,476.36** di equity — di cui $+1,399.39 versati/prelevati -> trading **$+76.97** · TWR **+12.02%**
|
||||||
|
|
||||||
|
## Salute
|
||||||
|
|
||||||
|
- giri di `book_execute`: 24/24
|
||||||
|
- eta' feed SKH all'ultimo giro: 0 min
|
||||||
|
|
||||||
|
## Lettura
|
||||||
|
|
||||||
|
*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.*
|
||||||
|
|
||||||
|
- `[stato]` A mercato su BTC $+349 (TP01 +0.453, SKH01 flat) e ETH $+213 (TP01 +0.274, SKH01 flat).
|
||||||
|
- `[leva]` Leva lorda **0.27x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.73x.
|
||||||
|
- `[pnl]` Equity $+28.60 con 4 fill e 4 round-trip chiusi (realizzato $-0.14 netto).
|
||||||
|
- `[vol]` BTC: implicita 39.8 contro realizzata 30g 45.5 (sotto); DVOL al 38° pctl di un anno; IV-rank espandente 0.14 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
|
||||||
|
- `[vol]` ETH: implicita 53.3 contro realizzata 30g 69.8 (sotto); DVOL al 27° pctl di un anno; IV-rank espandente 0.14 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
|
||||||
|
- `[evento]` 📌 BTC: giornata a +5.14%, **2.2 deviazioni** giornaliere (sd implicita dalla RV30 = 2.38%).
|
||||||
|
- `[evidenza]` 📌 Campione a oggi: **72 giorni, 34 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi.
|
||||||
|
|
||||||
|
## Analisi (agente)
|
||||||
|
|
||||||
|
*Scritta da `claude-opus-5` il 2026-09-04T00:38:30+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
|
||||||
|
|
||||||
|
Il fatto nuovo non e' il segno, e' la taglia: $+28.60 e un'equity a $2,074.42, la piu' alta delle dieci sedute mostrate, dopo giorni in cui il delta si e' mosso fra −17.87 e +12.06. Arriva da un BTC a +5.14%, 2.2 deviazioni, mentre il libro era gia' a target su entrambe le gambe: con nozionale lordo $562 e leva 0.27x, l'aumento e' rivalutazione di una posizione ferma, non l'esito di una decisione presa oggi. Il realizzato lo conferma — −0.14 netto su 4 round-trip, rumore di fee (0.0095) — e le posizioni restano $+349 e $+213, in linea con le sedute precedenti.
|
||||||
|
|
||||||
|
Insolito il quadro vol: entrambe le implicite sotto la realizzata 30g (39.8 contro 45.5, 53.3 contro 69.8), coi DVOL al 38° e 27° percentile dell'anno, in una giornata a due deviazioni. Non misurabile qui se sia salita la realizzata oggi o se l'implicita non abbia seguito.
|
||||||
|
|
||||||
|
Lettura alternativa, ugualmente compatibile: BTC fa +5.14% in un giorno ma +1.27% in sette, ed ETH −0.15%; buona parte del guadagno e' recupero dentro la settimana, non nuova direzione, benche' TSMOM sia 3/3 su su entrambi.
|
||||||
|
|
||||||
|
Un solo giorno pesa molto su un trading da $+76.97 in 72 giorni e 34 round-trip. Varrebbe un controllo umano sui 4 fill di una giornata chiusa a target: dovrebbero essere ri-taglie.
|
||||||
|
|
||||||
|
## Nota
|
||||||
|
|
||||||
|
*(vuota — campo dell'operatore)*
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Giornale di bordo — 2026-09-04
|
||||||
|
|
||||||
|
*Scritto 2026-09-05T00:36:37+00:00. Numeri misurati; nessuna interpretazione automatica.*
|
||||||
|
|
||||||
|
## Mercato
|
||||||
|
|
||||||
|
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **BTC** | $79,660.50 | -2.00% | +2.33% | +23.30% | 46.6% | ↑ ↑ ↑ (3/3 su) | 38.0 (24° pctl 1a) |
|
||||||
|
| **ETH** | $2,457.05 | -2.03% | +0.56% | +28.81% | 70.5% | ↑ ↑ ↑ (3/3 su) | 51.1 (18° pctl 1a) |
|
||||||
|
|
||||||
|
## Libro
|
||||||
|
|
||||||
|
Equity **$4,483.73** · nozionale lordo $1,173 · leva lorda 0.26x · barra dati 2026-09-04 · 24 giri
|
||||||
|
|
||||||
|
| | TP01 | SKH01 | target | posizione | azione |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **BTC** | +0.428 | flat | $+719 | $+717 | HOLD (a target) |
|
||||||
|
| **ETH** | +0.270 | flat | $+455 | $+456 | HOLD (a target) |
|
||||||
|
|
||||||
|
## P&L
|
||||||
|
|
||||||
|
- giorno: **$+2,409.31** di equity (24 letture) — di cui **$+2,410.14 movimenti di capitale** -> trading **$-0.83**
|
||||||
|
- realizzato: -0.02 netto su 2 round-trip chiusi · 5 fill · fee 0.2328
|
||||||
|
- cumulato dall'arming: **$+3,885.67** di equity — di cui $+3,809.53 versati/prelevati -> trading **$+76.14** · TWR **+11.70%**
|
||||||
|
|
||||||
|
## Salute
|
||||||
|
|
||||||
|
- giri di `book_execute`: 24/24
|
||||||
|
- eta' feed SKH all'ultimo giro: 0 min
|
||||||
|
|
||||||
|
## Lettura
|
||||||
|
|
||||||
|
*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.*
|
||||||
|
|
||||||
|
- `[movimento]` 📌 **Movimento di capitale $+2,410.14** fra le 13:47 e le 14:47 UTC: salto +116.8% contro un massimo spiegabile dal mercato di ±0.6% al tetto di leva. Scorporato dal P&L di trading qui sotto.
|
||||||
|
- `[stato]` A mercato su BTC $+717 (TP01 +0.428, SKH01 flat) e ETH $+456 (TP01 +0.270, SKH01 flat).
|
||||||
|
- `[leva]` Leva lorda **0.26x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.74x.
|
||||||
|
- `[pnl]` P&L di trading $-0.83 (equity $+2409.31 al lordo del movimento di capitale) con 5 fill e 2 round-trip chiusi (realizzato $-0.02 netto).
|
||||||
|
- `[vol]` BTC: implicita 38.0 contro realizzata 30g 46.6 (sotto); DVOL al 24° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
|
||||||
|
- `[vol]` ETH: implicita 51.1 contro realizzata 30g 70.5 (sotto); DVOL al 18° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
|
||||||
|
- `[evidenza]` 📌 Campione a oggi: **73 giorni, 36 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi.
|
||||||
|
|
||||||
|
## Analisi (agente)
|
||||||
|
|
||||||
|
*Scritta da `claude-opus-5` il 2026-09-05T00:37:58+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
|
||||||
|
|
||||||
|
Il fatto della giornata non è il mercato ma la cassa: l'equity passa da $2,074.42 a $4,483.73, e il salto di $+2,410.14 fra le 13:47 e le 14:47 UTC è il secondo dopo il delta +1,414.57 del 25/08. Il libro l'ha assorbito subito: le posizioni, ferme per dieci giorni fra $+344 e $+364 su BTC e fra $+205 e $+218 su ETH, oggi valgono $+717 e $+456, entrambe a target, a leva lorda 0.26x su un tetto di 1.00x. Nessuna scelta discrezionale: TP01 +0.428 e +0.270, SKH01 flat.
|
||||||
|
|
||||||
|
Il numero da maneggiare con cura è il $-0.83 di trading. BTC e ETH chiudono a -2.00% e -2.03% con $1,173 di nozionale lordo lungo: l'intervallo che contiene il versamento esce intero dal conteggio, quindi quel $-0.83 non copre la giornata piena. Il realizzato, $-0.02 netto su 2 round-trip e 0.2328 di fee, è compatibile sia con una giornata quasi neutra sia con una perdita di mercato finita fuori dal segmento — qui non si separa.
|
||||||
|
|
||||||
|
Insolito il quadro vol: implicita sotto la realizzata su entrambi (38.0 contro 46.6; 51.1 contro 70.5), DVOL al 24° e 18° percentile; VRP01 resta fermo per l'IV-rank (0.08 e 0.12), non per questo.
|
||||||
|
|
||||||
|
Salute piena, 24/24 giri e feed a 0 min; campione 73 giorni e 36 round-trip. Varrebbe che l'operatore confermi a mano importo e ora del versamento: la Nota è vuota.
|
||||||
|
|
||||||
|
## Nota
|
||||||
|
|
||||||
|
*(vuota — campo dell'operatore)*
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Giornale di bordo — 2026-09-05
|
||||||
|
|
||||||
|
*Scritto 2026-09-06T00:37:28+00:00. Numeri misurati; nessuna interpretazione automatica.*
|
||||||
|
|
||||||
|
## Mercato
|
||||||
|
|
||||||
|
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **BTC** | $79,835.00 | +0.22% | +2.03% | +24.19% | 46.4% | ↑ ↑ ↑ (3/3 su) | 38.7 (29° pctl 1a) |
|
||||||
|
| **ETH** | $2,481.10 | +0.98% | +0.93% | +30.40% | 70.4% | ↑ ↑ ↑ (3/3 su) | 51.7 (21° pctl 1a) |
|
||||||
|
|
||||||
|
## Libro
|
||||||
|
|
||||||
|
Equity **$4,490.66** · nozionale lordo $1,161 · leva lorda 0.26x · barra dati 2026-09-05 · 24 giri
|
||||||
|
|
||||||
|
| | TP01 | SKH01 | target | posizione | azione |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **BTC** | +0.420 | flat | $+708 | $+711 | HOLD (a target) |
|
||||||
|
| **ETH** | +0.268 | flat | $+451 | $+450 | HOLD (a target) |
|
||||||
|
|
||||||
|
## P&L
|
||||||
|
|
||||||
|
- giorno: **$+6.93** di equity (24 letture)
|
||||||
|
- realizzato: -0.16 netto su 3 round-trip chiusi · 3 fill · fee 0.0068
|
||||||
|
- cumulato dall'arming: **$+3,892.60** di equity — di cui $+3,809.53 versati/prelevati -> trading **$+83.07** · TWR **+11.87%**
|
||||||
|
|
||||||
|
## Salute
|
||||||
|
|
||||||
|
- giri di `book_execute`: 24/24
|
||||||
|
- eta' feed SKH all'ultimo giro: 0 min
|
||||||
|
|
||||||
|
## Lettura
|
||||||
|
|
||||||
|
*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.*
|
||||||
|
|
||||||
|
- `[stato]` A mercato su BTC $+711 (TP01 +0.420, SKH01 flat) e ETH $+450 (TP01 +0.268, SKH01 flat).
|
||||||
|
- `[leva]` Leva lorda **0.26x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.74x.
|
||||||
|
- `[pnl]` Equity $+6.93 con 3 fill e 3 round-trip chiusi (realizzato $-0.16 netto).
|
||||||
|
- `[vol]` BTC: implicita 38.7 contro realizzata 30g 46.4 (sotto); DVOL al 29° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
|
||||||
|
- `[vol]` ETH: implicita 51.7 contro realizzata 30g 70.4 (sotto); DVOL al 21° pctl di un anno; IV-rank espandente 0.13 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
|
||||||
|
- `[evidenza]` 📌 Campione a oggi: **74 giorni, 39 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi.
|
||||||
|
|
||||||
|
## Analisi (agente)
|
||||||
|
|
||||||
|
*Scritta da `claude-opus-5` il 2026-09-06T00:38:25+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
|
||||||
|
|
||||||
|
Il fatto della giornata non è di oggi: il 04/09 l'equity è passata da $2,074.42 a $4,483.73, +$2,409.31 in un giorno, e questa è la prima pagina intera scritta sul libro nuovo. La forma non è cambiata — leva lorda 0.26x, HOLD a target su entrambe le gambe, TP01 +0.420 e +0.268, SKH01 flat su tutte e due — ma le posizioni sì: $+711 e $+450 contro i $+347 e $+213 del 02/09. È cambiata la taglia, non il comportamento; e il $+6.93 di oggi va letto su una base doppia, dove la stessa cifra in dollari pesa meno di quanto pesava a fine agosto.
|
||||||
|
|
||||||
|
Il cumulato lo dice più chiaramente: $+3,892.60, di cui $+3,809.53 versati o prelevati e $+83.07 di trading in 74 giorni e 39 round-trip. La cifra citabile resta il TWR, +11.87%.
|
||||||
|
|
||||||
|
Insolito, e non spiegabile da questa pagina: implicita sotto realizzata su entrambi (38.7 contro 46.4, 51.7 contro 70.4) con DVOL al 29° e 21° percentile, mentre i 30 giorni segnano +24.19% e +30.40%. Lettura alternativa altrettanto compatibile col +$6.93: il mark-to-market di due long in una giornata salita (+0.22% e +0.98%), non un merito di selezione — con 3 fill e realizzato $-0.16 netto, qui non è distinguibile.
|
||||||
|
|
||||||
|
Un controllo umano lo merita il movimento del 04/09: che abbia la sua riga di giornale, non ricostruita dopo.
|
||||||
|
|
||||||
|
## Nota
|
||||||
|
|
||||||
|
*(vuota — campo dell'operatore)*
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Giornale di bordo — 2026-09-06
|
||||||
|
|
||||||
|
*Scritto 2026-09-07T00:36:43+00:00. Numeri misurati; nessuna interpretazione automatica.*
|
||||||
|
|
||||||
|
## Mercato
|
||||||
|
|
||||||
|
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **BTC** | $80,376.00 | +0.68% | +3.44% | +23.88% | 46.4% | ↑ ↑ ↑ (3/3 su) | 39.3 (35° pctl 1a) |
|
||||||
|
| **ETH** | $2,515.10 | +1.37% | +4.04% | +31.44% | 70.4% | ↑ ↑ ↑ (3/3 su) | 53.2 (27° pctl 1a) |
|
||||||
|
|
||||||
|
## Libro
|
||||||
|
|
||||||
|
Equity **$4,502.93** · nozionale lordo $1,175 · leva lorda 0.26x · barra dati 2026-09-06 · 24 giri
|
||||||
|
|
||||||
|
| | TP01 | SKH01 | target | posizione | azione |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **BTC** | +0.420 | flat | $+710 | $+717 | SELL $-7 |
|
||||||
|
| **ETH** | +0.268 | flat | $+453 | $+458 | HOLD (a target) |
|
||||||
|
|
||||||
|
## P&L
|
||||||
|
|
||||||
|
- giorno: **$+12.27** di equity (24 letture)
|
||||||
|
- realizzato: +0.13 netto su 1 round-trip chiusi · 1 fill · fee 0.0028
|
||||||
|
- cumulato dall'arming: **$+3,904.87** di equity — di cui $+3,834.43 versati/prelevati -> trading **$+70.44** · TWR **+8.02%**
|
||||||
|
|
||||||
|
## Salute
|
||||||
|
|
||||||
|
- giri di `book_execute`: 24/24
|
||||||
|
- eta' feed SKH all'ultimo giro: 0 min
|
||||||
|
|
||||||
|
## Lettura
|
||||||
|
|
||||||
|
*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.*
|
||||||
|
|
||||||
|
- `[stato]` A mercato su BTC $+717 (TP01 +0.420, SKH01 flat) e ETH $+458 (TP01 +0.268, SKH01 flat).
|
||||||
|
- `[leva]` Leva lorda **0.26x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.74x.
|
||||||
|
- `[pnl]` Equity $+12.27 con 1 fill e 1 round-trip chiusi (realizzato $+0.13 netto).
|
||||||
|
- `[vol]` BTC: implicita 39.3 contro realizzata 30g 46.4 (sotto); DVOL al 35° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
|
||||||
|
- `[vol]` ETH: implicita 53.2 contro realizzata 30g 70.4 (sotto); DVOL al 27° pctl di un anno; IV-rank espandente 0.14 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
|
||||||
|
- `[evidenza]` 📌 Campione a oggi: **75 giorni, 40 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi.
|
||||||
|
|
||||||
|
## Analisi (agente)
|
||||||
|
|
||||||
|
*Scritta da `claude-opus-5` il 2026-09-07T00:37:44+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
|
||||||
|
|
||||||
|
Il fatto del giorno non e' il $+12.27: e' che dopo il salto del 04/09 il libro ha ripreso la sua forma abituale a una taglia nuova. Le posizioni sono cresciute con l'equity ($+717 e $+458 contro $+349 e $+213 del 03/09), non col segnale: TP01 sta a 0.420 e 0.268, parziale su entrambe le gambe, e la leva lorda resta 0.26x su un tetto di 1.00x. SKH01 e' flat su BTC e su ETH — il quarto del libro che fa breakout oggi non porta nulla, e da quanti giorni sia cosi' non e' misurabile qui.
|
||||||
|
|
||||||
|
L'attivita' e' scesa: 1 fill e 1 round-trip contro i 3-5 dei giorni scorsi, e l'unica azione e' un SELL $-7 su una posizione da $+717. Vale un'occhiata umana, senza allarme: le soglie d'ordine sono in dollari assoluti e l'equity ha cambiato scala.
|
||||||
|
|
||||||
|
Cumulato $+3,904.87, di cui $+3,834.43 versati; il trading vale $+70.44 su 75 giorni e 40 round-trip — abbastanza poco che un movimento di capitale scorporato male se lo mangerebbe intero.
|
||||||
|
|
||||||
|
Una lettura alternativa dello stesso dato: implicite sotto le realizzate su entrambi (39.3 contro 46.4, 53.2 contro 70.4) e DVOL al 35° e 27° percentile dopo trenta giorni a +23.88% e +31.44%. Le regole ci leggono il gate VRP01 chiuso; e' altrettanto compatibile un rialzo che il mercato delle opzioni non sta pagando.
|
||||||
|
|
||||||
|
## Nota
|
||||||
|
|
||||||
|
*(vuota — campo dell'operatore)*
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Giornale di bordo — 2026-09-07
|
||||||
|
|
||||||
|
*Scritto 2026-09-08T00:36:58+00:00. Numeri misurati; nessuna interpretazione automatica.*
|
||||||
|
|
||||||
|
## Mercato
|
||||||
|
|
||||||
|
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **BTC** | $79,100.00 | -1.59% | +0.68% | +21.85% | 47.1% | ↑ ↑ ↑ (3/3 su) | 38.8 (31° pctl 1a) |
|
||||||
|
| **ETH** | $2,490.35 | -0.98% | +0.91% | +29.98% | 70.6% | ↑ ↑ ↑ (3/3 su) | 52.6 (25° pctl 1a) |
|
||||||
|
|
||||||
|
## Libro
|
||||||
|
|
||||||
|
Equity **$4,484.18** · nozionale lordo $1,156 · leva lorda 0.26x · barra dati 2026-09-07 · 24 giri
|
||||||
|
|
||||||
|
| | TP01 | SKH01 | target | posizione | azione |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **BTC** | +0.420 | flat | $+707 | $+704 | HOLD (a target) |
|
||||||
|
| **ETH** | +0.268 | flat | $+451 | $+452 | HOLD (a target) |
|
||||||
|
|
||||||
|
## P&L
|
||||||
|
|
||||||
|
- giorno: **$-18.75** di equity (24 letture)
|
||||||
|
- realizzato: +0.10 netto su 1 round-trip chiusi · 3 fill · fee 0.0066
|
||||||
|
- cumulato dall'arming: **$+3,886.12** di equity — di cui $+3,834.43 versati/prelevati -> trading **$+51.69** · TWR **+7.57%**
|
||||||
|
|
||||||
|
## Salute
|
||||||
|
|
||||||
|
- giri di `book_execute`: 24/24
|
||||||
|
- eta' feed SKH all'ultimo giro: 0 min
|
||||||
|
|
||||||
|
## Lettura
|
||||||
|
|
||||||
|
*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.*
|
||||||
|
|
||||||
|
- `[stato]` A mercato su BTC $+704 (TP01 +0.420, SKH01 flat) e ETH $+452 (TP01 +0.268, SKH01 flat).
|
||||||
|
- `[leva]` Leva lorda **0.26x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.74x.
|
||||||
|
- `[pnl]` Equity $-18.75 con 3 fill e 1 round-trip chiusi (realizzato $+0.10 netto).
|
||||||
|
- `[vol]` BTC: implicita 38.8 contro realizzata 30g 47.1 (sotto); DVOL al 31° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
|
||||||
|
- `[vol]` ETH: implicita 52.6 contro realizzata 30g 70.6 (sotto); DVOL al 25° pctl di un anno; IV-rank espandente 0.13 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
|
||||||
|
- `[evidenza]` 📌 Campione a oggi: **76 giorni, 41 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi.
|
||||||
|
|
||||||
|
## Analisi (agente)
|
||||||
|
|
||||||
|
*Scritta da `claude-opus-5` il 2026-09-08T00:37:50+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
|
||||||
|
|
||||||
|
La giornata rompe tre chiusure positive di fila (+28.60, +6.93, +12.27) con $-18.75, la perdita giornaliera più grande della serie qui riportata. È quasi tutta valutazione, non attività: 3 fill, 1 round-trip, realizzato $+0.10 netto, con BTC -1.59% e ETH -0.98% e il libro long su entrambi. Il segno torna; la taglia non è verificabile qui, perché la pagina non scompone il P&L per asset.
|
||||||
|
|
||||||
|
Ciò che è cambiato davvero risale al salto del 04/09: le posizioni sono passate dai $+357 / $+214 di fine agosto ai $+704 / $+452 di oggi e ci restano, mentre la leva lorda resta 0.26x. A parità di rischio relativo la stessa percentuale di mercato si scrive con un numero di dollari più grande: le prossime giornate rosse sembreranno peggiori senza esserlo.
|
||||||
|
|
||||||
|
Insolito il quadro della volatilità: implicita sotto la realizzata su entrambi (38.8 contro 47.1; 52.6 contro 70.6) con DVOL al 31° e al 25° percentile di un anno. Il gate di VRP01 legge l'IV-rank (0.11 e 0.13) e resta chiuso, ma una lettura altrettanto compatibile è che il mercato tratti il movimento a 30 giorni (+21.85%, +29.98%) come già avvenuto.
|
||||||
|
|
||||||
|
TP01 porta segnali parziali (0.420, 0.268) e SKH01 è flat malgrado TSMOM 3/3 su: c'è tendenza, non breakout. Il cumulato $+3,886.12 è quasi tutto versato ($+3,834.43); resta $+51.69 di trading su 76 giorni e 41 round-trip, TWR +7.57% — campione che non separa edge e fortuna. Salute piena, 24/24 giri e feed a 0 min: niente da controllare a mano oggi.
|
||||||
|
|
||||||
|
## Nota
|
||||||
|
|
||||||
|
*(vuota — campo dell'operatore)*
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Giornale di bordo — 2026-09-08
|
||||||
|
|
||||||
|
*Scritto 2026-09-09T00:36:48+00:00. Numeri misurati; nessuna interpretazione automatica.*
|
||||||
|
|
||||||
|
## Mercato
|
||||||
|
|
||||||
|
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **BTC** | $78,470.50 | -0.80% | +1.34% | +20.97% | 47.3% | ↑ ↑ ↑ (3/3 su) | 39.9 (40° pctl 1a) |
|
||||||
|
| **ETH** | $2,485.45 | -0.20% | +2.77% | +30.16% | 70.6% | ↑ ↑ ↑ (3/3 su) | 53.9 (33° pctl 1a) |
|
||||||
|
|
||||||
|
## Libro
|
||||||
|
|
||||||
|
Equity **$4,477.46** · nozionale lordo $1,149 · leva lorda 0.26x · barra dati 2026-09-08 · 24 giri
|
||||||
|
|
||||||
|
| | TP01 | SKH01 | target | posizione | azione |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **BTC** | +0.415 | flat | $+697 | $+698 | HOLD (a target) |
|
||||||
|
| **ETH** | +0.268 | flat | $+449 | $+451 | HOLD (a target) |
|
||||||
|
|
||||||
|
## P&L
|
||||||
|
|
||||||
|
- giorno: **$-6.72** di equity (24 letture)
|
||||||
|
- realizzato: +0.00 netto su 1 round-trip chiusi · 2 fill · fee 0.0055
|
||||||
|
- cumulato dall'arming: **$+3,879.40** di equity — di cui $+3,834.43 versati/prelevati -> trading **$+44.97** · TWR **+7.41%**
|
||||||
|
|
||||||
|
## Salute
|
||||||
|
|
||||||
|
- giri di `book_execute`: 24/24
|
||||||
|
- eta' feed SKH all'ultimo giro: 0 min
|
||||||
|
|
||||||
|
## Lettura
|
||||||
|
|
||||||
|
*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.*
|
||||||
|
|
||||||
|
- `[stato]` A mercato su BTC $+698 (TP01 +0.415, SKH01 flat) e ETH $+451 (TP01 +0.268, SKH01 flat).
|
||||||
|
- `[leva]` Leva lorda **0.26x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.74x.
|
||||||
|
- `[pnl]` Equity $-6.72 con 2 fill e 1 round-trip chiusi (realizzato $+0.00 netto).
|
||||||
|
- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($4,502.93). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
|
||||||
|
- `[vol]` BTC: implicita 39.9 contro realizzata 30g 47.3 (sotto); DVOL al 40° pctl di un anno; IV-rank espandente 0.14 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
|
||||||
|
- `[vol]` ETH: implicita 53.9 contro realizzata 30g 70.6 (sotto); DVOL al 33° pctl di un anno; IV-rank espandente 0.15 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
|
||||||
|
- `[evidenza]` 📌 Campione a oggi: **77 giorni, 42 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi.
|
||||||
|
|
||||||
|
## Analisi (agente)
|
||||||
|
|
||||||
|
*Scritta da `claude-opus-5` il 2026-09-09T00:37:27+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
|
||||||
|
|
||||||
|
Terzo giorno sotto il picco del 06/09 ($4,502.93), e secondo consecutivo in rosso dopo i $-18.75 del 07/09: oggi $-6.72. La differenza rispetto alla settimana precedente al versamento non e' il segno ma la scala — le posizioni sono passate da BTC $+347/ETH $+213 a BTC $+698/ETH $+451, quindi oscillazioni di equity dello stesso ordine in dollari valgono ora meno in percentuale. Con realizzato $+0.00 su 1 round-trip e HOLD a target su entrambe le gambe, la perdita del giorno e' rivalutazione delle posizioni aperte contro BTC -0.80% ed ETH -0.20%, non attivita' di trading.
|
||||||
|
|
||||||
|
L'elemento insolito e' altrove: TSMOM 3/3 su su entrambi gli asset, BTC +20.97% e ETH +30.16% a 30 giorni, e nonostante questo TP01 sta a +0.415 e +0.268 con SKH01 flat — leva lorda 0.26x su un tetto di 1.00x. Il libro e' poco investito dentro il trend che dovrebbe essere il suo caso favorevole; se questo sia disciplina di sizing o segnale debole non e' misurabile qui.
|
||||||
|
|
||||||
|
Lettura alternativa altrettanto compatibile: a 0.26x di esposizione il P&L giornaliero e' quasi interamente beta di mercato, e i 77 giorni / 42 round-trip non permettono di distinguerlo dall'edge.
|
||||||
|
|
||||||
|
Merita un'occhiata umana il round-trip chiuso con realizzato esattamente $+0.00 a fronte di fee 0.0055.
|
||||||
|
|
||||||
|
## Nota
|
||||||
|
|
||||||
|
*(vuota — campo dell'operatore)*
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Giornale di bordo — 2026-09-09
|
||||||
|
|
||||||
|
*Scritto 2026-09-10T00:38:03+00:00. Numeri misurati; nessuna interpretazione automatica.*
|
||||||
|
|
||||||
|
## Mercato
|
||||||
|
|
||||||
|
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **BTC** | $78,283.50 | -0.24% | +1.26% | +22.45% | 46.8% | ↑ ↑ ↑ (3/3 su) | 40.2 (42° pctl 1a) |
|
||||||
|
| **ETH** | $2,467.95 | -0.70% | +3.19% | +31.89% | 70.1% | ↑ ↑ ↑ (3/3 su) | 53.7 (32° pctl 1a) |
|
||||||
|
|
||||||
|
## Libro
|
||||||
|
|
||||||
|
Equity **$4,472.10** · nozionale lordo $1,157 · leva lorda 0.26x · barra dati 2026-09-09 · 24 giri
|
||||||
|
|
||||||
|
| | TP01 | SKH01 | target | posizione | azione |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **BTC** | +0.418 | flat | $+701 | $+704 | HOLD (a target) |
|
||||||
|
| **ETH** | +0.270 | flat | $+453 | $+453 | HOLD (a target) |
|
||||||
|
|
||||||
|
## P&L
|
||||||
|
|
||||||
|
- giorno: **$-5.36** di equity (24 letture)
|
||||||
|
- realizzato: +0.00 netto su 0 round-trip chiusi · 2 fill · fee 0.0047
|
||||||
|
- cumulato dall'arming: **$+3,874.04** di equity — di cui $+3,834.43 versati/prelevati -> trading **$+39.61** · TWR **+7.28%**
|
||||||
|
|
||||||
|
## Salute
|
||||||
|
|
||||||
|
- giri di `book_execute`: 24/24
|
||||||
|
- eta' feed SKH all'ultimo giro: 0 min
|
||||||
|
|
||||||
|
## Lettura
|
||||||
|
|
||||||
|
*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.*
|
||||||
|
|
||||||
|
- `[stato]` A mercato su BTC $+704 (TP01 +0.418, SKH01 flat) e ETH $+453 (TP01 +0.270, SKH01 flat).
|
||||||
|
- `[leva]` Leva lorda **0.26x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.74x.
|
||||||
|
- `[pnl]` Equity $-5.36 con 2 fill e 0 round-trip chiusi (realizzato $+0.00 netto).
|
||||||
|
- `[drawdown]` 📌 Equity **-0.68%** sotto il picco ($4,502.93). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
|
||||||
|
- `[vol]` BTC: implicita 40.2 contro realizzata 30g 46.8 (sotto); DVOL al 42° pctl di un anno; IV-rank espandente 0.14 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
|
||||||
|
- `[vol]` ETH: implicita 53.7 contro realizzata 30g 70.1 (sotto); DVOL al 32° pctl di un anno; IV-rank espandente 0.15 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
|
||||||
|
- `[evidenza]` 📌 Campione a oggi: **78 giorni, 42 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi.
|
||||||
|
|
||||||
|
## Analisi (agente)
|
||||||
|
|
||||||
|
*Scritta da `claude-opus-5` il 2026-09-10T00:39:20+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
|
||||||
|
|
||||||
|
Il fatto nuovo non e' il segno del giorno ma la sua composizione: $-5.36 di equity con 2 fill, zero round-trip chiusi e realizzato $+0.00. La perdita e' tutta marcatura su posizioni aperte, e le posizioni sono a target su entrambe le gambe ($+704 contro $+701, $+453 contro $+453): il libro non ha deciso niente, ha ri-tagliato. Nella serie qui sopra l'unico altro giorno senza round-trip chiusi e' il 01/09; e' il terzo giorno di equity in discesa dopo il picco del 06/09 a $4,502.93, con BTC -0.24% e ETH -0.70%.
|
||||||
|
|
||||||
|
L'esposizione la porta interamente TP01 (+0.418 su BTC, +0.270 su ETH) mentre SKH01 e' flat su entrambi, e questo con TSMOM 3/3 su e 30 giorni a +22.45% e +31.89%: il trend c'e', il breakout no. Esecuzione pulita, 24/24 giri e feed SKH a 0 min.
|
||||||
|
|
||||||
|
L'implicita sotto la realizzata (40.2 contro 46.8; 53.7 contro 70.1) si legge in due modi ugualmente compatibili — vol a sconto, oppure realizzata gonfiata dalla corsa dei 30 giorni. Quale dei due, non e' misurabile qui.
|
||||||
|
|
||||||
|
Da guardare senza urgenza: il cumulato $+3,874.04 e' quasi tutto versamento ($+3,834.43), il trading vale $+39.61 su 42 round-trip in 78 giorni. A quella taglia il TWR +7.28% non e' ancora una misura di edge.
|
||||||
|
|
||||||
|
## Nota
|
||||||
|
|
||||||
|
*(vuota — campo dell'operatore)*
|
||||||
@@ -69,7 +69,11 @@ chi lo riapre deve battere il motivo, non ripetere l'esperimento.
|
|||||||
solo LOWVOL 19-major regge standalone (FULL/HOLD 1.07) ma deflated-Sharpe 0.13 + storia 2.5a → **DEBOLE/
|
solo LOWVOL 19-major regge standalone (FULL/HOLD 1.07) ma deflated-Sharpe 0.13 + storia 2.5a → **DEBOLE/
|
||||||
forward STAT-MODE** (`2026-06-29-xsec-v2-nonmom.md`). (D) **MACRO regime-gate** (equity/credito/oro/tassi →
|
forward STAT-MODE** (`2026-06-29-xsec-v2-nonmom.md`). (D) **MACRO regime-gate** (equity/credito/oro/tassi →
|
||||||
de-risk crypto): **RIDONDANTE col trend** (corr→TP01 0.989; il gate lavora solo nel 2-3% dei giorni, TP01
|
de-risk crypto): **RIDONDANTE col trend** (corr→TP01 0.989; il gate lavora solo nel 2-3% dei giorni, TP01
|
||||||
già flat nei crash) → SCARTATO (`2026-06-29-macro-regime-gate.md`).
|
già flat nei crash) → SCARTATO (`2026-06-29-macro-regime-gate.md`). (D-bis, 10/09) **MACRO RISK-OFF ±2h intorno a
|
||||||
|
FOMC/CPI su TP01** (`r0910_macro_riskoff.py`, §78): **REFUTED 0/3** — ΔSh −0,097, drift −1,24%/anno, Δ vero al
|
||||||
|
4,2° pctl del null location-matched: nell'1% delle ore in finestra TP01 fa il 7% del guadagno (+1,05 bp/h
|
||||||
|
contro +0,17). Come il weekend: la coda del trend vive nelle ore «pericolose». Un gate «fondamentale»
|
||||||
|
che rifiuta i segnali intorno ai dati costerebbe quanto il funding, con segno scelto da chi lo propone.
|
||||||
|
|
||||||
- **Sweep strategie a 5 thread (2026-06-29) — 0 nuovi sleeve, 1 LEAD che rompe 2 muri su 3.** Ricerca
|
- **Sweep strategie a 5 thread (2026-06-29) — 0 nuovi sleeve, 1 LEAD che rompe 2 muri su 3.** Ricerca
|
||||||
parallela onesta su aree inesplorate (harness `altlib`+`xsec_v2_nonmom`, tutti i gate incl. il nuovo
|
parallela onesta su aree inesplorate (harness `altlib`+`xsec_v2_nonmom`, tutti i gate incl. il nuovo
|
||||||
@@ -701,7 +705,7 @@ chi lo riapre deve battere il motivo, non ripetere l'esperimento.
|
|||||||
**0,93** (alto) e **supera 1 in backwardation** mentre `f_skew` resta piatto -> **la parte maggiore
|
**0,93** (alto) e **supera 1 in backwardation** mentre `f_skew` resta piatto -> **la parte maggiore
|
||||||
del difetto si annulla proprio dove il gate fa tradare il sleeve**, quindi f=0,73 e' plausibilmente
|
del difetto si annulla proprio dove il gate fa tradare il sleeve**, quindi f=0,73 e' plausibilmente
|
||||||
**conservativo li'**. Non misurato (0/22 ingressi passano il gate) e in un crash i due pezzi vanno
|
**conservativo li'**. Non misurato (0/22 ingressi passano il gate) e in un crash i due pezzi vanno
|
||||||
in **direzioni contrarie**. **`VRP_CFG["f"]` NON cambiato.** Replica indipendente del f: **0,714**.
|
in **direzioni contrarie**. **`VRP_CFG["f"]` NON cambiato.** Replica indipendente del f: **0,714** (0,712 dal 09/09 sera, `cblib` causale; il 0,73 del 30/07 stampa 0,71).
|
||||||
📌 **(7) CHIUSURE DI FAMIGLIA.** **Il filone funding si chiude sul QUARTO lato** (affollamento):
|
📌 **(7) CHIUSURE DI FAMIGLIA.** **Il filone funding si chiude sul QUARTO lato** (affollamento):
|
||||||
su **53.430 ore / 3 anni** non predice ne' direzione ne' volatilita'. E i futures datati **non sono
|
su **53.430 ore / 3 anni** non predice ne' direzione ne' volatilita'. E i futures datati **non sono
|
||||||
un quinto lato**: il loro basis **E'** il funding del perp (+7,33% implicito contro **+6,48%**
|
un quinto lato**: il loro basis **E'** il funding del perp (+7,33% implicito contro **+6,48%**
|
||||||
@@ -1521,3 +1525,31 @@ una maschera a caso della stessa frequenza**.
|
|||||||
**Gated sull'IV sottile** (la copertura dinamica che §46 non aveva provato): migliora, perde.
|
**Gated sull'IV sottile** (la copertura dinamica che §46 non aveva provato): migliora, perde.
|
||||||
**Cosa lo riapre:** scadenza lunga che copra il grind (fuori dal vincolo ≤15g dell'operatore),
|
**Cosa lo riapre:** scadenza lunga che copra il grind (fuori dal vincolo ≤15g dell'operatore),
|
||||||
o un premio di varianza negativo su BTC. Non delta, tenor ≤15, gate IV: misurati.
|
o un premio di varianza negativo su BTC. Non delta, tenor ≤15, gate IV: misurati.
|
||||||
|
|
||||||
|
- **CROLLI / CROLLO CATTURATO / XRP — 0 candidati, 1 debito, 2 conclusioni smontate in revisione (2026-09-09)** —
|
||||||
|
`r0909_{libro_nei_crolli,crash_catturato,xrp_terza_gamba,deribit_universo}.py`, diario
|
||||||
|
`2026-09-09-crolli-opzioni-monete.md`. Domanda dell'operatore: *"guadagnare anche nei crolli (opzioni se
|
||||||
|
serve), piu' monete ma sempre in Deribit"*. **(1) Il libro nei crolli**: perde nel GIORNO (−0,48%/g su 160
|
||||||
|
giorni ≤ −5%, positivo nel 18%), immune-o-positivo per finestra (4 POSITIVO / 6 immune / 2 PERDE su 12), **> 0
|
||||||
|
in 8/8 peggiori finestre dal 2022** per la gamba short di SKH01 (108% dei guadagni, scomposizione esatta,
|
||||||
|
marcata all'uscita). Beta 0,0769 sulla finestra di §46 (riprodotto). Peggior giorno canonico 2025-10-10
|
||||||
|
−3,38%: crollo con TP01 a 0,505 (lo squeeze di §33 e' sulla lente hourly). 37,5/62,5 batte 12/12 episodi
|
||||||
|
ma maxDD 12,1% vs 9,4%; gate fallisce in-sample (cache `skh_sigtab` ferma al 25/07). **(2) Crollo
|
||||||
|
catturato**: l'archivio bite copre con bid/ask il 1-5/06/2026 (BTC −25%, ETH −30%), scadenza 19/06, inverse.
|
||||||
|
ETH pre-crollo (4 strutture): f_net **0,76 [0,71-0,86] = 0,712 del rally** (rimisurato la sera con `cblib` causale; era 0,74 = 0,714); f a scadenza 1,04 ma **tautologico** (ambo le
|
||||||
|
gambe ITM ⇒ solo f_net); peggior MTM **1,08** dopo il taglio alla larghezza — la prima stesura diceva 2,58 con
|
||||||
|
"IV corta esplosa a 108 per lo skew": **artefatto** (quote 158% di spread a T→0; la IV 108 e' del 18/06, a T→0);
|
||||||
|
put δ−0,10 costa **1,92×** il modello (punto nuovo, sopra l'1,27 di §46) e paga come il modello (0,94-1,04);
|
||||||
|
ingressi durante il crollo bimodali (perde il 46%). **Crollo a vol BASSA** (picco DVOL 29°/46° pctl), gate
|
||||||
|
IV-rank di VRP01 chiuso: **la condizione di §3 e' soddisfatta nella forma, non nella sostanza → non si
|
||||||
|
riapre**. BTC pre-crollo non misurabile (griglia strike rada). **(3) XRP terza gamba** (harness di
|
||||||
|
`r0822_sol_leg`, congelato): **diluisce come SOL** — hold-out −0,169 in 0/24, FULL −0,206 (L-FULL) /
|
||||||
|
−0,066 (2024+), un anno buono (2024), de-levering; L-PULITA dal 2024 (2023 0,57% >1% dal riferimento;
|
||||||
|
Coinbase delisto' XRP 2021-01→2023-07, riferimento Bitstamp prima); 5m flat 27-61%. Dato in
|
||||||
|
`data/raw/alt_xrp_*`. **(4) Venue**: 32 perp USDC, liquidi ≥$10M/g solo XRP/ETH/BTC/SOL; XS01 13/19 (APT
|
||||||
|
inactive); `BTCDVOL_USDC` future esiste ma vol 0 / spread 12%; PAXG perp non misurato (9 mesi, $0,18M/g).
|
||||||
|
**Debito §5.18** (`cblib.spot_series` + `asof` un'ora avanti) **riparato la sera stessa**, insieme al DVOL
|
||||||
|
(fino a 24h avanti): §11 0,714 → 0,712, verdetti invariati; `vrp_f_watch` f canonico 0,732 → 0,706 e la
|
||||||
|
differenza col candidato +0,097 [+0,042, +0,140] ora esclude lo zero (criterio (a) 28/40 ancora aperto). **Errori dell'autore smontati dal revisore `fable`**: il 2,6× (artefatto) e "8/12 GUADAGNA"
|
||||||
|
(l'ordine delle classi era il verdetto: immune → POSITIVO da' 4+6). Nessun cambio a libro/pesi/config.
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
movimento certo e moltiplica i segmenti (gli ambigui non spezzano: restano nel rendimento,
|
||||||
dichiarati accanto — P12). Il report stampa TWR, segmenti con le date, movimenti elencati,
|
dichiarati accanto — P12). Il report stampa TWR, segmenti con le date, movimenti elencati,
|
||||||
trading al netto, e il delta $ grezzo solo etichettato «movimenti INCLUSI». Verifica M23: la
|
trading al netto, e il delta $ grezzo solo etichettato «movimenti INCLUSI». Verifica M23: la
|
||||||
macchina riproduce il +10,80% del diario 01/09 sui suoi quattro punti. **Limite ereditato:** un
|
macchina riproduce il +10,80% del diario 01/09 sui suoi quattro punti. **Due limiti trovati in revisione (02/09):** l'intervallo che contiene un movimento certo esce INTERO dal rendimento (il suo P&L di mercato va in `certi`; errore massimo META' del movimento, per costruzione; il 25/08 ~$0,5) e a base di equity zero `twr` e `trading` sono entrambi None, perche' il salto 0→X del primo versamento e' invisibile al classificatore. I segmenti a lunghezza zero (movimento nel primo intervallo, o due consecutivi) non si stampano. **Seconda tornata di revisione, stesso giorno:** il classificatore era **cieco ~23 ore al giorno** — il
|
||||||
+10% di trading fra due letture orarie consecutive, a mercato fermo, sarebbe classificato
|
feed 1h si ferma alle 00:00, `asof` dava la stessa barra alle due letture, mercato «fermo» = 0, e
|
||||||
movimento — e' il limite dichiarato del rilevatore live (D5), non uno nuovo.
|
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
|
**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
|
(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'
|
**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
|
**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.
|
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
|
### Cosa resta scoperto
|
||||||
|
|
||||||
1. **La sonda dice di chi è il guasto, non lo aggira.** Col gateway giù il libro continua ad
|
1. **La sonda dice di chi è il guasto, non lo aggira.** Col gateway giù il libro continua ad
|
||||||
@@ -954,3 +1011,39 @@ impedire.** Danno reale quel giorno: nessuno — alle 09:47 l'equity era leggibi
|
|||||||
che si accende se la fixture viene rimossa.
|
che si accende se la fixture viene rimossa.
|
||||||
- ⚠️ **Resta il principio più largo:** oggi è deviato **solo** il watermark. `data/live/trades.db` e
|
- ⚠️ **Resta il principio più largo:** oggi è deviato **solo** il watermark. `data/live/trades.db` e
|
||||||
`data/live/book_executions.jsonl` sono esposti allo stesso errore (§5.12).
|
`data/live/book_executions.jsonl` sono esposti allo stesso errore (§5.12).
|
||||||
|
|
||||||
|
### Revisione settimanale in sola lettura (2026-09-09)
|
||||||
|
|
||||||
|
L'operatore ha chiesto «un processo di auto-revisione dei dati e della configurazione, che si autoregola,
|
||||||
|
si autoaggiorna, crea sistemi nuovi valutando mercato e dati macro, e mi informa su Telegram». La risposta,
|
||||||
|
misurata contro la memoria, e' stata in quattro pezzi: **la rilettura periodica manca e vale** (i nove
|
||||||
|
sorveglianti guardano ognuno la sua grandezza; nessuno rilegge il sistema intero, e la revisione con un
|
||||||
|
secondo modello ha trovato in una sessione due conclusioni false e un look-ahead); **l'autoregolazione dei
|
||||||
|
parametri no** (e' cio' che `scale_watch`/`venue_watch` trattano come anomalia; A8 della chiave di scala:
|
||||||
|
una riga di config, una di giornale, decisione dell'operatore); **la generazione automatica di strategie
|
||||||
|
solo con un freno** (ogni candidato e' un trial, M3, e la ricerca non e' il vincolo binding dal 26/07: il
|
||||||
|
miglior lead vale +0,046 €/giorno contro +4,07%/anno di drift per €100/mese in piu'); **il macro da
|
||||||
|
internet no** (gate macro, DVOL direzionale e skew de-risk sono gia' HEDGE/ridondanti; un dato dal web
|
||||||
|
non e' certificato — la APR USDC «pubblicata» e mai incassata). L'operatore ha scelto la prima («fai»).
|
||||||
|
|
||||||
|
Cosa gira: `scripts/cron_review.sh` (lunedi' 06:15 UTC, fuori dal :00 e dal martedi' 09:00, dopo il
|
||||||
|
`cron_daily` delle 00:30) → `scripts/live/revisione.py --quiet` → `src/live/revisione.scrivi`. Materiale
|
||||||
|
(~195k caratteri, sola lettura, ordine: regole e stato dichiarato, cio' che gira, cio' che e' successo):
|
||||||
|
CLAUDE.md intero, `config/live.json`, `crontab -l`, `git log` 14 g + `git status`, `monitor_health`,
|
||||||
|
stato dei sorveglianti (scale, vrp_f, usde, balance, venue_news, movimenti dichiarati, watermark — messi
|
||||||
|
PRIMA delle voci grandi, cosi' il tetto totale non li taglia mai: alla prima stesura `usde_convert` era
|
||||||
|
finito a 2 caratteri), 7 pagine di giornale, i diari della settimana. Prompt con dieci regole: sola
|
||||||
|
lettura, ogni segnalazione cita la fonte, solo numeri del materiale (guardia `numeri_non_supportati` di
|
||||||
|
`analista`, soglia 12), le decisioni vincolanti di §3 non si ripropongono, un'idea nuova si propone solo
|
||||||
|
citando la memoria che ha ucciso la simile, i gate si elencano con data e criterio senza anticipare il
|
||||||
|
verdetto, misurato ≠ dedotto, niente previsioni, URGENTE per cio' che serve all'operatore subito, cinque
|
||||||
|
titoli fissi. Rapporto in `docs/revisioni/<data>.md` firmato (modello, data, «lettore fallibile P13»,
|
||||||
|
tabella del materiale letto con caratteri e troncature); Telegram = la sola Sintesi, con taglio dichiarato.
|
||||||
|
Modello `claude-fable-5-1` via `analista.interroga` (CLI `claude -p`, `--allowed-tools ""`, timeout 900 s):
|
||||||
|
**non il modello che scrive il codice**, ed e' un test (`test_il_revisore_non_e_il_modello_che_scrive_il_codice`).
|
||||||
|
Un modello muto o una risposta senza i titoli produce un rapporto «NON eseguita» col motivo e un 🚨: il
|
||||||
|
silenzio non e' una revisione (P5). Guardie: `valida` di `cli` come prima istruzione (`test_cli_flag` lo
|
||||||
|
deriva dalla cartella), `--secco` non chiama e non scrive (test sull'albero dei file), un giro scrive **UN
|
||||||
|
solo file** (test), la cadenza e' dichiarata nel `.sh` e letta dalla crontab installata (`test_revisione`,
|
||||||
|
stesso schema di `test_book_cadenza`). Costo: una chiamata a settimana. Cio' che il sorvegliante non puo'
|
||||||
|
fare: eseguire. E' voluto.
|
||||||
|
|||||||
@@ -157,3 +157,25 @@
|
|||||||
`fresh_5m`). **REGOLA: un guasto IN CORSO si misura al GIORNO PEGGIORE, non in media** — la prima
|
`fresh_5m`). **REGOLA: un guasto IN CORSO si misura al GIORNO PEGGIORE, non in media** — la prima
|
||||||
stesura confrontava "tutto" con "ultimi 7g" e diluiva un guasto di 2 giorni al 13.5%, commettendo
|
stesura confrontava "tutto" con "ultimi 7g" e diluiva un guasto di 2 giorni al 13.5%, commettendo
|
||||||
a 7 giorni lo stesso errore che dichiarava di evitare a 3 mesi. Diario `2026-07-30-vrp-quote-reali.md`.
|
a 7 giorni lo stesso errore che dichiarava di evitare a 3 mesi. Diario `2026-07-30-vrp-quote-reali.md`.
|
||||||
|
|
||||||
|
- 🚨 **IL FEED 1h E' ETICHETTATO ALL'APERTURA, e `asof(ts)` guarda un'ora avanti (trovato in revisione
|
||||||
|
il 2026-09-09).** La barra 1h etichettata 20:00 chiude col 5m delle 20:55 (verificato: close 1h 20:00 =
|
||||||
|
close 5m 20:55 = 1603,5, mentre il 5m 19:55 fa 1572,95). Quindi `S.asof(ts)` su una serie indicizzata
|
||||||
|
con quell'etichetta restituisce la chiusura di **ts+1h**: un look-ahead di un'ora senza eccezioni e
|
||||||
|
senza NaN (D6, quarta occorrenza). Colpisce `cblib.spot_series` — cioe' il f 0,714 di §11 e ogni numero
|
||||||
|
di `cblib` con lo spot, compreso `ST = S.asof(exp)` che e' il prezzo di un'ora DOPO la scadenza.
|
||||||
|
✅ **RIPARATO la sera stessa in `cblib`** (`causale(s, cadenza)`: spot +1h, e **anche il DVOL
|
||||||
|
giornaliero, +1 giorno** — la riga del giorno D e' la chiusura delle 23:00 di D, verificato contro la
|
||||||
|
risoluzione 1h dell'API pubblica: seconda serie con lo stesso difetto, quinta occorrenza di D6). §11
|
||||||
|
riprodotto al millesimo su worktree HEAD e rimisurato: 0,714 → **0,712** (le correzioni hanno segno
|
||||||
|
opposto; DVOL orario di controllo 0,717). Il DVOL causale da feed giornaliero e' **stantio fino a 23 ore**
|
||||||
|
(0,2 pt mediana, 3 pt max; nel crollo ±0,05 di f): il feed DVOL orario e' il passo aperto. Diario
|
||||||
|
`2026-09-09b-debito-18-cblib-causale.md`.
|
||||||
|
- **XRP** (`XRP/USDC:USDC`, dal 2022-03-16) ricostruito il 2026-09-09 con `rebuild_history --asset XRP` e
|
||||||
|
spostato in **`data/raw/alt_xrp_*.parquet`** (namespace di ricerca come SOL: fuori dal feed attivo, non
|
||||||
|
rinfrescato dal cron, `load_data("XRP")` fallisce, presidiato da `test_sol_leg` via `DERIBIT_INSTR`).
|
||||||
|
Certificazione per anno vs riferimento USD: **Coinbase non ha XRP fra 2021-01 e 2023-07** (delisting per
|
||||||
|
la causa SEC) → riferimento **Bitstamp** prima, Coinbase dopo; >1% 0,06% (2022) · 0,57% (2023) · ≤0,02%
|
||||||
|
(2024+); mediana 5-6 bps; **5m flat 27-61%** (BTC/ETH 0,1-2%, SOL 21%): il 5m e' illiquido. Cache del
|
||||||
|
riferimento in `data/_cache/` (gitignored, fuori backup: si riproduce con rete).
|
||||||
|
|
||||||
|
|||||||
@@ -197,6 +197,18 @@ lati contro il **93%** di BTC_USDC, e il suo miglior bid sta a **un tick**.
|
|||||||
DVOL alla mediana del 7°/11° percentile storico, sottostante +25%/+44%.
|
DVOL alla mediana del 7°/11° percentile storico, sottostante +25%/+44%.
|
||||||
- ✅ **Replica indipendente del f del 30/07 su campione piu' lungo: f = 0,714 pooled** (IC95
|
- ✅ **Replica indipendente del f del 30/07 su campione piu' lungo: f = 0,714 pooled** (IC95
|
||||||
[0,690, 0,779], **0/19 osservazioni >= 1,0**). Attraversare lo spread costa **~10% del credito**.
|
[0,690, 0,779], **0/19 osservazioni >= 1,0**). Attraversare lo spread costa **~10% del credito**.
|
||||||
|
✅ **RIMISURATO il 2026-09-09 sera** (debito §5.18: `cblib` dava lo spot di un'ora DOPO e il DVOL di
|
||||||
|
fino a 24 ore DOPO l'ingresso): allo stesso taglio (22/08, n=19) **f = 0,712 [0,664, 0,732]** — le due
|
||||||
|
correzioni hanno segno opposto (solo spot 0,721, solo DVOL 0,693) e un DVOL orario di controllo da'
|
||||||
|
0,717; a oggi (n=23) 0,712 [0,677, 0,741]. **Il verdetto non cambia.** Cambiano i numeri di
|
||||||
|
contorno: Sharpe canonico BTC 32,58 → 29,61 con mediana onesta **2,12** (era 1,45) e banda
|
||||||
|
[−0,19, 29,61]; ETH 3,90 → 7,64 (mediana onesta 3,35); media BTC fra le ancore −0,66% → +11,44%
|
||||||
|
(cambia ancora segno); forfait fee 1,51× (BTC) / 1,87× (ETH); il regolamento e' il prezzo delle
|
||||||
|
08:00 e non delle 09:00 (P&L +3,3%); sottostante nel campione +21% / +40% (era +25% / +44%: il vecchio
|
||||||
|
regolamento del 21/08 era il prezzo delle 09:00, **+3,9%** sopra quello delle 08:00 — quanto pesa un'ora su
|
||||||
|
un numero a due estremi). Lo stesso `cblib` rigira anche `r0730`: il f 0,73 [0,706-0,805] del 30/07 stampa
|
||||||
|
ora **0,71 [0,680-0,756]** (n=27, 0/27 ≥ 1).
|
||||||
|
Diario `2026-09-09b-debito-18-cblib-causale.md`.
|
||||||
- ⚠️ **Correzione a un numero pubblicato:** il forfait fee del sleeve sovrastima il listino vero di
|
- ⚠️ **Correzione a un numero pubblicato:** il forfait fee del sleeve sovrastima il listino vero di
|
||||||
**1,44x (BTC) / 1,80x (ETH)**, non di "~2x". Il segno (conservativo) regge, la taglia no.
|
**1,44x (BTC) / 1,80x (ETH)**, non di "~2x". Il segno (conservativo) regge, la taglia no.
|
||||||
- ⚠️ **Rischio sul MINIMO, non sulle chiusure:** peggior mark infra-settimana mediana −6,4% del
|
- ⚠️ **Rischio sul MINIMO, non sulle chiusure:** peggior mark infra-settimana mediana −6,4% del
|
||||||
@@ -374,6 +386,13 @@ nuovo: **va decisa PRIMA del 23/10, non quel giorno.**
|
|||||||
(il collettore li accumula da solo), revisione comunque **2027-02-22**.
|
(il collettore li accumula da solo), revisione comunque **2027-02-22**.
|
||||||
- ✅ Confound di modello **ESCLUSO e misurato**: `f_markfit` = **0,992** (la mark IV di Deribit
|
- ✅ Confound di modello **ESCLUSO e misurato**: `f_markfit` = **0,992** (la mark IV di Deribit
|
||||||
riprezza i propri mid entro l'1%) -> RR/BF sono un fatto di **prezzo**, non l'output del fit.
|
riprezza i propri mid entro l'1%) -> RR/BF sono un fatto di **prezzo**, non l'output del fit.
|
||||||
|
📌 **Rigirato il 2026-09-09 sera con spot e DVOL causali** (debito §5.18): la scomposizione sul panel si
|
||||||
|
muove di **≤0,005** per fattore (term 0,880 · skew 0,917 · spread 0,910 · fit 0,995 · f_tot 0,729) e Q1/Q2
|
||||||
|
non si toccano (serie proprie); agli **ingressi** il controllo `f_markfit` passa da 1,045 a **0,991** e
|
||||||
|
le osservazioni che lo superano da 22/28 a **25/28** — l'ora di spot in piu' stava nel controllo, non
|
||||||
|
nell'effetto. Agli ingressi (n=28) si muove di piu': f_term 0,910 → 0,875, f_tot 0,732 → 0,706, e le
|
||||||
|
correlazioni `corr(f_tot_mid, rr7)` +0,21 → −0,06 e `atm7` +0,02 → +0,34 **cambiano segno/ordine** — sono
|
||||||
|
22-25 punti, dichiarate (P12), non usate.
|
||||||
- ✅ **Controllo positivo cablato nello script:** con l'ancora **assunta** a :30 invece del `ts_max`
|
- ✅ **Controllo positivo cablato nello script:** con l'ancora **assunta** a :30 invece del `ts_max`
|
||||||
osservato compare un **falso lead** a +15m (0,102 contro 0,031) — *"e' l'errore che avrei
|
osservato compare un **falso lead** a +15m (0,102 contro 0,031) — *"e' l'errore che avrei
|
||||||
pubblicato senza quel controllo"*.
|
pubblicato senza quel controllo"*.
|
||||||
@@ -3969,3 +3988,84 @@ quota > 0**: l'ultimo migliaio compra ~€100/anno e butta via l'assicurazione);
|
|||||||
a banda larga, non un tasso (libro a mercato il 22% dei giorni, TWR +10,8% in 70g, hold-out TP01
|
a banda larga, non un tasso (libro a mercato il 22% dei giorni, TWR +10,8% in 70g, hold-out TP01
|
||||||
~+0,05, fisco −30% a 10a); fondo d'emergenza da dichiarare. Operativamente nulla: cap dinamico,
|
~+0,05, fisco −30% a 10a); fondo d'emergenza da dichiarare. Operativamente nulla: cap dinamico,
|
||||||
rilevatore di salti collaudato il 25/08. **In attesa di importo e data.**
|
rilevatore di salti collaudato il 25/08. **In attesa di importo e data.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 74 — LIBRO-NEI-CROLLI (cosa fanno TP01 e SKH01 nei crolli, misurato invece che ricordato)
|
||||||
|
|
||||||
|
`scripts/research/r0909_libro_nei_crolli.py`, diario `2026-09-09-crolli-opzioni-monete.md`. **Libro,
|
||||||
|
pesi, config, cron INVARIATI.** Lente: sleeve di ricerca, ancora canonica, pesi importati da
|
||||||
|
`src/live/book` (P1); ⚠️ SKH01 marcato all'USCITA del trade (zero nell'87,7% dei giorni). Indice 50/50;
|
||||||
|
giorno di crollo ≤ −5%; 12 peggiori finestre di 20 g non sovrapposte + 9 episodi nominati prima.
|
||||||
|
|
||||||
|
**Nel GIORNO del crollo il libro PERDE** (−0,48%/g su 160 giorni, positivo nel 18%; ≤ −10%: −0,51%/g con
|
||||||
|
SKH01 +0,42). **Per finestra**: 4 POSITIVO / 6 immune / 2 PERDE; libro > 0 in 8/12 e **in 8/8 dal 2022**
|
||||||
|
(mediana +3,3% contro indice −29%); le due perdite (2019, maggio 2021) sono TP01 long dentro un trend
|
||||||
|
non girato. **La gamba short di SKH01 fa il 108% dei guadagni** (scomposizione ESATTA long+short==intera,
|
||||||
|
assert). Beta **+0,0769** sulla finestra di §46 (riprodotto al millesimo). Peggior giorno canonico
|
||||||
|
2025-10-10 −3,38%: un crollo con TP01 a 0,505 (lo squeeze di §33 sta sulla lente hourly, non si riproduce
|
||||||
|
qui). 37,5/62,5 (§26) batte 12/12 episodi, Sharpe 1,81 vs 1,82, maxDD 12,1% vs 9,4%;
|
||||||
|
`r0726_reeval_live_weight` da' `gate_pass False` su `delta_insample −0,0026` (cache segnali ferma al
|
||||||
|
25/07: stesse cifre del 26/07, e la condizione che fallisce e' in-sample).
|
||||||
|
🚨 **Errore dell'autore smontato in revisione:** la prima stesura provava GUADAGNA prima di immune e
|
||||||
|
stampava **8/12 GUADAGNA**; nell'ordine dichiarato sono 4+6. *L'ordine delle regole era il verdetto.*
|
||||||
|
**VERDETTO: `PERDE NEL GIORNO, POSITIVO PER FINESTRA DAL 2022, TUTTO SKH01 SHORT`** — nessun meccanismo
|
||||||
|
nuovo: quello che guadagna nei crolli e' gia' in produzione, e la leva che lo aumenta (il peso) e' chiusa dal gate.
|
||||||
|
|
||||||
|
## 75 — CROLLO-CATTURATO (il f delle opzioni su un crollo a quote vere)
|
||||||
|
|
||||||
|
`scripts/research/r0909_crash_catturato.py`. **Nessun ordine.** L'archivio bite copre con bid/ask il crollo
|
||||||
|
1-5/06/2026 (BTC −25%, ETH −30%; scadenza 19/06, 14-21 DTE, famiglia INVERSE); il campione di §11 partiva
|
||||||
|
dal fondo. ETH, ingressi pre-crollo (286 snapshot = **4 strutture**): f_net **0,76 [0,71-0,86] = 0,712 del rally** (era 0,74 = 0,714 prima della riparazione di §5.18, la sera stessa; put 1,98× e picco 0,99 idem);
|
||||||
|
f a scadenza 1,04 ma **tautologico** nell'83% (ambo le gambe ITM ⇒ solo f_net); peggior MTM **1,08**
|
||||||
|
dopo il taglio alla larghezza; put δ−0,10 costa **1,92×** il modello (punto nuovo sopra l'1,27 di §46)
|
||||||
|
e paga come il modello (0,94-1,04); ingressi durante il crollo bimodali (perde il 46%). BTC pre-crollo
|
||||||
|
non misurabile (griglia strike rada). **Crollo a vol BASSA**: picco DVOL al 29°/46° percentile, gate
|
||||||
|
IV-rank di VRP01 CHIUSO in tutte le ore. ⇒ la condizione di §3 e' soddisfatta nella forma, **non nella
|
||||||
|
sostanza: non si riapre**. Servono un crollo con IV-rank > 0,30 e la famiglia USDC-lineare.
|
||||||
|
🚨 **Due conclusioni dell'autore smontate in revisione:** «MTM 2,58× per lo skew (IV 108)» era un
|
||||||
|
artefatto (quote con 158% di spread a T→0 oltre la larghezza; la IV 108 e' del 18/06 a T→0); «f a
|
||||||
|
scadenza 1,04 = il modello prezza bene» era una tautologia. **Debito §5.18 RIPARATO la sera stessa** in `cblib`
|
||||||
|
(spot +1h, e anche il DVOL giornaliero, +1 giorno): ETH pre-crollo f_net **0,76 [0,71-0,86]** (era 0,74),
|
||||||
|
BTC durante **0,89 [0,74-1,05]** (era 0,79; con DVOL orario 0,83 — nel crollo il DVOL giornaliero stantio
|
||||||
|
vale −0,04/+0,06 di f), put δ−0,10 1,98× (era 1,92), DVOL pre 36,4 / 49,6 (il 37,3 di BTC era la chiusura del
|
||||||
|
PRIMO giorno di crollo: contaminato). Picco e percentili invariati. Verdetto invariato.
|
||||||
|
**VERDETTO: `CATTURATO, MA A VOL BASSA E FUORI DAL GATE: f_net NON CAMBIA COL REGIME`** — nessuna riapertura.
|
||||||
|
|
||||||
|
## 76 — XRP-TERZA-GAMBA (l'unica altra moneta liquida di Deribit, coi meccanismi congelati)
|
||||||
|
|
||||||
|
`scripts/research/r0909_xrp_terza_gamba.py` (harness IMPORTATO da `r0822_sol_leg`). Dato in
|
||||||
|
`data/raw/alt_xrp_*` (namespace di ricerca, non rinfrescato dal cron). Certificazione: >1% dal
|
||||||
|
riferimento 0,06% (2022) · **0,57% (2023)** · 0,01-0,02% (2024+) — Coinbase dal rilisting 2023-07
|
||||||
|
(delisting SEC), Bitstamp prima; **L-PULITA dal 2024**, come SOL; 5m flat **27-61%**.
|
||||||
|
dSharpe hold-out **−0,169 in 0/24 ancore** (SOL −0,166); FULL −0,206 (L-FULL) / −0,066 (2024+); dCAGR
|
||||||
|
−2,27 / −1,32 pp; de-levering: L-FULL 1,23 vs 2g×0,974 **1,44**, 2024+ 1,54 vs 1,53 pari; per anno 2022
|
||||||
|
−0,55 · 2023 −0,66 · **2024 +0,54** · 2025 −0,44 · 2026 −0,25 (**1/5**). TP01 XRP Sharpe −0,01 /
|
||||||
|
hold-out −0,49; SKH01 XRP maxDD 40,1% (il criterio di selezione di SKH01-V2-DD fallisce, come SOL);
|
||||||
|
corr gamba-book +0,35/+0,37. Eseguibilita' non e' il vincolo (min 1 XRP = $1,42).
|
||||||
|
⚠️ Due difetti del verdetto trovati dai test (Opus) e corretti: campione vuoto stampava «DILUISCE»
|
||||||
|
(ora NON MISURABILE, P5); criterio per anno cablato al 2024 invece che derivato dalla lente (P1).
|
||||||
|
**VERDETTO: `XRP DILUISCE COME SOL — UN ANNO BUONO, HOLD-OUT 0/24`** — nessuna proposta.
|
||||||
|
|
||||||
|
## 77 — UNIVERSO-DERIBIT (lettura del venue, €0)
|
||||||
|
|
||||||
|
`scripts/research/r0909_deribit_universo.py` (API pubblica, JSON con ora in `data/_cache`, `--fresh`).
|
||||||
|
Lettura 2026-09-09 15:05Z: **32 perpetual USDC** aperti (+6 non aperti, fra cui **APT inactive**);
|
||||||
|
liquidi ≥ $10M/g **solo XRP ($38M, spread 4 bps), ETH, BTC, SOL ($13M)**; tutto il resto $0,01-7M
|
||||||
|
(TAO, HYPE, ADA, AVAX, NEAR…). XS01 **13/19** (mancano ARB, OP, APT, INJ, TIA, SEI → §50 resta).
|
||||||
|
Opzioni USDC-lineari su 7 sottostanti (SOL 676, HYPE 442, XRP 442, TRX, AVAX). Trovati in revisione:
|
||||||
|
**`BTCDVOL_USDC-30SEP26`** (unico long-vol diretto del venue: vol 0, OI 132, spread 12% → non
|
||||||
|
negoziabile) e **PAXG perp** (oro, dal 2024-12, $0,18M/g: non misurato, non morto, muri di SOL/XRP).
|
||||||
|
**VERDETTO: `QUATTRO STRUMENTI LIQUIDI, TRE GIA' NEL LIBRO O MISURATI`** — l'universo "piu' monete" di Deribit e' finito.
|
||||||
|
|
||||||
|
## 78 — MACRO-RISKOFF (TP01 flat ±2h intorno a FOMC e CPI: vale qualcosa?)
|
||||||
|
|
||||||
|
`scripts/research/r0910_macro_riskoff.py` (10/09, issue #9, ~3 min). Calendario letto dal web (Fed + BLS,
|
||||||
|
61 FOMC + 88 CPI, 2019-03 → 2026-09), 684 ore in finestra = 1,04% del tempo. Verdetto pre-registrato
|
||||||
|
(ΔSh>0 con fee · ≥95° pctl del null location-matched a 1.000 estrazioni · positivo in ≥70% degli anni).
|
||||||
|
**REFUTED 0/3**: ΔSharpe giornaliero **−0,097**, drift **−1,24%/anno** (≈ −0,14 €/g a $4.455), negativo in
|
||||||
|
6/8 anni, e il Δ vero sta al **4,2° percentile** del null — le ore intorno ai dati macro sono fra le MIGLIORI
|
||||||
|
in cui TP01 e' esposto (+1,05 bp/h contro +0,17, t = 1,57): l'1% del tempo porta il 7% del guadagno. Stessa
|
||||||
|
lezione del weekend (§3): la coda del trend vive nelle ore «pericolose». Il segno opposto NON si apre
|
||||||
|
(ipotesi nuova, sotto l'MDE, selezionata sull'esito). Diario `2026-09-10b-macro-riskoff.md`.
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
> `data/live/scale_history.jsonl`, `tests/test_book_scale.py` (T1-T11, **18 test verdi**).
|
> `data/live/scale_history.jsonl`, `tests/test_book_scale.py` (T1-T11, **18 test verdi**).
|
||||||
> 🚨 **`config/live.json` NON e' stato toccato**, come prescrive il punto 2: la chiave e' assente,
|
> 🚨 **`config/live.json` NON e' stato toccato**, come prescrive il punto 2: la chiave e' assente,
|
||||||
> vale 1,00, e T7 dimostra bit-exact che il libro e' quello di sempre (`max|diff| = 0.0`).
|
> vale 1,00, e T7 dimostra bit-exact che il libro e' quello di sempre (`max|diff| = 0.0`).
|
||||||
|
> 📌 **(10/09, issue #5) A2 conta dal 2026-09-01**, primo giorno di `scale_watch`: 180 giorni cadono il
|
||||||
|
> **2027-02-28**, ed e' la data «non prima del» del gate in CLAUDE.md §4. Il «2026-10-01» scritto il 01/09 era
|
||||||
|
> il punto 5 (30 giorni di sorvegliante) scambiato per la data del gate: incompatibile con A2 per costruzione.
|
||||||
> **Restano i punti 5-7:** ≥30 giorni a 1,00 col sorvegliante attivo, poi `GATE SCALA-01`, poi
|
> **Restano i punti 5-7:** ≥30 giorni a 1,00 col sorvegliante attivo, poi `GATE SCALA-01`, poi
|
||||||
> `r0726_fee_sensitivity` rifatto. Il punto 5 non e' burocrazia — e' l'unico modo di scoprire che il
|
> `r0726_fee_sensitivity` rifatto. Il punto 5 non e' burocrazia — e' l'unico modo di scoprire che il
|
||||||
> sorvegliante e' rotto mentre la leva e' ancora 1,00.
|
> sorvegliante e' rotto mentre la leva e' ancora 1,00.
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Revisione settimanale — 2026-09-09
|
||||||
|
|
||||||
|
*Scritta il 2026-09-09T21:59:07Z dal modello `claude-fable-5-1`, revisore diverso dall'autore del codice, in SOLA LETTURA: nessun file del repo, nessuna config, nessun ordine e' stato toccato. E' l'opinione di un lettore fallibile (P13): ogni numero e ogni proposta vanno verificati alla fonte prima di agire. Guardia sui numeri: **sospetta** — 11 numeri non presenti nel materiale: 1.011, 1.341, 1.38279, 1.40085, 2.034, 2.43408, 4.47099, 4.47746, 400, 40008, 6879.*
|
||||||
|
|
||||||
|
## Sintesi
|
||||||
|
|
||||||
|
Revisione del 2026-09-09 21:59Z (primo giro, manuale: il cron e' del lunedi'). **Nessun URGENTE.** Nella settimana 02-08/09 il libro ha fatto 24/24 giri ogni giorno con feed SKH a 0 min, leva lorda 0,26x, equity $4.477,46 e TWR +7,41% (giornale 08/09). Tre cose da verificare, in ordine:
|
||||||
|
|
||||||
|
1. **`usde_watch` del 07/09 ha stimato un reward di 400,08 USDE**, dove i giorni normali (08-09/09) danno 0,38. E' la differenza fra i 2.434,08 USDE entrati e i 2.034,0 contati come trade, con `n_trades` fermo a **50**. Il registro `usde_convert.jsonl` mostra 8 + 30 + 16 = 54 ordini: ne mancano 4 da 100 = 400. Un lettore che vede al massimo 50 trade attribuisce al reward cio' che era un acquisto (D2, P2). Se `rendimento()` usa quella finestra, il tasso che `quota_da_decidere()` stampa ogni giorno e' inquinato.
|
||||||
|
2. **Il cuscino USDC (debito §5.17) resta senza sorvegliante, e la sua misura non e' definita**: a `balance_watch` 21:42Z l'USDC ha balance 1.400,85 ma equity 1.382,79; con l'equity totale 4.470,99 (watermark) il cuscino al 30% vale ~$1.341, quindi lo slack e' **+$59 sul balance o +$41 sull'equity USDC**. Quale dei due conta va deciso prima di scrivere il sorvegliante.
|
||||||
|
3. **Operatore**: il versamento di $2.410,14 del 04/09 e' solo *rilevato* (salto 13:47→14:47); `movimenti_dichiarati.jsonl` contiene il solo €25 del 25/08 e la Nota di giornale e' vuota 7/7 giorni. CLAUDE.md §2 chiede la dichiarazione il giorno stesso, e §1 («soldi fermi») dice ancora «in attesa di importo e data» per il piano €5.000: non si capisce se il 04/09 ne e' il primo pezzo.
|
||||||
|
|
||||||
|
## Cosa non torna
|
||||||
|
|
||||||
|
- **«2.434 USDE in 39 ordini»** (diario `2026-09-06-usde-quota-topup` §3, CLAUDE.md §1) non si riproduce dal registro: 8 ordini nel primo record, 30 eventi nella prima sonda, 16 nella seconda. Il totale USDE torna, il conteggio no (N11).
|
||||||
|
- **Haircut, due affermazioni opposte in CLAUDE.md §1** (M28): una dice che il 5% e' confermato dal passaggio a X:SM (`available_funds` $2.011,09 contro $2.010,90 attesi *con* haircut 5%); poche righe sotto «oggi comunque non si applica», frase del mattino del 31/08 sotto S:SM. `config/live.json` `_nota_usde` dice ancora «il modello ATTIVO e' Segregated» e giustifica `depeg_crit 0.95` come «meta' del buffer di haircut (10%)»: col 5% il criterio dichiarato (P6) non descrive piu' la soglia, e la nota e' una diagnosi cablata (P4).
|
||||||
|
- **GATE SCALA-01: data e condizione A2 non concordano** (CLAUDE.md §4). «Non prima del 2026-10-01» sono 30 giorni dal 01/09; A2 chiede k0 «≥180 g col criterio passato ogni giorno», e il criterio lo misura solo `scale_watch`, attivo dal 01/09 (`scale_history.jsonl`). 180 giorni dal 01/09 cadono nel 2027; dal 20/06 a fine 2026. Da quando si conta va chiarito prima del 01/10, non quel giorno.
|
||||||
|
- **Sorveglianti di cui il materiale non prova il funzionamento** (P5): `venue_news.jsonl` ha 5 voci tutte `seed: true` viste il 2026-08-31T07:40Z e nulla dopo; `scale_watch_state.json` non ha timestamp (streak 0 puo' essere «non ho girato»); `edge_watch` e' citato a «stato 27/07» (CLAUDE.md §4); `fee_watch` non compare. `monitor_health` copre 8 monitor, nessuno di questi quattro.
|
||||||
|
- **Data di arming**: CLAUDE.md §0 «LIVE dal 2026-06-20», `config/live.json` «ARMATO 2026-06-23», primo segmento TWR dal 2026-06-23 (diario `2026-09-02-debito-14`). Una delle tre non e' la stessa data.
|
||||||
|
- **Richieste dell'analista senza risposta**: i 4 fill del 03/09 («dovrebbero essere ri-taglie»), la conferma del versamento 04/09, il SELL $−7 del 06/09, il round-trip a $+0,00 con fee 0,0055 del 08/09. Nessuna Nota. Non e' un guasto: e' un canale che non chiude (N9).
|
||||||
|
- **CLAUDE.md §13 dice 1.011 test**; il diario `2026-09-09c` ne aggiunge 16 (`test_revisione.py`). E `scripts/cron_review.sh`, `scripts/live/revisione.py`, `src/live/revisione.py` sono `??` in `git status` mentre la crontab li chiama lunedi' 06:15 (P16 li fa girare, P11 non li salva).
|
||||||
|
|
||||||
|
## Cosa e' maturo da decidere
|
||||||
|
|
||||||
|
| gate | data | criterio scritto | stato nel materiale |
|
||||||
|
|---|---|---|---|
|
||||||
|
| STATARB | 2026-09-27 (fra 18 g) | soglie 24/07 + diagnostica sempre-short | monitor OK; Sharpe −1,61 al 26/08, nessuna lettura piu' recente. Si legge il 27/09 |
|
||||||
|
| SCALA-01 | non prima del 2026-10-01 (22 g) | 9 condizioni tutte necessarie | A8/A9 fatti; A2 non maturo e ambiguo (sopra); A3, A6, A7 non risultano fatti |
|
||||||
|
| USDE-01 | chiuso 28/08 | — | quota 68,79% (09/09) sotto `quota_max_frac` 0,85: dentro la decisione dell'operatore del 30/08 e 06/09 |
|
||||||
|
| PREVDAY-01 | **senza data** | 10 condizioni, 8/10 | N10: un gate senza data non si chiude |
|
||||||
|
|
||||||
|
XSR01 (23/10) e DVOLSPREAD (24/10) cadono oltre 30 giorni. `edge_watch` e' continuo ma l'ultimo stato e' del 27/07: non determinabile se sia ancora 8/8.
|
||||||
|
|
||||||
|
Decisioni dell'operatore, oggi: (a) dichiarare il 04/09; (b) dire se il piano €5.000 e' iniziato; (c) definire il cuscino (balance o equity USDC). Nessuna decisione di §3 ha la colonna «cosa la riapre» soddisfatta: `weights_tilt_null` fallisce ancora (26/07, in-sample); il crollo catturato e' a vol bassa, sotto il gate e inverse (diario 09/09 §2).
|
||||||
|
|
||||||
|
## Proposte (nessuna azione eseguita)
|
||||||
|
|
||||||
|
1. **Agente**: verificare in `usde_watch` il limite di lettura dei trade e come `rendimento()` tratta la finestra del 07/09; rigenerare la stima con i 54 ordini (P2).
|
||||||
|
2. **Agente**: riallineare `_nota_usde` (modello attivo X:SM, criterio di `depeg_crit` al 5%, `venue_cap_frac` null) e sciogliere la contraddizione di §1 datando le due frasi (M28).
|
||||||
|
3. **Agente + operatore**: il sorvegliante del cuscino e' gia' descritto in §5.17 (`USDC − cuscino_richiesto` derivato da config, P1); manca solo la scelta balance/equity.
|
||||||
|
4. **Agente**: uno stato senza timestamp non distingue «zero» da «non ho girato» (P5): `scale_watch_state.json` e `venue_news` meritano l'ora dell'ultimo giro, e i quattro sorveglianti fuori da `monitor_health` una riga li'.
|
||||||
|
5. **Agente**: committare i file della revisione, aggiornare il conteggio test.
|
||||||
|
6. **Operatore**: dare una data a PREVDAY-01 o chiuderlo (N10).
|
||||||
|
7. Nessuna idea di strategia: la settimana ha gia' misurato crolli, opzioni e XRP contro `20-ondate` (diario 09/09 §0) e non ho nulla che batta i motivi elencati li'.
|
||||||
|
|
||||||
|
## Cosa ho letto e cosa mi mancava
|
||||||
|
|
||||||
|
Letto: CLAUDE.md intero; `config/live.json`; crontab; git log dal 26/08 e status; `monitor_health`; stati di scale_watch, scale_history, vrp_f_watch, usde_watch, usde_convert, balance_watch, venue_news, movimenti_dichiarati, watermark; giornali 02-08/09; diari 02/09 (tre), 06/09, 09/09 (tre).
|
||||||
|
|
||||||
|
Mancava: (1) stato con timestamp di `edge_watch`, `fee_watch`, `venue_watch`, `scale_watch`; (2) l'output di `usde_watch.rendimento()` e `quota_da_decidere()`; (3) la lettura corrente di `paper_statarb` (Sharpe a oggi, non al 26/08), dvolspread, xsr; (4) il contenuto di `cron_daily.sh`, cioe' quali sorveglianti chiama; (5) `venue_news` dopo il 31/08; (6) il diff di CLAUDE.md e memoria 40 non committati; (7) il registro fill della settimana (per i 4 fill del 03/09 e il +0,00 del 08/09); (8) un registro depositi del gateway, che «non espone get_deposits».
|
||||||
|
|
||||||
|
---
|
||||||
|
## Firma e materiale
|
||||||
|
|
||||||
|
- revisore: `claude-fable-5-1` · cadenza settimanale · giornale 7 g · diari 7 g · commit 14 g
|
||||||
|
- scritto da `scripts/live/revisione.py` (cron `scripts/cron_review.sh`); l'unico file scritto e' questo
|
||||||
|
|
||||||
|
| voce | caratteri | troncata |
|
||||||
|
|---|---|---|
|
||||||
|
| CLAUDE.md (stato, book, numeri, decisioni vincolanti, gate, debiti, regole) | 80,310 | |
|
||||||
|
| config/live.json (unica autorita' dei guardrail live) | 7,184 | |
|
||||||
|
| crontab installata (crontab -l) | 1,312 | |
|
||||||
|
| git log dal 2026-08-26 + stato del working tree | 4,543 | |
|
||||||
|
| monitor_health (i forward-monitor stanno registrando?) | 522 | |
|
||||||
|
| stato scale_watch_state.json | 149 | |
|
||||||
|
| stato scale_history.jsonl (ultime 3) | 259 | |
|
||||||
|
| stato vrp_f_watch/state.json | 453 | |
|
||||||
|
| stato usde_watch.jsonl (ultime 3) | 1,339 | |
|
||||||
|
| stato usde_convert.jsonl (ultime 3) | 9,979 | |
|
||||||
|
| stato balance_watch.jsonl (ultime 2) | 578 | |
|
||||||
|
| stato venue_news.jsonl (ultime 5) | 1,564 | |
|
||||||
|
| stato movimenti_dichiarati.jsonl | 405 | |
|
||||||
|
| stato equity_seen.json (watermark) | 73 | |
|
||||||
|
| giornale 2026-09-02 | 3,824 | |
|
||||||
|
| giornale 2026-09-03 | 3,745 | |
|
||||||
|
| giornale 2026-09-04 | 3,999 | |
|
||||||
|
| giornale 2026-09-05 | 3,645 | |
|
||||||
|
| giornale 2026-09-06 | 3,630 | |
|
||||||
|
| giornale 2026-09-07 | 3,846 | |
|
||||||
|
| giornale 2026-09-08 | 3,765 | |
|
||||||
|
| diario 2026-09-02-debito-14-report-twr.md | 6,952 | |
|
||||||
|
| diario 2026-09-02b-debito-7-cadenza-docstring.md | 6,061 | |
|
||||||
|
| diario 2026-09-02c-flag-e-pulizia.md | 5,874 | |
|
||||||
|
| diario 2026-09-06-usde-quota-topup.md | 6,571 | |
|
||||||
|
| diario 2026-09-09-crolli-opzioni-monete.md | 17,894 | |
|
||||||
|
| diario 2026-09-09b-debito-18-cblib-causale.md | 14,150 | |
|
||||||
|
| diario 2026-09-09c-revisione-settimanale.md | 5,514 | |
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
# MINUTO :25, e la scelta NON e' arbitraria:
|
# 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
|
# :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.
|
# 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.
|
# 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.
|
# :25 nessun altro job. Il giro dura ~3 minuti a 4 chiamate/s, quindi finisce ben prima del :30.
|
||||||
#
|
#
|
||||||
|
|||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Sorveglianza ORARIA del cuscino USDC di regolamento (debito §5.17, issue #3) — v2.0.0+.
|
||||||
|
# Minuto :53 = DOPO il giro del book (:47, i cui fill si regolano in USDC) e fuori dal minuto
|
||||||
|
# tondo (rate-limit per-IP), dal :25 (catena), dal :35 (usde) e dal :42 (balance).
|
||||||
|
# PUO' INVIARE ORDINI: sotto zero di slack lancia usde_convert --esegui (vende USDE), con le
|
||||||
|
# guardie dichiarate nel docstring di scripts/live/cuscino_watch.py; lo interruttore e' lo
|
||||||
|
# stesso del libro (execution_enabled in config/live.json).
|
||||||
|
export PATH="/home/adriano/.local/bin:$PATH"
|
||||||
|
cd /opt/docker/PythagorasGoal || exit 1
|
||||||
|
mkdir -p logs
|
||||||
|
{
|
||||||
|
echo "===== $(date -u '+%Y-%m-%dT%H:%M:%SZ') cron_cuscino ====="
|
||||||
|
uv run python scripts/live/cuscino_watch.py --quiet || true
|
||||||
|
echo "===== done $(date -u '+%H:%M:%SZ') ====="
|
||||||
|
} >> logs/cron_cuscino.log 2>&1
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Revisione SETTIMANALE in sola lettura (dal 2026-09-09): un revisore diverso dall'autore
|
||||||
|
# (claude-fable-5-1) rilegge CLAUDE.md, config, crontab, commit, monitor_health, giornale e diari,
|
||||||
|
# e scrive docs/revisioni/<data>.md + una sintesi Telegram. Propone, non esegue: l'unico file
|
||||||
|
# scritto e' il rapporto. Cadenza: lunedi' 06:15 UTC — fuori dal minuto :00 (rate-limit) e dallo
|
||||||
|
# slot di release Deribit (martedi' 09:00), dopo il cron_daily delle 00:30 (giornale di domenica chiuso).
|
||||||
|
export PATH="/home/adriano/.local/bin:$PATH"
|
||||||
|
cd /opt/docker/PythagorasGoal || exit 1
|
||||||
|
mkdir -p logs
|
||||||
|
{
|
||||||
|
echo "===== $(date -u '+%Y-%m-%dT%H:%M:%SZ') cron_review ====="
|
||||||
|
uv run python scripts/live/revisione.py --quiet || true
|
||||||
|
echo "===== done $(date -u '+%H:%M:%SZ') ====="
|
||||||
|
} >> logs/cron_review.log 2>&1
|
||||||
@@ -115,7 +115,21 @@ def scrivi(con, g: date, modello: str, secco: bool = False, verbose: bool = True
|
|||||||
return stato
|
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__":
|
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()
|
con = T.connect()
|
||||||
g = date.fromisoformat(_arg("--giorno", datetime.now(timezone.utc).date().isoformat()))
|
g = date.fromisoformat(_arg("--giorno", datetime.now(timezone.utc).date().isoformat()))
|
||||||
st = scrivi(con, g, modello=_arg("--modello", A.MODELLO_DEFAULT),
|
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)
|
rec["n_posizioni"] = sum(1 for p in pos if abs(float(p.get("size") or 0)) > 1)
|
||||||
except Exception:
|
except Exception:
|
||||||
rec["nozionale_lordo"], rec["n_posizioni"] = None, None
|
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
|
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
|
Senza entrambi e' un DRY-RUN (stampa il piano, NON invia). Reconciliation dopo ogni ordine; log in
|
||||||
data/live/book_executions.jsonl.
|
data/live/book_executions.jsonl.
|
||||||
|
|
||||||
⚠️ CADENZA: SKH01 decide su griglia 230m -> questo script va lanciato ogni ~230 minuti con la feed
|
⚠️ CADENZA: ORARIA — `47 * * * *` in `scripts/cron_book.sh` (il minuto e' spiegato li'). SKH01
|
||||||
fresca all'ultima barra chiusa (NON il cron giornaliero, che mancherebbe gli ingressi). Gli exit di
|
decide su griglia 230m, ma il giro e' IDEMPOTENTE (riconcilia al target netto corrente; sotto
|
||||||
SKH sono SOFTWARE (latenza fino a fine barra 230m); solo il disaster-SL (-30%) e' on-book.
|
`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 # DRY-RUN (piano, nessun ordine)
|
||||||
uv run python scripts/live/book_execute.py --execute # esegue SOLO se execution_enabled=true
|
uv run python scripts/live/book_execute.py --execute # esegue SOLO se execution_enabled=true
|
||||||
@@ -340,5 +355,14 @@ def main():
|
|||||||
raise
|
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__":
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("book_execute.py", USO, flag=("--execute",), con_valore=())
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -93,5 +93,13 @@ def main() -> None:
|
|||||||
print(" regime QUIET: carry non raccoglibile (coerente con luglio 2026).")
|
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__":
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("cc01_regime_watch.py", USO)
|
||||||
main()
|
main()
|
||||||
|
|||||||
Executable
+329
@@ -0,0 +1,329 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""cuscino_watch.py — sorveglianza ORARIA del cuscino USDC di regolamento. PUO' INVIARE ORDINI.
|
||||||
|
|
||||||
|
PERCHE' ESISTE (debito §5.17, issue #3). Dal 06/09 il 68,7% dell'equity e' USDE. P&L e funding dei
|
||||||
|
perp USDC-lineari si regolano in USDC: una perdita del libro consuma il cuscino e fa salire la quota
|
||||||
|
USDE DA SOLA, senza un ordine. `usde_watch` allerta solo su `quota > quota_max_frac` (0,85): a
|
||||||
|
68,7% lo slack sul cuscino era ~$41-58, e un saldo USDC negativo costa lo 0,05%/GIORNO (18,25%/anno,
|
||||||
|
4,3x la resa che l'USDE compra). Nessuno guardava la grandezza giusta.
|
||||||
|
|
||||||
|
LA MISURA (decisione dell'operatore, 10/09): l'EQUITY USDC del conto — balance piu' P&L non
|
||||||
|
realizzato — perche' e' quella che risponde del regolamento; il balance vedrebbe il buco solo a
|
||||||
|
trade chiuso. Il cuscino richiesto e' DERIVATO da config/live.json e src/live/book.py tramite
|
||||||
|
`usde.cuscino_richiesto_usd` (n_asset x frac x disaster_sl_pct x equity totale): la stessa formula
|
||||||
|
che `usde_convert` applica a ogni piano — un sorvegliante che ridichiara il proprio bersaglio non
|
||||||
|
sta controllando niente (P1, 5 occorrenze nel progetto).
|
||||||
|
|
||||||
|
QUATTRO STATI SULLO SLACK (= equity USDC − cuscino), in frazioni del cuscino lette da config
|
||||||
|
(`usde.bande_cuscino`: preavviso 0,10 · margine 0,20 · riacquisto 0,40 — P5, P1):
|
||||||
|
* SCOPERTO — slack < 0: 🚨 alla transizione, e VENDITA automatica di USDE fino alla quota di
|
||||||
|
ripristino q* = usde.quota_ripristino() = 1 − 0,30 × 1,20 = 0,64 (cuscino + 20%).
|
||||||
|
* PREAVVISO — 0 <= slack < 10% del cuscino: ⚠️ alla transizione. Latenza comprata apposta.
|
||||||
|
* OK — 10% <= slack <= 40%: niente.
|
||||||
|
* ECCEDENTE — slack > 40% del cuscino: ACQUISTO automatico di USDE fino allo stesso q* (issue #7,
|
||||||
|
decisione dell'operatore 10/09: «riportarla a quota in autonomia»). E' cio' che
|
||||||
|
riporta la quota a bersaglio dopo un bonifico (+0,7 × importo di slack) o dopo un
|
||||||
|
tratto di guadagni del libro, senza che nessuno se ne ricordi. 📌 alla transizione.
|
||||||
|
* BLIND — conto non leggibile: ⚠️ alla transizione. "Non vedo" non e' "va tutto bene".
|
||||||
|
ISTERESI: vendita sotto 0, acquisto sopra 0,40, entrambe verso 0,20. Per oscillare lo slack deve
|
||||||
|
muoversi di ±0,20 × cuscino = ±6% dell'equity, e siccome slack = 0,7·USDC − 0,3·USDE, servono
|
||||||
|
±8,6% di equity USDC (~$380 oggi); un giro costa ~6 bps sull'importo mosso. Soglie DICHIARATE, non ottimizzate (M8). Il vecchio `quota_target` 0,70
|
||||||
|
lasciava slack ZERO per costruzione: a quella quota ogni ora in perdita avrebbe venduto.
|
||||||
|
|
||||||
|
LE GUARDIE DELLA RICONVERSIONE (tutte dichiarate qui):
|
||||||
|
* `execution_enabled` in config/live.json deve essere true — lo stesso interruttore del libro:
|
||||||
|
disarmare il libro disarma anche questo, senza un secondo posto da ricordare.
|
||||||
|
* NON si converte sotto `depeg_warn`, in nessun verso: vendere USDE sotto la pari cristallizza
|
||||||
|
la perdita del collaterale, comprarne mentre scivola e' peggio; li' decide l'operatore.
|
||||||
|
* al massimo UN tentativo ogni RIPRISTINO_MIN_ORE (6h), in qualunque verso: un tentativo che si
|
||||||
|
ripete ogni ora e' spam, e un sorvegliante che si ripete si impara a ignorare (P14).
|
||||||
|
* dopo un tentativo FALLITO da ECCEDENTE si riprova dopo RITENTATIVO_FALLITO_ORE (24h): USDC
|
||||||
|
fermo non e' un rischio, e un guasto persistente (tetto del venue tornato, book illeggibile)
|
||||||
|
diventerebbe 4 Telegram al giorno per sempre (revisione fable 10/09). Da SCOPERTO il ritmo
|
||||||
|
resta 6h: li' il fallimento e' un rischio che costa 0,05%/giorno.
|
||||||
|
* `usde_convert` porta le proprie guardie (banda di prezzo, book leggibile, tetto HARD sulla
|
||||||
|
quota): questo script NON le duplica, le eredita lanciando lo script vero (P15: la regola si
|
||||||
|
prova contro il codice che la esegue).
|
||||||
|
* `--secco`: niente ordini, niente Telegram, NIENTE riga nel registro: stampa cosa FAREBBE.
|
||||||
|
* niente lock: un `usde_convert --esegui` lanciato a mano nello stesso minuto del cron (:53)
|
||||||
|
puo' vendere due volte verso bersagli diversi. Limite dichiarato (D5), non riparato.
|
||||||
|
|
||||||
|
TRASPORTO (lezione del debito §5.2): il marcatore "gia' detto" e' l'esito di `notify` scritto nel
|
||||||
|
record (`allertato`); se l'invio fallisce la transizione si ripete al giro dopo invece di andare
|
||||||
|
persa per l'episodio intero.
|
||||||
|
|
||||||
|
La serie data/live/cuscino_watch.jsonl e' dentro il perimetro di backup ed e' sorvegliata da
|
||||||
|
monitor_health (un watch fermo = nessuno guarda il cuscino, e il silenzio si legge come zero).
|
||||||
|
|
||||||
|
uv run python scripts/live/cuscino_watch.py # un giro: legge, giudica, agisce
|
||||||
|
uv run python scripts/live/cuscino_watch.py --quiet # stampa solo se cambia qualcosa
|
||||||
|
uv run python scripts/live/cuscino_watch.py --secco # nessun ordine, nessun Telegram
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from src.live import usde as U # noqa: E402
|
||||||
|
from src.live.notifier import notify # noqa: E402
|
||||||
|
|
||||||
|
STATE = ROOT / "data" / "live" / "cuscino_watch.jsonl"
|
||||||
|
CONVERT = ROOT / "scripts" / "live" / "usde_convert.py"
|
||||||
|
# Le bande (preavviso / margine / riacquisto) NON stanno qui: si leggono da config via
|
||||||
|
# usde.bande_cuscino() a ogni giro (P1). Il margine e' il DOPPIO del preavviso per costruzione
|
||||||
|
# dell'ordine imposto (revisione fable 10/09: a margine == preavviso ogni 🚨 era seguito da un ⚠️).
|
||||||
|
RIPRISTINO_MIN_ORE = 6.0 # un tentativo di conversione (in qualunque verso) ogni 6 ore
|
||||||
|
RITENTATIVO_FALLITO_ORE = 24.0 # da ECCEDENTE, dopo un fallimento: un tentativo al giorno
|
||||||
|
TIMEOUT_CONVERT_S = 900 # usde_convert spazia gli ordini di 12s: 60 ordini = 12 min
|
||||||
|
|
||||||
|
|
||||||
|
def leggi(path: Path = STATE) -> list[dict]:
|
||||||
|
if not path.exists():
|
||||||
|
return []
|
||||||
|
out = []
|
||||||
|
for ln in path.read_text().splitlines():
|
||||||
|
ln = ln.strip()
|
||||||
|
if ln:
|
||||||
|
out.append(json.loads(ln))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def giudica(eq_usdc: float | None, equity_tot: float | None, cuscino: float | None,
|
||||||
|
bande: dict | None = None) -> dict:
|
||||||
|
"""PURA. -> stato, slack, soglie in dollari. `None` in ingresso = BLIND. `bande` = le frazioni
|
||||||
|
del cuscino (default: config, via usde.bande_cuscino)."""
|
||||||
|
if eq_usdc is None or equity_tot is None or cuscino is None:
|
||||||
|
return dict(stato="BLIND", slack=None, preavviso_usd=None, riacquisto_usd=None)
|
||||||
|
try:
|
||||||
|
b = bande or U.bande_cuscino()
|
||||||
|
except Exception as e: # noqa: BLE001 — config rotta = "non vedo", non un crash
|
||||||
|
return dict(stato="BLIND", slack=None, preavviso_usd=None, riacquisto_usd=None,
|
||||||
|
motivo=f"bande del cuscino non leggibili: {e}")
|
||||||
|
slack = eq_usdc - cuscino
|
||||||
|
preavviso, riacquisto = b["preavviso"] * cuscino, b["riacquisto"] * cuscino
|
||||||
|
if slack < 0:
|
||||||
|
stato = "SCOPERTO"
|
||||||
|
elif slack < preavviso:
|
||||||
|
stato = "PREAVVISO"
|
||||||
|
elif slack > riacquisto:
|
||||||
|
stato = "ECCEDENTE"
|
||||||
|
else:
|
||||||
|
stato = "OK"
|
||||||
|
return dict(stato=stato, slack=round(slack, 2), preavviso_usd=round(preavviso, 2),
|
||||||
|
riacquisto_usd=round(riacquisto, 2))
|
||||||
|
|
||||||
|
|
||||||
|
AZIONE = {"SCOPERTO": "vendita", "ECCEDENTE": "acquisto"} # gli stati che convertono
|
||||||
|
|
||||||
|
|
||||||
|
def transizione(prev: dict | None, stato: str) -> bool:
|
||||||
|
"""PURA. Si allerta se lo stato e' cambiato, O se l'ultima allerta per questo stato non e'
|
||||||
|
partita (`allertato` False): un invio fallito non consuma la transizione (debito §5.2)."""
|
||||||
|
if stato == "OK":
|
||||||
|
return False
|
||||||
|
if prev is None or prev.get("stato") != stato:
|
||||||
|
return True
|
||||||
|
return not bool(prev.get("allertato"))
|
||||||
|
|
||||||
|
|
||||||
|
def puo_riconvertire(prev_records: list[dict], now_ms: int, execution_enabled: bool,
|
||||||
|
px: float | None, depeg_warn: float,
|
||||||
|
min_ore: float = RIPRISTINO_MIN_ORE, stato: str = "SCOPERTO",
|
||||||
|
dopo_fallito_ore: float = RITENTATIVO_FALLITO_ORE) -> tuple[bool, str]:
|
||||||
|
"""PURA. Le guardie della conversione automatica. -> (si', motivo se no). ⚠️ il motivo finisce
|
||||||
|
in un'allerta: niente '<' (Telegram HTML) — l'escape nel sink lo copre, ma qui non serve."""
|
||||||
|
if not execution_enabled:
|
||||||
|
return False, "execution_enabled=false in config/live.json (il libro e' disarmato: anche questo)"
|
||||||
|
if px is None:
|
||||||
|
return False, "prezzo USDE non leggibile: non si vende al buio (P5)"
|
||||||
|
if px < depeg_warn:
|
||||||
|
return False, f"USDE {px:.4f} sotto depeg_warn {depeg_warn}: ne' vendere (cristallizza) ne' comprare (scivola) — decide l'operatore"
|
||||||
|
# contano solo i tentativi VERI (subprocess lanciato): un record con `tentata=False` — prezzo
|
||||||
|
# illeggibile, --secco, interruttore spento — non consuma il budget (revisione fable 10/09)
|
||||||
|
ultimo = [r for r in prev_records if (r.get("riconversione") or {}).get("tentata")]
|
||||||
|
if ultimo:
|
||||||
|
ore = (now_ms - int(ultimo[-1]["ts"])) / 3_600_000
|
||||||
|
fallito = not (ultimo[-1].get("riconversione") or {}).get("ok")
|
||||||
|
soglia = dopo_fallito_ore if (fallito and stato == "ECCEDENTE") else min_ore
|
||||||
|
if ore < soglia:
|
||||||
|
return False, (f"ultimo tentativo {ore:.1f}h fa, sotto le {soglia:.0f}h"
|
||||||
|
+ (" (era FALLITO: da ECCEDENTE si riprova una volta al giorno)" if soglia == dopo_fallito_ore else "")
|
||||||
|
+ ": non si insiste")
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
|
def riconverti(quota: float, timeout_s: float = TIMEOUT_CONVERT_S) -> dict:
|
||||||
|
"""Lancia lo script VERO con le sue guardie. Ritorna esito + coda dell'output (P3)."""
|
||||||
|
cmd = [sys.executable, str(CONVERT), "--quota", f"{quota:.4f}", "--esegui"]
|
||||||
|
try:
|
||||||
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_s, cwd=str(ROOT))
|
||||||
|
coda = (r.stdout + r.stderr).strip().splitlines()[-12:]
|
||||||
|
return dict(rc=r.returncode, ok=r.returncode == 0, coda=coda, cmd=" ".join(cmd[1:]))
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return dict(rc=None, ok=False, coda=[f"timeout dopo {timeout_s:.0f}s"], cmd=" ".join(cmd[1:]))
|
||||||
|
except Exception as e: # noqa: BLE001 — si registra, non si ingoia
|
||||||
|
return dict(rc=None, ok=False, coda=[f"{type(e).__name__}: {e}"], cmd=" ".join(cmd[1:]))
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_client():
|
||||||
|
try:
|
||||||
|
from src.live.deribit import DeribitRead
|
||||||
|
return DeribitRead()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _execution_enabled() -> bool:
|
||||||
|
try:
|
||||||
|
return bool(json.loads((ROOT / "config" / "live.json").read_text()).get("execution_enabled"))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
quiet, secco = "--quiet" in sys.argv, "--secco" in sys.argv
|
||||||
|
c = U.cfg()
|
||||||
|
records = leggi()
|
||||||
|
prev = records[-1] if records else None
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
now_ms = int(now.timestamp() * 1000)
|
||||||
|
client = _safe_client()
|
||||||
|
|
||||||
|
eq_usdc = eq_usde = None
|
||||||
|
motivo_blind = None
|
||||||
|
if client is None:
|
||||||
|
motivo_blind = "gateway non raggiungibile"
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
eq_usdc = float(client.account_summary("USDC")["equity"])
|
||||||
|
except Exception as e:
|
||||||
|
motivo_blind = f"conto USDC non leggibile ({type(e).__name__})"
|
||||||
|
try:
|
||||||
|
eq_usde = float(client.account_summary("USDE").get("equity") or 0)
|
||||||
|
except Exception as e:
|
||||||
|
motivo_blind = motivo_blind or f"conto USDE non leggibile ({type(e).__name__})"
|
||||||
|
px, px_fonte = U.prezzo(client)
|
||||||
|
equity_tot = cuscino = come = None
|
||||||
|
if eq_usdc is not None and eq_usde is not None:
|
||||||
|
usd, _ = U.valuta(eq_usde, px)
|
||||||
|
equity_tot = eq_usdc + usd
|
||||||
|
cuscino, come = U.cuscino_richiesto_usd(equity_tot)
|
||||||
|
try:
|
||||||
|
bande = U.bande_cuscino(c)
|
||||||
|
except Exception as e: # noqa: BLE001 — si registra e si allerta, non si crasha (P5)
|
||||||
|
bande, motivo_blind = None, f"config usde: {e}"
|
||||||
|
g = giudica(eq_usdc, equity_tot, cuscino, bande)
|
||||||
|
stato = g["stato"]
|
||||||
|
if stato == "BLIND" and g.get("motivo"):
|
||||||
|
motivo_blind = motivo_blind or g["motivo"]
|
||||||
|
|
||||||
|
rec = dict(ts=now_ms, data=now.strftime("%Y-%m-%dT%H:%M:%SZ"), stato=stato,
|
||||||
|
motivo_blind=motivo_blind, eq_usdc=eq_usdc, eq_usde=eq_usde, px=px,
|
||||||
|
px_fonte=px_fonte, equity_tot=equity_tot, cuscino=cuscino, cuscino_come=come,
|
||||||
|
slack=g["slack"], preavviso_usd=g["preavviso_usd"], riacquisto_usd=g["riacquisto_usd"],
|
||||||
|
bande=bande, allertato=None, allerta_errore=None, riconversione=None, secco=secco)
|
||||||
|
|
||||||
|
# --- conversione automatica: SCOPERTO vende, ECCEDENTE compra, stesso bersaglio q* ---
|
||||||
|
if stato in AZIONE:
|
||||||
|
ok, perche = puo_riconvertire(records, now_ms, _execution_enabled(), px, c["depeg_warn"], stato=stato)
|
||||||
|
quota = U.quota_ripristino(bande["margine"])
|
||||||
|
if not ok:
|
||||||
|
rec["riconversione"] = dict(tentata=False, verso=AZIONE[stato], quota=quota, motivo=perche)
|
||||||
|
elif secco:
|
||||||
|
rec["riconversione"] = dict(tentata=False, verso=AZIONE[stato], quota=quota,
|
||||||
|
motivo=f"--secco: avrei lanciato usde_convert --quota {quota:.4f} --esegui ({AZIONE[stato]})")
|
||||||
|
else:
|
||||||
|
esito = riconverti(quota)
|
||||||
|
rec["riconversione"] = dict(tentata=True, verso=AZIONE[stato], quota=quota, **esito)
|
||||||
|
|
||||||
|
# --- allarmi: alla transizione, marcatore scritto con l'esito dell'invio ---
|
||||||
|
# ⚠️ Telegram e' in parse_mode HTML: un '<' nudo nel testo fa fallire l'invio (successo al
|
||||||
|
# primo giro vero, 13:53Z del 10/09: «(< 10% del cuscino)»). Qui non si scrive mai '<'.
|
||||||
|
def _esito_azione(r: dict) -> str:
|
||||||
|
if r.get("tentata"):
|
||||||
|
return (f"{r['verso']} " + ("ESEGUITA" if r.get("ok") else "FALLITA")
|
||||||
|
+ f" (quota bersaglio {r['quota']:.0%})")
|
||||||
|
return f"{r.get('verso', 'conversione')} NON tentata: {r.get('motivo')}"
|
||||||
|
|
||||||
|
if transizione(prev, stato):
|
||||||
|
if secco:
|
||||||
|
rec["allertato"] = False
|
||||||
|
elif stato == "SCOPERTO":
|
||||||
|
rec["allertato"] = notify("🚨 CUSCINO USDC SCOPERTO", {
|
||||||
|
"USDC equity": f"${eq_usdc:,.2f}", "cuscino richiesto": f"${cuscino:,.2f} ({come})",
|
||||||
|
"slack": f"${g['slack']:+,.2f}", "azione": _esito_azione(rec["riconversione"] or {}),
|
||||||
|
"nota": "un USDC negativo costa 0,05%/giorno; il libro si regola in USDC"}, tentativi=3)
|
||||||
|
elif stato == "PREAVVISO":
|
||||||
|
rec["allertato"] = notify("⚠️ cuscino USDC in esaurimento", {
|
||||||
|
"USDC equity": f"${eq_usdc:,.2f}", "cuscino richiesto": f"${cuscino:,.2f}",
|
||||||
|
"slack": f"${g['slack']:+,.2f} (sotto il {bande['preavviso']:.0%} del cuscino)",
|
||||||
|
"nota": f"sotto zero vende USDE da solo fino a quota {U.quota_ripristino(bande['margine']):.0%}"})
|
||||||
|
elif stato == "ECCEDENTE":
|
||||||
|
rec["allertato"] = notify("📌 cuscino USDC eccedente: riacquisto USDE", {
|
||||||
|
"USDC equity": f"${eq_usdc:,.2f}", "cuscino richiesto": f"${cuscino:,.2f}",
|
||||||
|
"slack": f"${g['slack']:+,.2f} (sopra il {bande['riacquisto']:.0%} del cuscino)",
|
||||||
|
"azione": _esito_azione(rec["riconversione"] or {})})
|
||||||
|
elif stato == "BLIND":
|
||||||
|
rec["allertato"] = notify("⚠️ cuscino_watch BLIND", {"motivo": motivo_blind or "?",
|
||||||
|
"nota": "'non vedo' non e' 'va tutto bene' (P5)"})
|
||||||
|
elif rec["riconversione"] and rec["riconversione"].get("tentata") and not secco:
|
||||||
|
# non e' una transizione ma e' un ordine mandato: si dice sempre
|
||||||
|
r = rec["riconversione"]
|
||||||
|
rec["allertato"] = notify(f"📌 cuscino USDC: {r['verso']} " + ("eseguita" if r["ok"] else "FALLITA"),
|
||||||
|
{"quota bersaglio": f"{r['quota']:.0%}", "esito": " | ".join(r["coda"][-3:])})
|
||||||
|
if rec["allertato"] is False and not secco:
|
||||||
|
from src.live.notifier import ultimo_errore
|
||||||
|
rec["allerta_errore"] = ultimo_errore() # P3: si registra dove si ingoia
|
||||||
|
|
||||||
|
# --secco e' secco ANCHE sul registro: un giro a mano non deve diventare il `prev` del cron,
|
||||||
|
# ne' far sembrare vivo un monitor fermo (revisione fable 10/09)
|
||||||
|
if not secco:
|
||||||
|
STATE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with STATE.open("a") as fh:
|
||||||
|
fh.write(json.dumps(rec) + "\n")
|
||||||
|
|
||||||
|
cambiato = prev is None or prev.get("stato") != stato or rec["riconversione"] is not None
|
||||||
|
if not quiet or cambiato:
|
||||||
|
print("=" * 78)
|
||||||
|
print(f" CUSCINO WATCH — {rec['data']} {'(SECCO)' if secco else ''}")
|
||||||
|
print("=" * 78)
|
||||||
|
if stato == "BLIND":
|
||||||
|
print(f" stato : BLIND — {motivo_blind}")
|
||||||
|
else:
|
||||||
|
print(f" conto : USDC equity ${eq_usdc:,.2f} + USDE {eq_usde:,.2f} @ {px if px else 'n/d'}"
|
||||||
|
f" ({px_fonte}) = ${equity_tot:,.2f}")
|
||||||
|
print(f" cuscino : ${cuscino:,.2f} richiesti ({come})")
|
||||||
|
print(f" slack : ${g['slack']:+,.2f} (preavviso sotto ${g['preavviso_usd']:,.2f},"
|
||||||
|
f" riacquisto sopra ${g['riacquisto_usd']:,.2f}, bersaglio quota {U.quota_ripristino(bande['margine']):.0%})")
|
||||||
|
print(f" stato : {stato}")
|
||||||
|
if rec["riconversione"]:
|
||||||
|
r = rec["riconversione"]
|
||||||
|
print(f" {r.get('verso', 'conversione'):<12}: {'TENTATA' if r.get('tentata') else 'NON tentata'} — quota {r['quota']:.4f}"
|
||||||
|
+ (f" — {r.get('motivo')}" if r.get("motivo") else ""))
|
||||||
|
for ln in r.get("coda", []):
|
||||||
|
print(f" {ln}")
|
||||||
|
if rec["allertato"] is not None:
|
||||||
|
print(f" allerta : {'inviata' if rec['allertato'] else 'NON inviata (si ripete al giro dopo)'}"
|
||||||
|
+ (f" — {rec['allerta_errore']}" if rec.get("allerta_errore") else ""))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
USO = """uso: cuscino_watch.py [--quiet] [--secco]
|
||||||
|
|
||||||
|
cuscino_watch.py — sorveglianza ORARIA del cuscino USDC; sotto zero vende USDE, sopra la banda ricompra.
|
||||||
|
uv run python scripts/live/cuscino_watch.py # un giro: legge, giudica, agisce
|
||||||
|
uv run python scripts/live/cuscino_watch.py --quiet # stampa solo se cambia qualcosa
|
||||||
|
uv run python scripts/live/cuscino_watch.py --secco # nessun ordine, nessun Telegram
|
||||||
|
|
||||||
|
Dettaglio nel docstring in testa al file."""
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("cuscino_watch.py", USO, flag=("--quiet", "--secco"), con_valore=())
|
||||||
|
sys.exit(main())
|
||||||
@@ -144,5 +144,15 @@ def main() -> int:
|
|||||||
return 0
|
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__":
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("edge_watch.py", USO, flag=("--quiet",), con_valore=())
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|||||||
@@ -252,5 +252,15 @@ def main() -> int:
|
|||||||
return 2 if r["level"] == "AZIONE" else 0
|
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__":
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("fee_watch.py", USO, flag=("--quiet",), con_valore=())
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|||||||
@@ -41,7 +41,19 @@ def una(con, g: date, nota: str | None = None, verbose: bool = True) -> Path:
|
|||||||
return f
|
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__":
|
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'
|
# 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.
|
# (letta dal DB) descrivono due istanti diversi e la pagina si contraddice da sola.
|
||||||
sys.path.insert(0, str(ROOT / "scripts" / "live"))
|
sys.path.insert(0, str(ROOT / "scripts" / "live"))
|
||||||
|
|||||||
@@ -137,5 +137,14 @@ def main():
|
|||||||
raise
|
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__":
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("live_execute.py", USO, flag=("--execute",), con_valore=())
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -74,5 +74,15 @@ def main():
|
|||||||
(f"{len(r['orders'])} ordine/i costruito/i sopra." if r["orders"] else "Target flat: 0 ordini."))
|
(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__":
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("live_trend.py", USO, flag=("--no-net",), con_valore=("--equity",))
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -88,5 +88,14 @@ def main():
|
|||||||
print(" Validato: invio ordine reale, fill, fee reali, reconciliation, ritorno a flat.")
|
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__":
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("microtest.py", USO, flag=("--live",), con_valore=())
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -55,5 +55,15 @@ def main() -> int:
|
|||||||
return 0
|
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__":
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("monitor_health.py", USO, flag=("--quiet",), con_valore=())
|
||||||
raise SystemExit(main())
|
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%}")
|
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__":
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("paper_combo.py", USO, flag=("--status", "--reset"), con_valore=())
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -87,5 +87,15 @@ def main():
|
|||||||
print(f" posizioni correnti: {pf.current_positions()}")
|
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__":
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("paper_portfolio.py", USO, flag=("--status", "--reset"), con_valore=())
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -53,6 +53,21 @@ MODELED_CAPITAL = 2000.0 # nominale, ribilanciamento continuo
|
|||||||
REAL_CAPITAL = 600.0 # capitale mainnet reale
|
REAL_CAPITAL = 600.0 # capitale mainnet reale
|
||||||
MIN_ORDER = 5.0 # min order Deribit -> sotto, il conto vero NON ribilancia
|
MIN_ORDER = 5.0 # min order Deribit -> sotto, il conto vero NON ribilancia
|
||||||
|
|
||||||
|
# --- GATE PRE-REGISTRATO `GATE PREVDAY-01` (2026-08-23, RESULTS-0822 §54) -----------------------
|
||||||
|
# Cablato qui il 2026-09-10 (issue #4): esisteva solo nel testo, e la revisione del 09/09 lo ha
|
||||||
|
# letto come "gate senza data". Decisione dell'operatore: tenere la data, cablare kill e veto.
|
||||||
|
GATE_DATE = "2027-06-21" # decisione piena: 7 condizioni (a)-(g), tutte necessarie
|
||||||
|
KILL_SHARPE = -0.50 # kill: Sharpe forward < -0,50 ...
|
||||||
|
KILL_MIN_ACTIVE_DAYS = 180 # ... su >= 180 giorni di barre ATTIVE (posizione != 0)
|
||||||
|
MIN_RECON_FRAC = 0.80 # veto d'integrita': >= 80% di barre ricostruibili dal feed
|
||||||
|
RECON_TOL = 1.5e-6 # net_modeled e' registrato a 6 decimali: mezzo ulp di tolleranza
|
||||||
|
RECENTI_GIORNI = 30 # "i minuti non registrati non devono crescere": ultimi 30g vs tutto
|
||||||
|
MIN_DIV_CRESCITA = 5 # ... ma UNA barra divergente su 1943 non e' una crescita (P14): la
|
||||||
|
# crescita richiede >= 5 divergenti recenti E tasso doppio del totale
|
||||||
|
# LENTE: il kill si misura sullo Sharpe GIORNALIERO (somma delle barre orarie per giorno UTC),
|
||||||
|
# la lente con cui e' misurato ogni altro sleeve del progetto — §54: sulla lente ORARIA lo stesso
|
||||||
|
# forward vale +2,04 contro +1,56 giornaliero. Al kill la famiglia NON si ri-ottimizza.
|
||||||
|
|
||||||
|
|
||||||
def build_bars() -> dict[str, pd.DataFrame]:
|
def build_bars() -> dict[str, pd.DataFrame]:
|
||||||
return {a: load(a, "1h").reset_index(drop=True) for a in ASSETS}
|
return {a: load(a, "1h").reset_index(drop=True) for a in ASSETS}
|
||||||
@@ -145,6 +160,151 @@ def advance(st: dict, dfs: dict) -> dict:
|
|||||||
return st
|
return st
|
||||||
|
|
||||||
|
|
||||||
|
def _returns_df() -> pd.DataFrame | None:
|
||||||
|
if not RETURNS_FILE.exists():
|
||||||
|
return None
|
||||||
|
r = pd.read_json(RETURNS_FILE, lines=True)
|
||||||
|
if r.empty:
|
||||||
|
return None
|
||||||
|
r["dt"] = pd.to_datetime(r["ts"], unit="ms", utc=True)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def sharpe_giornaliero(r: pd.DataFrame, col: str = "net_modeled") -> tuple[float, int]:
|
||||||
|
"""PURA. Sharpe annualizzato sui rendimenti GIORNALIERI (somma delle barre orarie per giorno
|
||||||
|
UTC) -> (sharpe, n_giorni). nan sotto 30 giorni o a varianza nulla."""
|
||||||
|
d = r.groupby(r["dt"].dt.floor("D"))[col].sum()
|
||||||
|
if len(d) < 30 or d.std() == 0:
|
||||||
|
return float("nan"), int(len(d))
|
||||||
|
return float(d.mean() / d.std() * np.sqrt(365.25)), int(len(d))
|
||||||
|
|
||||||
|
|
||||||
|
def giorni_attivi(r: pd.DataFrame) -> int:
|
||||||
|
"""PURA. Giorni UTC con almeno una barra a posizione != 0 (il kill conta le barre ATTIVE).
|
||||||
|
⚠️ le posizioni registrate sono quelle del libro REAL-$600 (advance scrive `pr`), lo Sharpe
|
||||||
|
del kill e' MODELED: divergono solo se un ingresso da 0 vale < $5 di nozionale, cosa che a
|
||||||
|
vol-target 20% non succede (10/09: 81/81 giorni). Dichiarato, non riparato (D5)."""
|
||||||
|
att = r[(r["pos_btc"] != 0) | (r["pos_eth"] != 0)]
|
||||||
|
return int(att["dt"].dt.floor("D").nunique())
|
||||||
|
|
||||||
|
|
||||||
|
def kill_check(sharpe: float, n_attivi: int, today: pd.Timestamp) -> tuple[str, str]:
|
||||||
|
"""PURA. -> (stato, motivo). KILL solo con >= 180 giorni attivi E Sharpe < -0,50 (§54):
|
||||||
|
a quell'orizzonte P(uccidere un edge vivo a Sharpe 1,2) = 9,7%."""
|
||||||
|
if n_attivi < KILL_MIN_ACTIVE_DAYS:
|
||||||
|
return "NON_MATURO", (f"{n_attivi} giorni attivi < {KILL_MIN_ACTIVE_DAYS}: il kill non e' "
|
||||||
|
f"leggibile (mancano {KILL_MIN_ACTIVE_DAYS - n_attivi} giorni)")
|
||||||
|
if not np.isfinite(sharpe):
|
||||||
|
return "NON_MISURABILE", "Sharpe forward non misurabile"
|
||||||
|
if sharpe < KILL_SHARPE:
|
||||||
|
return "KILL", f"Sharpe {sharpe:+.2f} < {KILL_SHARPE:+.2f} su {n_attivi} giorni attivi -> RITIRARE, senza ri-ottimizzare"
|
||||||
|
return "SUPERATO", f"Sharpe {sharpe:+.2f} >= {KILL_SHARPE:+.2f} su {n_attivi} giorni attivi"
|
||||||
|
|
||||||
|
|
||||||
|
def ricostruzione(r: pd.DataFrame, dfs: dict, start_ts: int) -> dict:
|
||||||
|
"""Rigioca la strategia CONGELATA sul feed di OGGI dallo `start_ts` del monitor e confronta
|
||||||
|
barra per barra `net_modeled` registrato con quello ricalcolato (ribilanciamento continuo,
|
||||||
|
la stessa aritmetica di `advance`). Una barra e' RICOSTRUIBILE se coincide entro RECON_TOL.
|
||||||
|
|
||||||
|
Cosa misura: quante barre registrate sono riproducibili dal dato certificato. Le divergenti
|
||||||
|
sono barre calcolate su un feed che il rebuild ha poi rivisto (§54: 67 su 1512, tutte
|
||||||
|
all'ora del cron) — e' la misura del veto d'integrita' (>= 80%, e i minuti persi non devono
|
||||||
|
crescere: qui, divergenti/giorno negli ultimi 30g contro l'intera finestra)."""
|
||||||
|
data = {}
|
||||||
|
for a in ASSETS:
|
||||||
|
df = dfs[a]
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
rr = np.zeros(len(c)); rr[1:] = c[1:] / c[:-1] - 1.0
|
||||||
|
data[a] = dict(ts=df["timestamp"].values.astype("int64"), r=rr, tgt=prevday_target(df))
|
||||||
|
idx = {a: {int(t): i for i, t in enumerate(data[a]["ts"])} for a in ASSETS}
|
||||||
|
# posizione iniziale come init_state: target all'ultima barra <= start_ts
|
||||||
|
pos = {}
|
||||||
|
for a in ASSETS:
|
||||||
|
i0 = max(i for t, i in idx[a].items() if t <= start_ts)
|
||||||
|
pos[a] = float(data[a]["tgt"][i0])
|
||||||
|
ricalc, presenti = [], []
|
||||||
|
for t in r["ts"].astype("int64"):
|
||||||
|
t = int(t)
|
||||||
|
if any(t not in idx[a] for a in ASSETS):
|
||||||
|
ricalc.append(np.nan); presenti.append(False); continue
|
||||||
|
net = 0.0
|
||||||
|
for a in ASSETS:
|
||||||
|
i = idx[a][t]
|
||||||
|
tgt = float(data[a]["tgt"][i])
|
||||||
|
net += WEIGHT * (pos[a] * float(data[a]["r"][i]) - FEE_SIDE * abs(tgt - pos[a]))
|
||||||
|
pos[a] = tgt
|
||||||
|
ricalc.append(net); presenti.append(True)
|
||||||
|
ricalc = np.asarray(ricalc, dtype=float)
|
||||||
|
reg = r["net_modeled"].values.astype(float)
|
||||||
|
ok = np.isfinite(ricalc) & (np.abs(ricalc - reg) <= RECON_TOL)
|
||||||
|
n = len(reg)
|
||||||
|
giorni = r["dt"].dt.floor("D")
|
||||||
|
span_g = max(1, int(giorni.nunique()))
|
||||||
|
recenti = r["dt"] >= (r["dt"].max() - pd.Timedelta(days=RECENTI_GIORNI))
|
||||||
|
div_rec = int((~ok[recenti.values]).sum())
|
||||||
|
g_rec = max(1, int(giorni[recenti].nunique()))
|
||||||
|
return dict(n=n, ricostruibili=int(ok.sum()), frac=float(ok.sum() / n) if n else float("nan"),
|
||||||
|
assenti_nel_feed=int((~np.asarray(presenti)).sum()),
|
||||||
|
div_per_giorno=float((n - ok.sum()) / span_g),
|
||||||
|
div_recenti=div_rec, div_per_giorno_recenti=float(div_rec / g_rec),
|
||||||
|
divergenti_ts=[int(t) for t in r["ts"].values[~ok]][:20])
|
||||||
|
|
||||||
|
|
||||||
|
def veto_check(frac: float, div_g_tot: float, div_g_rec: float, div_rec: int) -> tuple[bool, str]:
|
||||||
|
"""PURA. Veto d'integrita' (blocca, non decide): sotto l'80% di barre ricostruibili, o con le
|
||||||
|
divergenze che CRESCONO negli ultimi 30 giorni, la finestra si ESTENDE invece di decidere."""
|
||||||
|
motivi = []
|
||||||
|
if not np.isfinite(frac) or frac < MIN_RECON_FRAC:
|
||||||
|
motivi.append(f"barre ricostruibili {frac:.1%} < {MIN_RECON_FRAC:.0%}")
|
||||||
|
if div_rec >= MIN_DIV_CRESCITA and div_g_rec > 2.0 * div_g_tot + 1e-12:
|
||||||
|
motivi.append(f"divergenze in crescita: {div_rec} negli ultimi {RECENTI_GIORNI}g "
|
||||||
|
f"({div_g_rec:.2f}/g contro {div_g_tot:.2f}/g sull'intera finestra)")
|
||||||
|
return (not motivi), ("; ".join(motivi) if motivi else
|
||||||
|
f"ricostruibili {frac:.1%}, divergenti {div_rec} negli ultimi {RECENTI_GIORNI}g "
|
||||||
|
f"({div_g_rec:.2f}/g contro {div_g_tot:.2f}/g totale)")
|
||||||
|
|
||||||
|
|
||||||
|
def print_gate(st: dict, dfs: dict, today: pd.Timestamp | None = None) -> dict:
|
||||||
|
"""Stampa lo stato del gate pre-registrato e lo RITORNA (per i test e per chi legge il log).
|
||||||
|
Non decide niente: al kill e alla data stampa cosa la regola dice, in maiuscolo."""
|
||||||
|
r = _returns_df()
|
||||||
|
out = dict(gate_date=GATE_DATE, kill="NON_MISURABILE", veto_ok=None)
|
||||||
|
print(f"\n GATE PRE-REGISTRATO `PREVDAY-01` (23/08, RESULTS-0822 §54) — decisione {GATE_DATE}")
|
||||||
|
if r is None or len(r) < 2:
|
||||||
|
print(" serie forward assente o troppo corta: niente da misurare")
|
||||||
|
return out
|
||||||
|
today = today or pd.Timestamp.now(tz="UTC").normalize()
|
||||||
|
sh, n_g = sharpe_giornaliero(r)
|
||||||
|
n_att = giorni_attivi(r)
|
||||||
|
stato, motivo = kill_check(sh, n_att, today)
|
||||||
|
out.update(sharpe_giornaliero=sh, n_giorni=n_g, n_attivi=n_att, kill=stato, kill_motivo=motivo)
|
||||||
|
print(f" Sharpe forward GIORNALIERO (MODELED): {sh:+.2f} su {n_g} giorni, {n_att} attivi"
|
||||||
|
f" [lente oraria {_sharpe_orario(r):+.2f}: non si cita, §54]")
|
||||||
|
if stato == "KILL":
|
||||||
|
print(f" *** KILL: {motivo} ***")
|
||||||
|
else:
|
||||||
|
print(f" kill (Sharpe < {KILL_SHARPE:+.2f} su >= {KILL_MIN_ACTIVE_DAYS}g attivi): {stato} — {motivo}")
|
||||||
|
rc = ricostruzione(r, dfs, int(st["start_ts"]))
|
||||||
|
ok, vm = veto_check(rc["frac"], rc["div_per_giorno"], rc["div_per_giorno_recenti"], rc["div_recenti"])
|
||||||
|
out.update(ricostruzione=rc, veto_ok=ok, veto_motivo=vm)
|
||||||
|
print(f" veto d'integrita': {'ok' if ok else '*** VETO — la finestra si ESTENDE ***'} — {vm}"
|
||||||
|
f" ({rc['ricostruibili']}/{rc['n']} barre, {rc['assenti_nel_feed']} assenti nel feed)")
|
||||||
|
if today >= pd.Timestamp(GATE_DATE, tz="UTC"):
|
||||||
|
print(f" *** DECISIONE DOVUTA ({GATE_DATE}): (a) DSR >= 0,95 sulla famiglia dichiarata "
|
||||||
|
f"(b) cella al buio == congelata (c) delta libro > +0,05 col funding (d) weights_tilt_null "
|
||||||
|
f"(e) ADDS+robust_oos+non-hedge sulla cella al buio (f) day_boundary_robust "
|
||||||
|
f"(g) Sharpe forward > 0 [{sh:+.2f}] — TUTTE necessarie ***")
|
||||||
|
else:
|
||||||
|
print(f" decisione fra {(pd.Timestamp(GATE_DATE, tz='UTC') - today).days} giorni; "
|
||||||
|
f"le condizioni (a)-(b) sono STRUTTURALI e il forward non le cambia (§54)")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _sharpe_orario(r: pd.DataFrame) -> float:
|
||||||
|
x = r["net_modeled"].astype(float)
|
||||||
|
return float(x.mean() / x.std() * np.sqrt(24 * 365.25)) if x.std() > 0 else float("nan")
|
||||||
|
|
||||||
|
|
||||||
def print_status(st: dict, dfs: dict):
|
def print_status(st: dict, dfs: dict):
|
||||||
days = (max(int(dfs[a]["timestamp"].iloc[-1]) for a in ASSETS) - st["start_ts"]) / 86400_000
|
days = (max(int(dfs[a]["timestamp"].iloc[-1]) for a in ASSETS) - st["start_ts"]) / 86400_000
|
||||||
rm = st["cap_modeled"] / MODELED_CAPITAL - 1
|
rm = st["cap_modeled"] / MODELED_CAPITAL - 1
|
||||||
@@ -156,7 +316,9 @@ def print_status(st: dict, dfs: dict):
|
|||||||
print(f" MODELED ($2000 nominale): {rm*100:+6.2f}% eq ${st['cap_modeled']:.2f} maxDD {st['dd_modeled']*100:.1f}%")
|
print(f" MODELED ($2000 nominale): {rm*100:+6.2f}% eq ${st['cap_modeled']:.2f} maxDD {st['dd_modeled']*100:.1f}%")
|
||||||
print(f" REAL-$600 (min-order $5) : {rr*100:+6.2f}% eq ${st['cap_real']:.2f} maxDD {st['dd_real']*100:.1f}%")
|
print(f" REAL-$600 (min-order $5) : {rr*100:+6.2f}% eq ${st['cap_real']:.2f} maxDD {st['dd_real']*100:.1f}%")
|
||||||
print(f" -> fill-haircut MODELED-REAL: {(rm-rr)*100:+.2f} pp (lo scettico l'aveva segnalato)")
|
print(f" -> fill-haircut MODELED-REAL: {(rm-rr)*100:+.2f} pp (lo scettico l'aveva segnalato)")
|
||||||
print(f" log: {RETURNS_FILE}\n")
|
print(f" log: {RETURNS_FILE}")
|
||||||
|
print_gate(st, dfs)
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -224,5 +224,15 @@ def main() -> None:
|
|||||||
print("\n fatto. I file *.%s.* sono l'evidenza del difetto e restano nel backup." % SUFFISSO)
|
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__":
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("paper_regen.py", USO, flag=(), con_valore=("--solo",))
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"""Revisione settimanale in SOLA LETTURA: un modello DIVERSO dall'autore rilegge il sistema.
|
||||||
|
|
||||||
|
uv run python scripts/live/revisione.py # chiama il revisore, scrive docs/revisioni/<data>.md, Telegram
|
||||||
|
uv run python scripts/live/revisione.py --secco # solo la taglia del materiale: niente modello, niente file
|
||||||
|
uv run python scripts/live/revisione.py --quiet # per il cron (scripts/cron_review.sh, lunedi' 06:15 UTC)
|
||||||
|
|
||||||
|
Cosa scrive: SOLO il rapporto. Cosa non tocca: config, libro, cron, codice, dati. Le proposte
|
||||||
|
restano proposte; decide l'operatore. Dettagli e ragioni in `src/live/revisione.py`.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
USO = """uso: revisione.py [--giorno AAAA-MM-GG] [--modello NOME] [--secco] [--no-telegram] [--quiet]
|
||||||
|
--giorno G data della revisione (default: adesso, UTC)
|
||||||
|
--modello M revisore (default: src.live.revisione.MODELLO = claude-fable-5-1 — NON il modello che scrive il codice)
|
||||||
|
--secco stampa la taglia del materiale e si ferma: niente modello, niente file, niente Telegram
|
||||||
|
--no-telegram scrive il rapporto ma non manda la sintesi
|
||||||
|
--quiet stampa solo l'esito
|
||||||
|
⚠️ SPENDE una chiamata al modello e MANDA un messaggio Telegram, salvo --secco/--no-telegram.
|
||||||
|
Scrive un solo file: docs/revisioni/<data>.md."""
|
||||||
|
|
||||||
|
|
||||||
|
def _arg(flag: str, default=None):
|
||||||
|
a = sys.argv[1:]
|
||||||
|
return a[a.index(flag) + 1] if flag in a and a.index(flag) + 1 < len(a) else default
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("revisione.py", USO, flag=("--secco", "--no-telegram", "--quiet"),
|
||||||
|
con_valore=("--giorno", "--modello"))
|
||||||
|
from src.live import revisione as R
|
||||||
|
g = _arg("--giorno")
|
||||||
|
adesso = (datetime.fromisoformat(g).replace(tzinfo=timezone.utc) if g
|
||||||
|
else datetime.now(timezone.utc))
|
||||||
|
esito = R.scrivi(ROOT, adesso, modello=_arg("--modello", R.MODELLO),
|
||||||
|
secco="--secco" in sys.argv, telegram="--no-telegram" not in sys.argv,
|
||||||
|
quiet="--quiet" in sys.argv)
|
||||||
|
if "--quiet" in sys.argv and not esito.get("secco"):
|
||||||
|
print(f"revisione {esito['data']}: {esito.get('stato')}"
|
||||||
|
+ (f" — {esito.get('errore')}" if esito.get("errore") else "") + f" {esito.get('file')}")
|
||||||
|
raise SystemExit(0 if esito.get("secco") or esito.get("stato") in ("ok", "sospetta") else 1)
|
||||||
@@ -58,5 +58,15 @@ def main() -> None:
|
|||||||
print(f"\n invio: {rep['invio']}")
|
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__":
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("scale_watch.py", USO, flag=("--quiet", "--secco"), con_valore=())
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -146,5 +146,14 @@ def main() -> None:
|
|||||||
print("inviato" if send(txt) else "NON inviato (config Telegram assente o rete KO)")
|
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__":
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("telegram_daily.py", USO, flag=("--dry-run",), con_valore=())
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -91,8 +91,22 @@ def report() -> None:
|
|||||||
print(f" movimenti capitale : {r['certi']:+,.2f} certi | {r['ambigui']:+,.2f} ambiguo/i"
|
print(f" movimenti capitale : {r['certi']:+,.2f} certi | {r['ambigui']:+,.2f} ambiguo/i"
|
||||||
f" (restano nel P&L, dichiarati)")
|
f" (restano nel P&L, dichiarati)")
|
||||||
for ev in r["eventi"]:
|
for ev in r["eventi"]:
|
||||||
print(f" {ev['ts_dopo'][:16]} {ev['delta']:+,.2f} [{ev['classe']}]")
|
mk = ev.get("mercato_max_pct")
|
||||||
print(f" trading da arming : {r['trading']:+,.2f} (equity al netto dei movimenti certi)")
|
if ev.get("dichiarazione"):
|
||||||
|
dettaglio = ev["dichiarazione"]
|
||||||
|
elif mk is None:
|
||||||
|
dettaglio = ev.get("mercato_nota") or "mercato non misurabile"
|
||||||
|
else:
|
||||||
|
dettaglio = 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}]")
|
||||||
|
for av in r.get("avvisi") or []:
|
||||||
|
print(f" ⚠️ {av}")
|
||||||
|
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 di un movimento RILEVATO esce intera, quella di un DICHIARATO "
|
||||||
|
f"resta — vedi journal.rendimento_twr)")
|
||||||
if r["twr"] is None:
|
if r["twr"] is None:
|
||||||
print(f" TWR : n/d ({r['motivo']})")
|
print(f" TWR : n/d ({r['motivo']})")
|
||||||
else:
|
else:
|
||||||
@@ -118,8 +132,16 @@ def report() -> None:
|
|||||||
con.close()
|
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__":
|
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:
|
if "--reconcile" in args:
|
||||||
reconcile()
|
reconcile()
|
||||||
elif "--report" in args:
|
elif "--report" in args:
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ la size. Percio' si SPAZIANO gli ordini (`--pausa`) e si fa backoff sul TEMPO, r
|
|||||||
solo dopo rifiuti ripetuti. Dimezzare la size a ogni rifiuto, come faceva la prima versione, e' la
|
solo dopo rifiuti ripetuti. Dimezzare la size a ogni rifiuto, come faceva la prima versione, e' la
|
||||||
reazione sbagliata al sintomo giusto: consuma i tentativi senza toccare la causa.
|
reazione sbagliata al sintomo giusto: consuma i tentativi senza toccare la causa.
|
||||||
|
|
||||||
uv run python scripts/live/usde_convert.py --quota 0.70 # piano, nessun ordine
|
uv run python scripts/live/usde_convert.py --quota 0.64 # piano, nessun ordine (0,64 = bersaglio di cuscino_watch)
|
||||||
uv run python scripts/live/usde_convert.py --quota 0.70 --esegui # manda l'ordine
|
uv run python scripts/live/usde_convert.py --quota 0.64 --esegui # manda l'ordine
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -88,15 +88,10 @@ EPS_CUSCINO = 0.01 # 1 centesimo: il criterio e' ">= cuscino", e
|
|||||||
# questo epsilon il rumore float boccia il caso di pareggio.
|
# questo epsilon il rumore float boccia il caso di pareggio.
|
||||||
|
|
||||||
|
|
||||||
def cuscino_richiesto_usd(equity_tot: float) -> tuple[float, str]:
|
# Il cuscino di regolamento vive in src/live/usde.py dal 10/09 (issue #3): cuscino_watch lo
|
||||||
"""Cuscino USDC di REGOLAMENTO = disaster-SL sulla massima esposizione lorda del libro.
|
# sorveglia ogni ora e questo script lo rispetta a ogni piano — due lettori, UNA formula (P1).
|
||||||
Derivato da config/live.json + src/live/book.py, mai ridichiarato qui."""
|
# L'alias resta per r0906_usde_tetto_sonda, che lo importa da qui.
|
||||||
cfg = json.loads((ROOT / "config" / "live.json").read_text())
|
cuscino_richiesto_usd = U.cuscino_richiesto_usd
|
||||||
sl = float(cfg["disaster_sl_pct"])
|
|
||||||
from src.live import book as B
|
|
||||||
frac, n_asset = float(B.WEIGHT), 2
|
|
||||||
lordo_max = equity_tot * frac * n_asset
|
|
||||||
return lordo_max * sl, f"{n_asset} asset x frac {frac} x SL {sl:.0%} su ${equity_tot:,.2f}"
|
|
||||||
|
|
||||||
|
|
||||||
def book_top(timeout: float = 5.0) -> tuple[float | None, float | None]:
|
def book_top(timeout: float = 5.0) -> tuple[float | None, float | None]:
|
||||||
@@ -165,7 +160,9 @@ def piano(eq_usdc: float, eq_usde: float, px: float, quota_target: float,
|
|||||||
motivi.append(f"gia' a bersaglio ({quota_ora:.1%} vs {quota_target:.0%}, toll {TOLL:.1%})")
|
motivi.append(f"gia' a bersaglio ({quota_ora:.1%} vs {quota_target:.0%}, toll {TOLL:.1%})")
|
||||||
if abs(delta_usd) < MIN_ORDER_USD:
|
if abs(delta_usd) < MIN_ORDER_USD:
|
||||||
motivi.append(f"delta ${abs(delta_usd):,.2f} sotto il minimo ${MIN_ORDER_USD:,.2f}")
|
motivi.append(f"delta ${abs(delta_usd):,.2f} sotto il minimo ${MIN_ORDER_USD:,.2f}")
|
||||||
if cap_usd is not None and val_target > cap_usd + EPS_CUSCINO:
|
# il tetto del venue limita l'ACQUISTO di USDE: una VENDITA va sempre verso il tetto, e bloccarla
|
||||||
|
# renderebbe morta la riconversione automatica di cuscino_watch (revisione fable 10/09)
|
||||||
|
if side == "buy" and cap_usd is not None and val_target > cap_usd + EPS_CUSCINO:
|
||||||
motivi.append(
|
motivi.append(
|
||||||
f"TETTO DEL VENUE: bersaglio ${val_target:,.2f} sopra ${cap_usd:,.2f} "
|
f"TETTO DEL VENUE: bersaglio ${val_target:,.2f} sopra ${cap_usd:,.2f} "
|
||||||
f"({cap_frac:.1%} dell'equity, misurato {cfg.get('venue_cap_misurato')}). "
|
f"({cap_frac:.1%} dell'equity, misurato {cfg.get('venue_cap_misurato')}). "
|
||||||
|
|||||||
+64
-11
@@ -66,6 +66,25 @@ VERDETTO_DOPO = "2026-08-29T12:30:00Z" # ultima finestra reward del 29/08 conc
|
|||||||
DECISIONE_QUOTA_DAL = "2026-08-31" # lunedi': si decide la quota
|
DECISIONE_QUOTA_DAL = "2026-08-31" # lunedi': si decide la quota
|
||||||
EPS_GIORNI = 0.5 # sotto mezza giornata l'APR non si annualizza
|
EPS_GIORNI = 0.5 # sotto mezza giornata l'APR non si annualizza
|
||||||
|
|
||||||
|
# --- lettura dei trade spot: i DUE limiti del canale, misurati il 2026-09-10 -----------------
|
||||||
|
# (1) `count` massimo di Deribit per get_user_trades_by_instrument: 1000. Il 07/09 il lettore
|
||||||
|
# chiedeva limit=50 e la sonda del 06/09 aveva fatto 54 ordini: i 4 non letti (400 USDE)
|
||||||
|
# sono finiti nel «reward» (issue #2). Se il venue restituisce ESATTAMENTE `limit` righe la
|
||||||
|
# lista puo' essere troncata e la somma NON e' leggibile (P12: non si attribuisce).
|
||||||
|
# (2) il gateway chiama l'endpoint senza `historical`/timestamp: Deribit restituisce solo le
|
||||||
|
# ultime 24h — DOCUMENTATO ("Accessing historical trades and orders using API": recent
|
||||||
|
# trades 24h, orders 30 min, count max 1000) e misurato (il fill 08/09 06:47Z invisibile
|
||||||
|
# alle 12:55Z del 10/09, quello del 09/09 21:47Z visibile). Se l'ultima lettura e' piu'
|
||||||
|
# vecchia della finestra, i trade fra la lettura e il bordo sono INVISIBILI e la somma non
|
||||||
|
# e' leggibile.
|
||||||
|
# ⚠️ BUCO DICHIARATO (D5): il cron gira ogni 24h + pochi secondi; con la tolleranza di 5 min
|
||||||
|
# i trade nei primi ≤5 min dopo la lettura precedente sarebbero invisibili e sommati come
|
||||||
|
# «leggibile». Innocuo: nessun attrezzo converte USDE alle 12:35 UTC, e senza tolleranza il
|
||||||
|
# giro quotidiano sarebbe SEMPRE «non leggibile» e il tasso non avrebbe mai finestre nuove.
|
||||||
|
TRADE_LIMIT = 1000
|
||||||
|
FINESTRA_GATEWAY_MS = 24 * 3_600_000
|
||||||
|
TOLL_FINESTRA_MS = 5 * 60_000
|
||||||
|
|
||||||
|
|
||||||
def _parse(ts: str) -> datetime:
|
def _parse(ts: str) -> datetime:
|
||||||
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
||||||
@@ -177,14 +196,36 @@ def _safe_client():
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _trades_spot_da(client, ts_ms: int) -> tuple[float | None, int | None]:
|
def trade_copertura(n_righe: int, prev_ts_ms: int, now_ms: int,
|
||||||
"""Somma con segno (buy +, sell -) dell'USDE scambiato spot dopo ts_ms. None = non leggibile."""
|
limit: int = TRADE_LIMIT, finestra_ms: int = FINESTRA_GATEWAY_MS,
|
||||||
|
toll_ms: int = TOLL_FINESTRA_MS) -> str | None:
|
||||||
|
"""PURA. None se la lista di trade COPRE l'intervallo (prev, now]; altrimenti il motivo per cui
|
||||||
|
NON lo copre — e allora la somma non si usa (P12: un reward calcolato su una lista incompleta
|
||||||
|
e' un'invenzione, il 07/09 valeva 400 USDE)."""
|
||||||
|
if n_righe >= limit:
|
||||||
|
return f"lista troncata: {n_righe} righe = limite {limit} del venue"
|
||||||
|
if now_ms - prev_ts_ms > finestra_ms + toll_ms:
|
||||||
|
ore = (now_ms - prev_ts_ms) / 3_600_000
|
||||||
|
return (f"ultima lettura {ore:.1f}h fa, oltre la finestra di {finestra_ms / 3_600_000:.0f}h "
|
||||||
|
f"del gateway: i trade piu' vecchi sono invisibili")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _trades_spot_da(client, ts_ms: int, now_ms: int | None = None
|
||||||
|
) -> tuple[float | None, int | None, str | None]:
|
||||||
|
"""Somma con segno (buy +, sell -) dell'USDE scambiato spot dopo ts_ms.
|
||||||
|
-> (somma, n, motivo). somma None = non leggibile, e `motivo` dice perche' (P4)."""
|
||||||
if client is None:
|
if client is None:
|
||||||
return None, None
|
return None, None, "gateway non raggiungibile"
|
||||||
try:
|
try:
|
||||||
rows = client.trade_history(U.SPOT, limit=50)
|
rows = client.trade_history(U.SPOT, limit=TRADE_LIMIT)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
return None, None
|
return None, None, f"trade_history: {type(e).__name__}"
|
||||||
|
if now_ms is None:
|
||||||
|
now_ms = int(datetime.now(timezone.utc).timestamp() * 1000)
|
||||||
|
motivo = trade_copertura(len(rows), ts_ms, now_ms)
|
||||||
|
if motivo is not None:
|
||||||
|
return None, None, motivo
|
||||||
tot, n = 0.0, 0
|
tot, n = 0.0, 0
|
||||||
for t in rows:
|
for t in rows:
|
||||||
if int(t.get("timestamp") or 0) <= ts_ms:
|
if int(t.get("timestamp") or 0) <= ts_ms:
|
||||||
@@ -192,7 +233,7 @@ def _trades_spot_da(client, ts_ms: int) -> tuple[float | None, int | None]:
|
|||||||
amt = float(t.get("amount") or 0)
|
amt = float(t.get("amount") or 0)
|
||||||
tot += amt if (t.get("direction") or "").lower() == "buy" else -amt
|
tot += amt if (t.get("direction") or "").lower() == "buy" else -amt
|
||||||
n += 1
|
n += 1
|
||||||
return round(tot, 8), n
|
return round(tot, 8), n, None
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
@@ -221,10 +262,11 @@ def main() -> int:
|
|||||||
if eq_usde is not None:
|
if eq_usde is not None:
|
||||||
usd, _nota = U.valuta(eq_usde, px)
|
usd, _nota = U.valuta(eq_usde, px)
|
||||||
|
|
||||||
trades_usde, n_trades = (None, None)
|
trades_usde, n_trades, trades_motivo = (None, None, None)
|
||||||
an = dict(delta=None, reward_stimato=None, reward_rilevato=False, con_trade=None)
|
an = dict(delta=None, reward_stimato=None, reward_rilevato=False, con_trade=None)
|
||||||
if stato == "OK" and prev is not None and prev.get("eq_usde") is not None:
|
if stato == "OK" and prev is not None and prev.get("eq_usde") is not None:
|
||||||
trades_usde, n_trades = _trades_spot_da(client, int(prev["ts"]))
|
trades_usde, n_trades, trades_motivo = _trades_spot_da(
|
||||||
|
client, int(prev["ts"]), int(now.timestamp() * 1000))
|
||||||
an = analizza(prev, eq_usde, trades_usde)
|
an = analizza(prev, eq_usde, trades_usde)
|
||||||
|
|
||||||
quota = None
|
quota = None
|
||||||
@@ -237,7 +279,7 @@ def main() -> int:
|
|||||||
stato=stato, motivo_blind=motivo_blind,
|
stato=stato, motivo_blind=motivo_blind,
|
||||||
eq_usde=eq_usde, px=px, px_fonte=px_fonte, usd=usd,
|
eq_usde=eq_usde, px=px, px_fonte=px_fonte, usd=usd,
|
||||||
eq_usdc=eq_usdc, quota=quota,
|
eq_usdc=eq_usdc, quota=quota,
|
||||||
trades_usde=trades_usde, n_trades=n_trades, **an,
|
trades_usde=trades_usde, n_trades=n_trades, trades_motivo=trades_motivo, **an,
|
||||||
)
|
)
|
||||||
v, v_motivo = verdetto(records + [rec], now)
|
v, v_motivo = verdetto(records + [rec], now)
|
||||||
rec["verdetto"], rec["verdetto_motivo"] = v, v_motivo
|
rec["verdetto"], rec["verdetto_motivo"] = v, v_motivo
|
||||||
@@ -312,7 +354,8 @@ def main() -> int:
|
|||||||
f" fa margine). Con cross X:SM sarebbero ~${cross:,.0f} (haircut"
|
f" fa margine). Con cross X:SM sarebbero ~${cross:,.0f} (haircut"
|
||||||
f" {c['haircut']:.0%})")
|
f" {c['haircut']:.0%})")
|
||||||
if an["delta"] is not None:
|
if an["delta"] is not None:
|
||||||
attr = ("non attribuibile (trade non leggibili)" if an["reward_stimato"] is None
|
attr = (f"non attribuibile (trade non leggibili: {trades_motivo})"
|
||||||
|
if an["reward_stimato"] is None
|
||||||
else f"reward stimato {an['reward_stimato']:+.6f} USDE"
|
else f"reward stimato {an['reward_stimato']:+.6f} USDE"
|
||||||
+ (" [con trade nel mezzo]" if an["con_trade"] else ""))
|
+ (" [con trade nel mezzo]" if an["con_trade"] else ""))
|
||||||
print(f" delta : {an['delta']:+.6f} USDE dall'ultima lettura -> {attr}")
|
print(f" delta : {an['delta']:+.6f} USDE dall'ultima lettura -> {attr}")
|
||||||
@@ -330,5 +373,15 @@ def main() -> int:
|
|||||||
return 0
|
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__":
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("usde_watch.py", USO, flag=("--quiet",), con_valore=())
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|||||||
@@ -98,5 +98,15 @@ def main() -> int:
|
|||||||
return 0
|
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__":
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("venue_watch.py", USO, flag=("--quiet",), con_valore=())
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ lontana, che il 30/07 aveva misurato sottoprezzata ~2.3x.
|
|||||||
piu' mispriced in RELATIVO (f_long 5.85 contro 2.23) ma pesa il **3.2%** del premio corto invece
|
piu' mispriced in RELATIVO (f_long 5.85 contro 2.23) ma pesa il **3.2%** del premio corto invece
|
||||||
del **18.4%**, quindi sul credito NETTO l'effetto e' minore: f_net **0.852** contro **0.718**.
|
del **18.4%**, quindi sul credito NETTO l'effetto e' minore: f_net **0.852** contro **0.718**.
|
||||||
Meccanismo giusto, segno della conclusione sbagliato.
|
Meccanismo giusto, segno della conclusione sbagliato.
|
||||||
|
(Numeri del 30/07, misurati con `cblib` che leggeva spot e DVOL di UN'ORA / fino a 24 ORE dopo
|
||||||
|
l'ingresso — debito §5.18, riparato la sera del 09/09: da allora la tabella stampata da questo
|
||||||
|
script e' la fonte, e il f canonico e' passato da 0,732 a 0,706 sulle stesse 28 coppie.)
|
||||||
|
|
||||||
E' pero' una misura su **16 coppie**: la differenza appaiata e' **+0.109 con IC95
|
E' pero' una misura su **16 coppie**: la differenza appaiata e' **+0.109 con IC95
|
||||||
[-0.047, +0.193]**, cioe' **compatibile con zero**. Serve piu' campione, e "quando ce ne sara'
|
[-0.047, +0.193]**, cioe' **compatibile con zero**. Serve piu' campione, e "quando ce ne sara'
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ Regola che vale per ogni misura fatta con questo modulo: il f della sola gamba c
|
|||||||
dello spread. Il modello sbaglia soprattutto sull'ala che si COMPRA (piu' OTM, IV piu' alta per
|
dello spread. Il modello sbaglia soprattutto sull'ala che si COMPRA (piu' OTM, IV piu' alta per
|
||||||
skew, prezzata dal modello a vol ATM) -> misurare una gamba sola da' la risposta sbagliata con
|
skew, prezzata dal modello a vol ATM) -> misurare una gamba sola da' la risposta sbagliata con
|
||||||
segno rassicurante.
|
segno rassicurante.
|
||||||
|
|
||||||
|
Seconda regola (09/09, debito §5.18): spot e DVOL escono da qui gia' CAUSALI — `asof(ts)` da'
|
||||||
|
l'ultima chiusura nota a ts. Chi costruisce una serie di prezzi altrove usi `causale()`, non
|
||||||
|
l'indice del feed com'e' (etichettato all'apertura: D6).
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -65,23 +69,48 @@ def puts(asset: str, df: pd.DataFrame | None = None) -> pd.DataFrame:
|
|||||||
return d[(d["asset"] == asset) & (d["option_type"] == "P")]
|
return d[(d["asset"] == asset) & (d["option_type"] == "P")]
|
||||||
|
|
||||||
|
|
||||||
|
def causale(s: pd.Series, cadenza: str) -> pd.Series:
|
||||||
|
"""Rietichetta una serie di CHIUSURE dall'APERTURA della barra all'istante in cui la chiusura
|
||||||
|
e' nota (apertura + cadenza), cosi' `s.asof(ts)` restituisce l'ultima chiusura CONOSCIUTA a ts
|
||||||
|
e mai una futura. I valori non si toccano, solo le etichette.
|
||||||
|
|
||||||
|
🚨 Perche' esiste (debito §5.18, 09/09): i feed del progetto sono etichettati all'apertura —
|
||||||
|
la barra 1h `20:00` chiude col 5m delle 20:55 (verificato: chiusura 1h a T == chiusura 5m a
|
||||||
|
T+55 nel 100% delle barre) e la riga DVOL del giorno D e' la chiusura delle 23:00 di D
|
||||||
|
(verificato contro la risoluzione 1h dell'API pubblica). Senza questo spostamento `asof(ts)`
|
||||||
|
dava lo spot di UN'ORA dopo e il DVOL di FINO A 24 ORE dopo l'ingresso (D6, quinta occorrenza)."""
|
||||||
|
out = s.copy()
|
||||||
|
out.index = out.index + pd.Timedelta(cadenza)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=8)
|
@lru_cache(maxsize=8)
|
||||||
def spot_series(asset: str) -> pd.Series:
|
def spot_series(asset: str) -> pd.Series:
|
||||||
|
"""Chiusure 1h del feed certificato, etichettate all'istante in cui sono NOTE (apertura + 1h).
|
||||||
|
|
||||||
|
`spot_series(a).asof(ts)` = ultima chiusura oraria conosciuta a `ts`. Conseguenza sul
|
||||||
|
regolamento: `asof(exp)` alle 08:00 e' la chiusura della barra 07:00, cioe' il prezzo delle
|
||||||
|
08:00 — prima era il prezzo delle 09:00, un'ora DOPO la scadenza."""
|
||||||
from scripts.analysis.research_lab import load_tf
|
from scripts.analysis.research_lab import load_tf
|
||||||
px = load_tf(asset, "1h")
|
px = load_tf(asset, "1h")
|
||||||
s = pd.Series(px["close"].values.astype(float),
|
s = pd.Series(px["close"].values.astype(float),
|
||||||
index=pd.to_datetime(px["timestamp"], unit="ms", utc=True)).sort_index()
|
index=pd.to_datetime(px["timestamp"], unit="ms", utc=True)).sort_index()
|
||||||
s.index = pd.DatetimeIndex(s.index).as_unit("ns") # le quote bite hanno microsecondi
|
s.index = pd.DatetimeIndex(s.index).as_unit("ns") # le quote bite hanno microsecondi
|
||||||
return s
|
return causale(s, "1h")
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=8)
|
@lru_cache(maxsize=8)
|
||||||
def dvol_series(asset: str) -> pd.Series:
|
def dvol_series(asset: str) -> pd.Series:
|
||||||
|
"""DVOL giornaliero (chiusura), etichettato all'istante in cui e' NOTO (giorno + 1D).
|
||||||
|
|
||||||
|
`dvol_series(a).asof(ts)` = ultima chiusura giornaliera del DVOL conosciuta a `ts`: dentro il
|
||||||
|
giorno D e' la chiusura di D−1, quindi vecchia fino a 23 ore. E' il prezzo della causalita'
|
||||||
|
con un feed giornaliero; l'alternativa (DVOL orario dall'API pubblica) non e' nel feed."""
|
||||||
d = pd.read_parquet(RAW / f"dvol_{asset.lower()}.parquet")
|
d = pd.read_parquet(RAW / f"dvol_{asset.lower()}.parquet")
|
||||||
s = pd.Series(d["close"].values.astype(float),
|
s = pd.Series(d["close"].values.astype(float),
|
||||||
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)).sort_index()
|
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)).sort_index()
|
||||||
s.index = pd.DatetimeIndex(s.index).as_unit("ns")
|
s.index = pd.DatetimeIndex(s.index).as_unit("ns")
|
||||||
return s
|
return causale(s, "1D")
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ prezzo
|
# ------------------------------------------------------------------ prezzo
|
||||||
|
|||||||
@@ -310,11 +310,15 @@ def boot_ci(x: np.ndarray, n: int = 4000, seed: int = 822) -> tuple[float, float
|
|||||||
def main() -> None:
|
def main() -> None:
|
||||||
ap = argparse.ArgumentParser()
|
ap = argparse.ArgumentParser()
|
||||||
ap.add_argument("--no-live", action="store_true", help="non interrogare il venue")
|
ap.add_argument("--no-live", action="store_true", help="non interrogare il venue")
|
||||||
|
ap.add_argument("--al", default=None,
|
||||||
|
help="data di taglio UTC (riproduzione): esclude le scadenze >= questa data; default adesso")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
print(__doc__.split("\n\n")[0])
|
print(__doc__.split("\n\n")[0])
|
||||||
puts = load_puts()
|
puts = load_puts()
|
||||||
today = pd.Timestamp.now(tz="UTC")
|
today = pd.Timestamp(args.al, tz="UTC") if args.al else pd.Timestamp.now(tz="UTC")
|
||||||
|
print(f"\n taglio dei trade: scadenze < {today:%Y-%m-%d %H:%M}Z"
|
||||||
|
+ (" (--al: riproduzione)" if args.al else " (adesso)"))
|
||||||
venue = venue_read(not args.no_live)
|
venue = venue_read(not args.no_live)
|
||||||
|
|
||||||
# ------------------------------------------------------------ §1 REGIME
|
# ------------------------------------------------------------ §1 REGIME
|
||||||
@@ -323,7 +327,7 @@ def main() -> None:
|
|||||||
"misurato 4 volte). Se nel campione quel gate non scatta mai, il campione non contiene\n"
|
"misurato 4 volte). Se nel campione quel gate non scatta mai, il campione non contiene\n"
|
||||||
"la strategia — contiene la sua astensione.\n")
|
"la strategia — contiene la sua astensione.\n")
|
||||||
for a in ASSETS:
|
for a in ASSETS:
|
||||||
V = CB.dvol_series(a)
|
V = CB.causale(CB.dvol_series(a), "-1D") # calendario: il DVOL DEL giorno D (la serie di cblib e' causale: sta a D+1)
|
||||||
win = V[(V.index >= puts["ts"].min()) & (V.index <= puts["ts"].max())]
|
win = V[(V.index >= puts["ts"].min()) & (V.index <= puts["ts"].max())]
|
||||||
hist = V[V.index < win.index[0]].to_numpy(float)
|
hist = V[V.index < win.index[0]].to_numpy(float)
|
||||||
print(f" {a}: DVOL nel campione min {win.min():.1f} / mediana {win.median():.1f} / "
|
print(f" {a}: DVOL nel campione min {win.min():.1f} / mediana {win.median():.1f} / "
|
||||||
@@ -372,7 +376,7 @@ def main() -> None:
|
|||||||
print(" Regola del 19/06: «rivalutare quando la catena cattura un crash». Un criterio senza\n"
|
print(" Regola del 19/06: «rivalutare quando la catena cattura un crash». Un criterio senza\n"
|
||||||
" numero non e' un criterio, quindi eccolo (IV-rank espandente causale, come nel sleeve):")
|
" numero non e' un criterio, quindi eccolo (IV-rank espandente causale, come nel sleeve):")
|
||||||
for a in ASSETS:
|
for a in ASSETS:
|
||||||
V = CB.dvol_series(a)
|
V = CB.causale(CB.dvol_series(a), "-1D") # idem: date di calendario, non etichette causali
|
||||||
vals = V.to_numpy(float)
|
vals = V.to_numpy(float)
|
||||||
ivr = np.full(len(vals), np.nan)
|
ivr = np.full(len(vals), np.nan)
|
||||||
for i in range(1, len(vals)):
|
for i in range(1, len(vals)):
|
||||||
@@ -753,13 +757,17 @@ def main() -> None:
|
|||||||
|
|
||||||
# ------------------------------------------------------------ §7 VERDETTO
|
# ------------------------------------------------------------ §7 VERDETTO
|
||||||
hr("§7 VERDETTO")
|
hr("§7 VERDETTO")
|
||||||
print(""" SCARTATO — su questi dati la domanda non e' decidibile. Quattro motivi, indipendenti
|
print(""" (numeri della stesura del 22/08, RIMISURATI il 09/09 con spot e DVOL causali — debito §5.18:
|
||||||
|
erano 0.714 [0.690, 0.779] · 32.58 / 1.45 / -0.35 · 3.90 -> 2.41 · -1.35% -> +11.22% · +25% / +44%.
|
||||||
|
Il verdetto non cambia: le quattro ragioni non dipendono da un'ora di spot.)
|
||||||
|
|
||||||
|
SCARTATO — su questi dati la domanda non e' decidibile. Quattro motivi, indipendenti
|
||||||
l'uno dall'altro: ciascuno da solo basterebbe.
|
l'uno dall'altro: ciascuno da solo basterebbe.
|
||||||
|
|
||||||
1. IL CAMPIONE NON CONTIENE LA STRATEGIA. 0 settimane su 19 passano il gate IV-rank>0.30
|
1. IL CAMPIONE NON CONTIENE LA STRATEGIA. 0 settimane su 19 passano il gate IV-rank>0.30
|
||||||
che E' l'alpha di VRP01 — il sleeve sarebbe stato FLAT per l'intero campione. La DVOL
|
che E' l'alpha di VRP01 — il sleeve sarebbe stato FLAT per l'intero campione. La DVOL
|
||||||
mediana sta al 7 (BTC) / 11 (ETH) percentile della storia 2021+, e il sottostante e'
|
mediana sta al 7 (BTC) / 11 (ETH) percentile della storia 2021+, e il sottostante e'
|
||||||
salito del +25% / +44%: vol implicita bassa E rialzo forte = il regime MIGLIORE
|
salito del +21% / +40%: vol implicita bassa E rialzo forte = il regime MIGLIORE
|
||||||
possibile per vendere put, non uno neutro. Le settimane sono 10 per asset, non 16
|
possibile per vendere put, non uno neutro. Le settimane sono 10 per asset, non 16
|
||||||
(prima del 2026-06-09 l'archivio seguiva solo la finestra 13-20 DTE: quella 4-10 DTE
|
(prima del 2026-06-09 l'archivio seguiva solo la finestra 13-20 DTE: quella 4-10 DTE
|
||||||
non esiste). Esito: 10/10 vincenti su BTC, e la lente giusta non e' lo Sharpe ma la
|
non esiste). Esito: 10/10 vincenti su BTC, e la lente giusta non e' lo Sharpe ma la
|
||||||
@@ -770,13 +778,13 @@ def main() -> None:
|
|||||||
regime di vol bassa persistente, non davanti a un muro.
|
regime di vol bassa persistente, non davanti a un muro.
|
||||||
|
|
||||||
2. IL TITOLO NON SOPRAVVIVE ALL'ORA D'INGRESSO. E' un max-of-k mai dichiarato, misurato
|
2. IL TITOLO NON SOPRAVVIVE ALL'ORA D'INGRESSO. E' un max-of-k mai dichiarato, misurato
|
||||||
qui per la prima volta su questa struttura: su 9 ancore lo Sharpe canonico BTC 32.58
|
qui per la prima volta su questa struttura: su 9 ancore lo Sharpe canonico BTC 29.61
|
||||||
sta all'89 percentile, la MEDIANA ONESTA e' 1.45 e la banda tocca il NEGATIVO (-0.35).
|
sta all'89 percentile, la MEDIANA ONESTA e' 2.12 e la banda tocca il NEGATIVO (-0.19).
|
||||||
Su ETH 3.90 -> 2.41. Il numero da citare per BTC e' 1.45, non 32.58.
|
Su ETH 7.64 -> 3.35. Il numero da citare per BTC e' 2.12, non 29.61.
|
||||||
⚠️ E il meccanismo non e' quello che avevo scritto prima di guardarlo. Non e' solo la
|
⚠️ E il meccanismo non e' quello che avevo scritto prima di guardarlo. Non e' solo la
|
||||||
vol campionaria a ballare: fra le ancore la MEDIA settimanale BTC va da -1.35% a
|
vol campionaria a ballare: fra le ancore la MEDIA settimanale BTC va da -0.66% a
|
||||||
+11.22% del rischio — CAMBIA SEGNO — e la deviazione standard di 11x (ETH: media 2.9x,
|
+11.44% del rischio — CAMBIA SEGNO — e la deviazione standard di 9.2x (ETH: media 2.1x,
|
||||||
sd 2.4x). Spostare l'ora d'ingresso cambia lo snapshot e quindi quali strike combaciano
|
sd 3.2x). Spostare l'ora d'ingresso cambia lo snapshot e quindi quali strike combaciano
|
||||||
col delta bersaglio: non e' lo stesso trade un'ora dopo, e' UN'ALTRA STRUTTURA. A 10
|
col delta bersaglio: non e' lo stesso trade un'ora dopo, e' UN'ALTRA STRUTTURA. A 10
|
||||||
settimane questo parametro di disturbo domina il segnale che si vorrebbe misurare.
|
settimane questo parametro di disturbo domina il segnale che si vorrebbe misurare.
|
||||||
|
|
||||||
@@ -793,7 +801,7 @@ def main() -> None:
|
|||||||
4. LO SPREAD E' IL COSTO DOMINANTE, E SULLA FAMIGLIA ESEGUIBILE E' PEGGIO. Attraversare
|
4. LO SPREAD E' IL COSTO DOMINANTE, E SULLA FAMIGLIA ESEGUIBILE E' PEGGIO. Attraversare
|
||||||
il bid/ask costa ~10% del credito sugli INVERSE; nella regione di strike che serve lo
|
il bid/ask costa ~10% del credito sugli INVERSE; nella regione di strike che serve lo
|
||||||
spread relativo mediano e' 24-37% sugli inverse e 29-59% sulle USDC, con open interest
|
spread relativo mediano e' 24-37% sugli inverse e 29-59% sulle USDC, con open interest
|
||||||
~0. Il f del credito netto si replica indipendentemente a 0.714 (IC95 [0.690, 0.779],
|
~0. Il f del credito netto si replica indipendentemente a 0.712 (IC95 [0.664, 0.732],
|
||||||
0/19 osservazioni >= 1.0) — conferma dello 0.73 del 30/07 su un campione piu' lungo e
|
0/19 osservazioni >= 1.0) — conferma dello 0.73 del 30/07 su un campione piu' lungo e
|
||||||
con un percorso diverso.
|
con un percorso diverso.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
- 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);
|
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
|
- 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 # attesa + log + simulazione
|
||||||
uv run python scripts/research/r0823_sl_anchor.py --venue # + riscontro sul conto (2 letture)
|
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
|
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`.""")
|
domanda non ne ha bisogno. Si usa quindi cio' che il percorso sanzionato offre: `get_open_orders`.""")
|
||||||
m = datetime.now(timezone.utc).minute
|
m = datetime.now(timezone.utc).minute
|
||||||
if 5 <= m <= 10 or 24 <= m <= 30:
|
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 (:07) / cron_chain (:25). Non interrogo.")
|
print(f"\n ⏸ minuto :{m:02d} — finestra di cron_book (:47) / cron_chain (:25). Non interrogo.")
|
||||||
return
|
return
|
||||||
from src.live.deribit import DeribitRead
|
from src.live.deribit import DeribitRead
|
||||||
r = 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
|
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,
|
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.
|
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
|
· ⚠️ Aggancio operativo (RIPARATO il 2026-09-02, `tests/test_book_cadenza.py`): fino a quel
|
||||||
«ogni ~230 minuti» (la griglia di SKH01) mentre il cron gira **ogni ora** (misurato sopra: 1443
|
giorno il docstring di `scripts/live/book_execute.py` prescriveva «ogni ~230 minuti» (la
|
||||||
giri in 1442 ore). Chi "correggesse" la cadenza verso il docstring sposterebbe il libro dalla
|
griglia di SKH01) mentre il cron gira **ogni ora** (misurato sopra: 1443 giri in 1442 ore).
|
||||||
riga 1h alla riga 4h — quella in cui BTC raddoppia gli scatti.
|
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
|
(*) ⚠️ 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
|
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
|
⚠️ 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,
|
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`
|
e dal 2026-09-02 `tests/test_book_cadenza.py` tiene d'accordo docstring, cron_book.sh e crontab
|
||||||
ne prescrive una (~230 min) che cade nella riga peggiore. Non e' un'azione richiesta qui: e' il
|
(il docstring ne prescriveva una, ~230 min, nella riga 4h). Resta il parametro da rileggere prima
|
||||||
parametro da rileggere prima del gradino, insieme a G2.""")
|
del gradino, insieme a G2.""")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -167,7 +167,8 @@ def calibra_catena() -> dict:
|
|||||||
skew[nm] = (np.array(xs), np.array(ys))
|
skew[nm] = (np.array(xs), np.array(ys))
|
||||||
|
|
||||||
# struttura a termine: IV ATM a DTE t contro il DVOL 30g dello stesso istante
|
# struttura a termine: IV ATM a DTE t contro il DVOL 30g dello stesso istante
|
||||||
dv = cblib.dvol_series("BTC")
|
dv = cblib.causale(cblib.dvol_series("BTC"), "-1D") # join per GIORNO: il DVOL del giorno D — la serie di
|
||||||
|
# cblib e' causale dal 09/09 sera (la chiusura di D sta a D+1); senza questo il join accosterebbe l'IV di D al DVOL di D−1
|
||||||
a = q[q["ad"].between(0.45, 0.55)].copy()
|
a = q[q["ad"].between(0.45, 0.55)].copy()
|
||||||
a["giorno"] = a["ts"].dt.floor("D")
|
a["giorno"] = a["ts"].dt.floor("D")
|
||||||
dvd = dv.copy()
|
dvd = dv.copy()
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""r0906_usde_tetto_sonda.py — sonda il TETTO del venue sull'USDE a SALDO NEUTRO, passi da 1-2 USDE.
|
||||||
|
INVIA ORDINI VERI (spot USDE_USDC, fee 0, ~$0,001 di spread per ordine).
|
||||||
|
|
||||||
|
Metodo (diari 30-31/08, sezione «la lezione di metodo»): un probe unico di taglia sbagliata produce un
|
||||||
|
falso negativo che si legge come risultato. Qui si sale a passi piccoli fino al primo rifiuto, si
|
||||||
|
ripete il rifiuto dopo aver riletto il book (per escludere l'ask che si e' mosso: il messaggio del
|
||||||
|
venue e' lo stesso `not_enough_funds_in_currency`), poi si CONFERMA a saldo neutro:
|
||||||
|
SELL 1 -> BUY 1 (deve passare) -> BUY 1 (deve essere rifiutato). Il bracket e' [L, L+1) USDE, con
|
||||||
|
l'equity totale letta nello stesso istante, cosi' la frazione ha la sua banda.
|
||||||
|
|
||||||
|
Guardie: prezzo dal BOOK (ask+5 tick, tetto 1,0010 / bid-5 tick, pavimento 0,9990), banda
|
||||||
|
[0,995, 1,005]; al massimo MAX_ADD USDE in piu' (quota mai oltre QUOTA_STOP); pausa fra ordini.
|
||||||
|
|
||||||
|
uv run python scripts/research/r0906_usde_tetto_sonda.py # dry-run: stato e piano
|
||||||
|
uv run python scripts/research/r0906_usde_tetto_sonda.py --esegui # sonda davvero
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "live"))
|
||||||
|
|
||||||
|
from src.live import usde as U # noqa: E402
|
||||||
|
from src.live.deribit import DeribitRead # noqa: E402
|
||||||
|
import usde_convert as C # noqa: E402 (book_top, leggi_stato: stessi attrezzi)
|
||||||
|
|
||||||
|
LOG = ROOT / "data" / "live" / "usde_convert.jsonl"
|
||||||
|
PX_MIN, PX_MAX = C.PX_MIN, C.PX_MAX
|
||||||
|
TICK, MARGINE, CAP_BUY, CAP_SELL = 0.0001, 5, 1.0010, 0.9990
|
||||||
|
MAX_ADD = 60 # USDE in piu' al massimo (--max-add N): oltre, «tetto non trovato entro il limite»
|
||||||
|
QUOTA_HARD = 0.70 # mai oltre: e' la quota decisa dall'operatore il 30/08 (`usde.quota_target`), e coincide
|
||||||
|
# col CUSCINO DI REGOLAMENTO derivato da config (usde_convert.cuscino_richiesto_usd),
|
||||||
|
# che qui e' la guardia che morde davvero: si legge a ogni passo, non si ridichiara.
|
||||||
|
SCALA = [100.0, 20.0, 4.0, 1.0] # passi: si scende di uno a ogni doppio rifiuto (M13: risoluzione dell'effetto)
|
||||||
|
PAUSA, PAUSA_RIFIUTO = 12.0, 20.0
|
||||||
|
|
||||||
|
|
||||||
|
def stato(c: DeribitRead) -> dict:
|
||||||
|
eq_usdc, eq_usde, px, fonte = C.leggi_stato(c)
|
||||||
|
val, _ = U.valuta(eq_usde, px)
|
||||||
|
return {"usdc": eq_usdc, "usde": eq_usde, "px": px, "fonte": fonte, "val": val, "tot": eq_usdc + val,
|
||||||
|
"quota": val / (eq_usdc + val) if (eq_usdc + val) > 0 else None}
|
||||||
|
|
||||||
|
|
||||||
|
def prezzo_ordine(side: str) -> float | None:
|
||||||
|
bid, ask = C.book_top()
|
||||||
|
if side == "buy":
|
||||||
|
return None if ask is None else round(min(ask + MARGINE * TICK, CAP_BUY), 4)
|
||||||
|
return None if bid is None else round(max(bid - MARGINE * TICK, CAP_SELL), 4)
|
||||||
|
|
||||||
|
|
||||||
|
def ordine(c: DeribitRead, side: str, amt: float, px: float) -> tuple[bool, str]:
|
||||||
|
resp = c._unwrap(c._post("/mcp-deribit/tools/place_order",
|
||||||
|
{"instrument_name": U.SPOT, "side": side, "amount": float(amt),
|
||||||
|
"type": "limit", "price": px, "label": "usde-sonda-0906"})) or {}
|
||||||
|
if not isinstance(resp, dict) or resp.get("error") or "order" not in resp:
|
||||||
|
err = resp.get("error", resp) if isinstance(resp, dict) else resp
|
||||||
|
return False, str(err)[:120]
|
||||||
|
o = resp["order"]
|
||||||
|
return True, f"{o.get('order_state')} filled {o.get('filled_amount')}"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
esegui = "--esegui" in sys.argv[1:]
|
||||||
|
max_add = float(sys.argv[sys.argv.index("--max-add") + 1]) if "--max-add" in sys.argv else MAX_ADD
|
||||||
|
c = DeribitRead()
|
||||||
|
s0 = stato(c)
|
||||||
|
if s0["px"] is None:
|
||||||
|
print(" ✗ prezzo USDE non leggibile — non si sonda al buio (P5)"); return 2
|
||||||
|
print("=" * 78)
|
||||||
|
print(f" SONDA TETTO USDE — {datetime.now(timezone.utc):%Y-%m-%dT%H:%M:%SZ} "
|
||||||
|
f"{'ESECUZIONE' if esegui else 'DRY-RUN'}")
|
||||||
|
print("=" * 78)
|
||||||
|
cap = U.cfg().get("venue_cap_frac")
|
||||||
|
print(f" conto : USDC ${s0['usdc']:,.2f} + USDE {s0['usde']:,.4f} (${s0['val']:,.2f}) = ${s0['tot']:,.2f}"
|
||||||
|
f" quota {s0['quota']:.2%} · tetto in config {cap:.1%} = {cap * s0['tot']:,.1f} USDE")
|
||||||
|
cusc, come = C.cuscino_richiesto_usd(s0["tot"])
|
||||||
|
print(f" piano : BUY a passi {'→'.join(f'{x:.0f}' for x in SCALA)} fino al rifiuto (max +{max_add:.0f} USDE;"
|
||||||
|
f" quota ≤{QUOTA_HARD:.0%} e USDC ≥ cuscino ${cusc:,.2f} = {come}), conferma SELL 1 → BUY 1 ok → BUY 1 rifiutato")
|
||||||
|
if not esegui:
|
||||||
|
print(" → dry-run. Rilancia con --esegui."); return 0
|
||||||
|
|
||||||
|
eventi: list[dict] = []
|
||||||
|
|
||||||
|
def fai(side: str, amt: float) -> bool:
|
||||||
|
px = prezzo_ordine(side)
|
||||||
|
if px is None or not (PX_MIN <= px <= PX_MAX):
|
||||||
|
print(f" ✗ prezzo d'ordine {px} non leggibile/fuori banda — mi fermo"); raise SystemExit(3)
|
||||||
|
ok, nota = ordine(c, side, amt, px)
|
||||||
|
time.sleep(3.0)
|
||||||
|
s = stato(c)
|
||||||
|
ev = {"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "side": side, "amount": amt,
|
||||||
|
"px": px, "ok": ok, "nota": nota, "usde": s["usde"], "tot": s["tot"], "quota": s["quota"]}
|
||||||
|
eventi.append(ev)
|
||||||
|
print(f" · {side.upper():4} {amt:>3.0f} @ {px:.4f} -> {'OK ' if ok else 'RIFIUTATO'} "
|
||||||
|
f"USDE {s['usde']:,.4f} eq ${s['tot']:,.2f} quota {s['quota']:.3%} [{nota}]")
|
||||||
|
return ok
|
||||||
|
|
||||||
|
# --- salita a passi piccoli -------------------------------------------------------------
|
||||||
|
i_step, aggiunti, muro, limite = 0, 0.0, None, None
|
||||||
|
while True:
|
||||||
|
step = SCALA[i_step]
|
||||||
|
s = stato(c)
|
||||||
|
cusc, _ = C.cuscino_richiesto_usd(s["tot"])
|
||||||
|
if aggiunti + step > max_add:
|
||||||
|
limite = f"+{aggiunti:.0f} USDE raggiunti (max-add {max_add:.0f})"; break
|
||||||
|
if (s["val"] + step * s["px"]) / s["tot"] > QUOTA_HARD:
|
||||||
|
limite = f"quota {s['quota']:.2%} + passo supererebbe {QUOTA_HARD:.0%}"; break
|
||||||
|
if s["usdc"] - step * s["px"] < cusc:
|
||||||
|
limite = f"USDC ${s['usdc']:,.2f} − passo scenderebbe sotto il cuscino ${cusc:,.2f}"; break
|
||||||
|
if fai("buy", step):
|
||||||
|
aggiunti += step; time.sleep(PAUSA); continue
|
||||||
|
time.sleep(PAUSA_RIFIUTO) # rifiuto: rileggo il book e riprovo UGUALE
|
||||||
|
if fai("buy", step):
|
||||||
|
aggiunti += step; time.sleep(PAUSA); continue
|
||||||
|
if i_step < len(SCALA) - 1:
|
||||||
|
i_step += 1; time.sleep(PAUSA); continue # doppio rifiuto: passo piu' fine
|
||||||
|
muro = stato(c); break
|
||||||
|
if limite:
|
||||||
|
s = stato(c)
|
||||||
|
print(f" ✗ limite della sonda: {limite} — tetto del venue NON trovato entro il limite;"
|
||||||
|
f" quota {s['quota']:.2%}, USDE {s['usde']:,.4f}")
|
||||||
|
|
||||||
|
if muro is None:
|
||||||
|
s = stato(c)
|
||||||
|
esito = {"verdetto": "NON_TROVATO", "limite": limite, "usde": s["usde"], "equity_tot": s["tot"],
|
||||||
|
"quota": s["quota"], "aggiunti": aggiunti, "eventi": eventi}
|
||||||
|
else:
|
||||||
|
# --- conferma a saldo neutro ---------------------------------------------------------
|
||||||
|
time.sleep(PAUSA)
|
||||||
|
ok_sell = fai("sell", 1); time.sleep(PAUSA)
|
||||||
|
ok_buy1 = fai("buy", 1) if ok_sell else False; time.sleep(PAUSA)
|
||||||
|
ok_buy2 = fai("buy", 1) if ok_buy1 else None; time.sleep(3.0)
|
||||||
|
fin = stato(c)
|
||||||
|
confermato = bool(ok_sell and ok_buy1 and ok_buy2 is False)
|
||||||
|
L = fin["usde"]
|
||||||
|
lo, hi = L / fin["tot"], (L + 1) / fin["tot"]
|
||||||
|
esito = {"verdetto": "CONFERMATO" if confermato else "NON_CONFERMATO",
|
||||||
|
"bracket_usde": [L, L + 1], "equity_tot": fin["tot"], "frazione": [lo, hi],
|
||||||
|
"aggiunti": aggiunti, "eventi": eventi}
|
||||||
|
print()
|
||||||
|
print(f" TETTO : [{L:,.4f} · {L + 1:,.4f}) USDE con equity ${fin['tot']:,.2f}"
|
||||||
|
f" ⇒ frazione {lo:.2%} – {hi:.2%} ({esito['verdetto']}: "
|
||||||
|
f"sell1 {ok_sell} / buy1 {ok_buy1} / buy1 {ok_buy2})")
|
||||||
|
print(f" config : venue_cap_frac {cap:.3f} → bordo basso misurato oggi {lo:.4f}"
|
||||||
|
+ (" (config sotto il misurato: conservativa, ok)" if cap <= lo + 1e-9 else " ⚠️ config SOPRA il misurato"))
|
||||||
|
print(f" conto : USDC ${fin['usdc']:,.2f} + USDE {fin['usde']:,.4f} = ${fin['tot']:,.2f} quota {fin['quota']:.2%}")
|
||||||
|
with LOG.open("a") as f:
|
||||||
|
f.write(json.dumps({"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||||
|
"sonda_tetto": esito}) + "\n")
|
||||||
|
print(f" registrato : {LOG.relative_to(ROOT)}")
|
||||||
|
return 0 if esito["verdetto"] == "CONFERMATO" else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
"""r0909 — IL CROLLO CATTURATO: il `f` di stress delle opzioni, misurato su quote VERE.
|
||||||
|
|
||||||
|
PERCHE' ESISTE. La decisione vincolante del 19/06 ("niente short-vol da modello in deploy", e la
|
||||||
|
gemella "niente long-vol scalp") dichiara UNA condizione che la riapre: *un `f` di stress reale
|
||||||
|
misurato su un crash catturato*. COLLAR01 (§71) e PAVIMENTO-LEVA (§72) chiudono con la stessa
|
||||||
|
condizione. Il 22/08 (§11) il campione con quote vere era "la strategia mentre STA FERMA":
|
||||||
|
19 settimane di rally, "l'unico episodio ad alta vol del campione e' un rally, non un crash".
|
||||||
|
|
||||||
|
FATTO NUOVO (2026-09-09): l'archivio ereditato `bite_archive.parquet` copre con bid E ask
|
||||||
|
il crollo del 1-5 giugno 2026 — BTC −25% e ETH −30% in 5 giorni (peggior giorno ETH −10,6%
|
||||||
|
il 05/06), su UNA scadenza (19 giugno, 14-21 DTE agli ingressi). Il campione del 22/08
|
||||||
|
partiva DOPO, dal fondo. Nessuno lo aveva usato: e' il crash catturato che la regola aspettava.
|
||||||
|
|
||||||
|
COSA MISURA (a ogni ora d'ingresso dal 30/05 al 04/06, BTC e ETH — l'ora e' un'ancora, si
|
||||||
|
riportano mediana e banda, mai il massimo):
|
||||||
|
A. la geometria di VRP01 (vendi put δ−0,28, compra put δ−0,10, stessa scadenza) a quote REALI:
|
||||||
|
credito al bid/ask, mark-to-market ora per ora dalle standing quotes, regolamento a scadenza
|
||||||
|
sullo spot certificato. Contro lo STESSO trade prezzato dal modello del sleeve (BS su DVOL,
|
||||||
|
`sleeves._bs_put`). f_stress = perdita reale / perdita modellata, a scadenza e al peggior MTM.
|
||||||
|
B. lo specchio LONG (compra la put δ−0,10 all'ask; e il debit spread −0,28/−0,10): quanto del
|
||||||
|
payoff modellato si incassa davvero al bid — a scadenza (uscita passiva) e al miglior punto
|
||||||
|
del percorso (limite superiore con look-ahead, dichiarato).
|
||||||
|
C. il contesto: DVOL, RV realizzata, IV-rank causale (il gate di VRP01 era aperto o chiuso?),
|
||||||
|
e cosa dice il modello del sleeve (`_vrp_weekly_asset`) per le settimane di giugno.
|
||||||
|
|
||||||
|
LIMITI DICHIARATI PRIMA DEI NUMERI: n = 1 crollo, 1 scadenza per asset, tenor 14-21 giorni
|
||||||
|
(VRP01 e' a 7), 2-5 mila righe/giorno (40-125 strumenti/ora), `underlying_price` assente
|
||||||
|
nell'archivio (spot dal feed 1h certificato, D1), regolamento approssimato allo spot dell'ora
|
||||||
|
di scadenza (Deribit usa una TWAP di 30 min). La famiglia dell'archivio e' INVERSE (premi in
|
||||||
|
BTC/ETH; `ETH-19JUN26-1900-P`): VRP01 opererebbe sulla USDC-lineare, che il conto puo' marginare
|
||||||
|
e che NON e' nell'archivio (§11) — il f qui misurato e' della famiglia che il conto non negozia.
|
||||||
|
Un episodio e' un episodio: da' la TAGLIA di f nello stress, non la sua distribuzione.
|
||||||
|
|
||||||
|
DUE CORREZIONI DELLA REVISIONE (fable, 09/09), applicate:
|
||||||
|
* il feed 1h e' etichettato all'APERTURA (la barra 20:00 chiude alle 21:00): `S.asof(ts)`
|
||||||
|
guardava un'ora AVANTI. Corretto la mattina del 09/09 qui con uno `spot_causale` locale;
|
||||||
|
la sera dello stesso giorno la riparazione e' entrata in `cblib` (`causale`, debito §5.18) e
|
||||||
|
copre anche il DVOL giornaliero (fino a 24 ore avanti): questo script usa le serie di `cblib`
|
||||||
|
COME SONO, senza ri-spostarle.
|
||||||
|
* il costo di chiusura `(ask corta − bid lunga)·spot` puo' superare la LARGHEZZA dello spread
|
||||||
|
(una quota da 158% di spread relativo a T→0): uno spread a rischio definito non costa mai piu'
|
||||||
|
della larghezza per chiudere (si lascia regolare). Il MTM e' tagliato alla larghezza e le ore
|
||||||
|
tagliate sono contate. Senza il taglio il "peggior MTM" era un artefatto di quote (2,6×).
|
||||||
|
* `f_exp` e' TAUTOLOGICO quando entrambe le gambe finiscono ITM (payoff = larghezza per reale
|
||||||
|
e modello): si riduce a una funzione di f_net. Si stampa la quota di trade in cui succede.
|
||||||
|
|
||||||
|
Nessun file di produzione toccato. Nessun ordine.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||||
|
|
||||||
|
import cblib as CB # noqa: E402
|
||||||
|
from src.portfolio.sleeves import VRP_CFG, _vrp_weekly_asset # noqa: E402
|
||||||
|
|
||||||
|
ARCHIVIO = ROOT / "data" / "raw" / "cb_chain" / "bite_archive.parquet"
|
||||||
|
ASSETS = ("BTC", "ETH")
|
||||||
|
EXP = pd.Timestamp("2026-06-19 08:00", tz="UTC")
|
||||||
|
INGRESSI = (pd.Timestamp("2026-05-30", tz="UTC"), pd.Timestamp("2026-06-05", tz="UTC")) # [da, a)
|
||||||
|
CROLLO = (pd.Timestamp("2026-06-01", tz="UTC"), pd.Timestamp("2026-06-06", tz="UTC"))
|
||||||
|
PRE = pd.Timestamp("2026-06-02", tz="UTC") # ingressi "prima del crollo" = prima del primo giorno a −7%/−8% (il 01/06 BTC fece −3,1%: e' DENTRO "pre", dichiarato)
|
||||||
|
TOL_D = (0.08, 0.06) # geometria VRP01: |δ corta + 0,28| ≤ 0,08 e |δ lunga + 0,10| ≤ 0,06, altrimenti "fuori geometria"
|
||||||
|
SHORT_D, LONG_D = VRP_CFG["short_delta"], VRP_CFG["long_delta"] # P1: dal sleeve, non ridichiarati
|
||||||
|
FEE_FRAC = VRP_CFG["fee_frac"]
|
||||||
|
# listino Deribit opzioni (stesse costanti di r0822_vrp_real_quotes, verificate al venue il 22/08)
|
||||||
|
FEE_UNDERLYING, FEE_PREMIUM_CAP, FEE_DELIVERY = 0.0003, 0.125, 0.00015
|
||||||
|
|
||||||
|
|
||||||
|
def leg_fee(prem_base: float, spot: float) -> float:
|
||||||
|
return min(FEE_UNDERLYING * spot, FEE_PREMIUM_CAP * prem_base * spot)
|
||||||
|
|
||||||
|
|
||||||
|
def carica() -> pd.DataFrame:
|
||||||
|
a = pd.read_parquet(ARCHIVIO)
|
||||||
|
a["ts"] = pd.to_datetime(a["ts"], utc=True)
|
||||||
|
a["exp"] = pd.to_datetime(a["exp"], utc=True)
|
||||||
|
a = a[(a["option_type"] == "P") & (a["exp"] == EXP) & (a["ts"] >= INGRESSI[0]) & (a["ts"] <= EXP)]
|
||||||
|
a = a.drop_duplicates(subset=["ts", "instrument_name"], keep="last")
|
||||||
|
a["dte"] = (a["exp"] - a["ts"]).dt.total_seconds() / 86400.0
|
||||||
|
return a.reset_index(drop=True)
|
||||||
|
|
||||||
|
|
||||||
|
def bs_spread(S: float, Ks: float, Kl: float, T: float, sig: float) -> float:
|
||||||
|
return CB.bs_put(S, Ks, T, sig) - CB.bs_put(S, Kl, T, sig)
|
||||||
|
|
||||||
|
|
||||||
|
def bs_put_vec(S, K, T, sig):
|
||||||
|
"""`cblib.bs_put` vettorizzata (stessa formula, r=0); T≤0 → intrinseco."""
|
||||||
|
from scipy.stats import norm
|
||||||
|
S = np.asarray(S, float); T = np.asarray(T, float); sig = np.asarray(sig, float)
|
||||||
|
out = np.maximum(K - S, 0.0)
|
||||||
|
ok = (T > 0) & (sig > 0)
|
||||||
|
d1 = (np.log(S[ok] / K) + 0.5 * sig[ok] ** 2 * T[ok]) / (sig[ok] * np.sqrt(T[ok]))
|
||||||
|
out[ok] = K * norm.cdf(-(d1 - sig[ok] * np.sqrt(T[ok]))) - S[ok] * norm.cdf(-d1)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
class Quote:
|
||||||
|
"""Tabella ora × strumento (bid, ask, iv) costruita UNA volta per asset, con spot e DVOL per ora."""
|
||||||
|
|
||||||
|
def __init__(self, seg: pd.DataFrame, S: pd.Series, V: pd.Series):
|
||||||
|
q = seg.dropna(subset=["bid", "ask"]); q = q[(q["bid"] > 0) & (q["ask"] > 0)]
|
||||||
|
self.bid = q.pivot_table(index="ts", columns="instrument_name", values="bid")
|
||||||
|
self.ask = q.pivot_table(index="ts", columns="instrument_name", values="ask")
|
||||||
|
self.iv = q.pivot_table(index="ts", columns="instrument_name", values="iv")
|
||||||
|
idx = self.bid.index
|
||||||
|
self.spot = pd.Series([float(S.asof(t)) for t in idx], index=idx)
|
||||||
|
self.dvol = pd.Series([float(V.asof(t)) for t in idx], index=idx) / 100.0
|
||||||
|
self.T = pd.Series(np.maximum((EXP - idx).total_seconds().values / 86400.0, 0.0) / 365.25, index=idx)
|
||||||
|
|
||||||
|
|
||||||
|
def percorso(Q: Quote, dopo: pd.Timestamp, inst_s: str, inst_l: str, Ks: float, Kl: float) -> pd.DataFrame:
|
||||||
|
"""Ora per ora dopo l'ingresso: costo REALE di chiudere lo short spread (ricompra la corta
|
||||||
|
all'ask, rivende la lunga al bid), valore REALE di liquidare il debit spread (vende la corta
|
||||||
|
al bid, ricompra la lunga all'ask) e la put lunga (bid); piu' il valore MODELLATO (BS a DVOL_t)."""
|
||||||
|
if inst_s not in Q.bid.columns or inst_l not in Q.bid.columns:
|
||||||
|
return pd.DataFrame()
|
||||||
|
m = (Q.bid.index > dopo) & Q.bid[inst_s].notna() & Q.bid[inst_l].notna()
|
||||||
|
if not m.any():
|
||||||
|
return pd.DataFrame()
|
||||||
|
idx = Q.bid.index[m]
|
||||||
|
sp = Q.spot.loc[idx].values; dv = Q.dvol.loc[idx].values; T = Q.T.loc[idx].values
|
||||||
|
bs_, as_ = Q.bid.loc[idx, inst_s].values, Q.ask.loc[idx, inst_s].values
|
||||||
|
bl, al = Q.bid.loc[idx, inst_l].values, Q.ask.loc[idx, inst_l].values
|
||||||
|
mod_s, mod_l = bs_put_vec(sp, Ks, T, dv), bs_put_vec(sp, Kl, T, dv)
|
||||||
|
grezzo = (as_ - bl) * sp
|
||||||
|
return pd.DataFrame(dict(ts=idx, spot=sp, dvol=dv,
|
||||||
|
chiudi_short=np.minimum(grezzo, Ks - Kl), chiudi_grezzo=grezzo,
|
||||||
|
liquida_long=np.clip((bs_ - al) * sp, 0.0, Ks - Kl), liquida_put=bl * sp,
|
||||||
|
mod_spread=mod_s - mod_l, mod_put=mod_l,
|
||||||
|
iv_s=Q.iv.loc[idx, inst_s].values, iv_l=Q.iv.loc[idx, inst_l].values))
|
||||||
|
|
||||||
|
|
||||||
|
def trade(asset: str, ts: pd.Timestamp, snap: pd.DataFrame, Q: Quote,
|
||||||
|
S: pd.Series, V: pd.Series) -> dict | None:
|
||||||
|
legs = CB.pick_legs(snap, SHORT_D, LONG_D)
|
||||||
|
if legs is None:
|
||||||
|
return None
|
||||||
|
spot = float(S.asof(ts)); dvol = float(V.asof(ts)); T = (EXP - ts).total_seconds() / 86400.0 / 365.25
|
||||||
|
hist = V[V.index < ts]; ivr = float((hist < dvol).mean()) if len(hist) else np.nan
|
||||||
|
Ks, Kl = legs["k_short"], legs["k_long"]
|
||||||
|
# --- A. short spread (VRP01) — reale al bid/ask, modello a DVOL
|
||||||
|
cred = (legs["bid_short"] - legs["ask_long"]) * spot
|
||||||
|
if cred <= 0:
|
||||||
|
return None
|
||||||
|
cred_mod = bs_spread(spot, Ks, Kl, T, dvol / 100.0)
|
||||||
|
ST = float(S.asof(EXP))
|
||||||
|
payoff = max(Ks - ST, 0.0) - max(Kl - ST, 0.0)
|
||||||
|
fee_in = leg_fee(legs["bid_short"], spot) + leg_fee(legs["ask_long"], spot)
|
||||||
|
fee_out = sum(min(FEE_DELIVERY * ST, FEE_PREMIUM_CAP * max(K - ST, 0.0)) for K in (Ks, Kl) if K > ST)
|
||||||
|
pnl_real = cred - payoff - fee_in - fee_out
|
||||||
|
pnl_mod = cred_mod - payoff - FEE_FRAC * cred_mod
|
||||||
|
p = percorso(Q, ts, legs["inst_short"], legs["inst_long"], Ks, Kl)
|
||||||
|
if p.empty:
|
||||||
|
return None
|
||||||
|
mtm_real = cred - p["chiudi_short"] # P&L aperto se si chiude ORA a quote vere (tagliato alla larghezza)
|
||||||
|
mtm_mod = cred_mod - p["mod_spread"] # lo stesso trade riprezzato a BS/DVOL_t (costruzione nostra: il sleeve tiene a scadenza)
|
||||||
|
i_min = int(mtm_real.idxmin())
|
||||||
|
n_tagli = int((p["chiudi_grezzo"] > Ks - Kl).sum())
|
||||||
|
# --- B. specchio long: put δ−0,10 comprata all'ask; debit spread −0,28/−0,10
|
||||||
|
costo_put = legs["ask_long"] * spot
|
||||||
|
mod_put = CB.bs_put(spot, Kl, T, dvol / 100.0)
|
||||||
|
costo_deb = (legs["ask_short"] - legs["bid_long"]) * spot
|
||||||
|
mod_deb = cred_mod
|
||||||
|
pay_put_exp = max(Kl - ST, 0.0)
|
||||||
|
pay_deb_exp = payoff
|
||||||
|
# il picco del percorso e' un limite SUPERIORE con look-ahead: si dichiara, non si vende
|
||||||
|
i_put = int(p["liquida_put"].idxmax()); i_deb = int(p["liquida_long"].idxmax())
|
||||||
|
risk = (Ks - Kl) - cred
|
||||||
|
in_geom = abs(legs["d_short"] - SHORT_D) <= TOL_D[0] and abs(legs["d_long"] - LONG_D) <= TOL_D[1]
|
||||||
|
return dict(
|
||||||
|
in_geom=bool(in_geom),
|
||||||
|
asset=asset, ts=ts, dte=(EXP - ts).total_seconds() / 86400.0, spot=spot, dvol=dvol, ivrank=ivr,
|
||||||
|
k_short=Ks, k_long=Kl, d_short=legs["d_short"], d_long=legs["d_long"], width=Ks - Kl,
|
||||||
|
iv_s0=legs["iv_short"], iv_l0=legs["iv_long"], ST=ST, und_ret=ST / spot - 1.0,
|
||||||
|
cred_real=cred, cred_mod=cred_mod, f_net=cred / cred_mod if cred_mod > 0 else np.nan,
|
||||||
|
risk=risk, pnl_real=pnl_real, pnl_mod=pnl_mod,
|
||||||
|
perdita_su_risk=-pnl_real / risk, perdita_su_coll=-pnl_real / Ks,
|
||||||
|
f_exp=(-pnl_real / -pnl_mod) if pnl_mod < 0 and pnl_real < 0 else np.nan,
|
||||||
|
mtm_real_min=float(mtm_real.min()), mtm_mod_min=float(mtm_mod.min()),
|
||||||
|
mtm_min_ts=p["ts"].iloc[i_min], n_tagli=n_tagli, n_ore=int(len(p)),
|
||||||
|
iv_s_at_min=float(p["iv_s"].iloc[i_min]), iv_l_at_min=float(p["iv_l"].iloc[i_min]), dvol_at_min=float(p["dvol"].iloc[i_min]) * 100,
|
||||||
|
itm_pieno=bool(ST <= Kl), # entrambe le gambe ITM: payoff = larghezza, f_exp si riduce a f_net
|
||||||
|
f_mtm=(mtm_real.min() / mtm_mod.min()) if mtm_mod.min() < 0 and mtm_real.min() < 0 else np.nan,
|
||||||
|
spot_min=float(p["spot"].min()), iv_s_max=float(p["iv_s"].max()), iv_l_max=float(p["iv_l"].max()),
|
||||||
|
n_ore_percorso=int(len(p)),
|
||||||
|
# long
|
||||||
|
costo_put=costo_put, mod_put=mod_put, f_put_in=costo_put / mod_put if mod_put > 0 else np.nan,
|
||||||
|
put_exp=pay_put_exp - costo_put, put_exp_mod=pay_put_exp - mod_put,
|
||||||
|
put_best=float(p["liquida_put"].iloc[i_put]) - costo_put, put_best_mod=float(p["mod_put"].iloc[i_put]) - mod_put,
|
||||||
|
put_best_ts=p["ts"].iloc[i_put], bleed_put=costo_put / spot,
|
||||||
|
costo_deb=costo_deb, mod_deb=mod_deb,
|
||||||
|
deb_exp=pay_deb_exp - costo_deb, deb_exp_mod=pay_deb_exp - mod_deb,
|
||||||
|
deb_best=float(p["liquida_long"].iloc[i_deb]) - costo_deb, deb_best_mod=float(p["mod_spread"].iloc[i_deb]) - mod_deb,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def misura() -> dict:
|
||||||
|
a = carica()
|
||||||
|
out = {"trades": {}, "contesto": {}, "sleeve": {}}
|
||||||
|
for asset in ASSETS:
|
||||||
|
seg = a[a["asset"] == asset]
|
||||||
|
S, V = CB.spot_series(asset), CB.dvol_series(asset) # gia' causali (cblib, dal 09/09 sera)
|
||||||
|
Q = Quote(seg, S, V)
|
||||||
|
rows = []
|
||||||
|
for ts in sorted(seg["ts"].unique()):
|
||||||
|
ts = pd.Timestamp(ts)
|
||||||
|
if not (INGRESSI[0] <= ts < INGRESSI[1]):
|
||||||
|
continue
|
||||||
|
t = trade(asset, ts, seg[seg["ts"] == ts], Q, S, V)
|
||||||
|
if t:
|
||||||
|
rows.append(t)
|
||||||
|
out["trades"][asset] = pd.DataFrame(rows)
|
||||||
|
# contesto: DVOL, RV5 e RV1 sul crollo
|
||||||
|
px = S[(S.index >= pd.Timestamp("2026-05-20", tz="UTC")) & (S.index <= EXP)]
|
||||||
|
pd_ = px.resample("1D").last().dropna() # chiusura del giorno = ultima chiusura nota entro le 24:00
|
||||||
|
# CONTESTO per giorno: la serie di cblib e' causale (la chiusura del giorno D sta a D+1);
|
||||||
|
# qui si vuole "il DVOL DEL giorno D" accanto al prezzo del giorno D, quindi si toglie lo
|
||||||
|
# spostamento SOLO per la tabella e per il picco del crollo. `dvol_pre` e `ivr_pre` restano
|
||||||
|
# sulla serie causale: sono cio' che si sapeva alle 00:00 del primo giorno di crollo.
|
||||||
|
dvg = CB.causale(V, "-1D")
|
||||||
|
hist_all = dvg[dvg.index < CROLLO[0]]
|
||||||
|
lr = np.log(pd_).diff()
|
||||||
|
rv5 = lr.rolling(5).std() * np.sqrt(365.25) * 100
|
||||||
|
dv = dvg[(dvg.index >= pd.Timestamp("2026-05-20", tz="UTC")) & (dvg.index <= EXP)]
|
||||||
|
out["contesto"][asset] = dict(
|
||||||
|
spot=pd_, ret=lr, rv5=rv5, dvol=dv,
|
||||||
|
dvol_max_crollo=float(dv[(dv.index >= CROLLO[0]) & (dv.index <= CROLLO[1])].max()),
|
||||||
|
dvol_pre=float(V.asof(CROLLO[0])),
|
||||||
|
pctl_picco=float((hist_all < dv[(dv.index >= CROLLO[0]) & (dv.index <= CROLLO[1])].max()).mean()),
|
||||||
|
ivr_pre=float((hist_all < V.asof(CROLLO[0])).mean()),
|
||||||
|
rv5_max=float(rv5[(rv5.index >= CROLLO[0]) & (rv5.index <= CROLLO[1] + pd.Timedelta(days=3))].max()),
|
||||||
|
caduta=float(pd_[CROLLO[0]:CROLLO[1]].min() / pd_[:CROLLO[0]].iloc[-1] - 1.0),
|
||||||
|
)
|
||||||
|
wk = _vrp_weekly_asset(asset)
|
||||||
|
out["sleeve"][asset] = wk[(wk.index >= pd.Timestamp("2026-05-10", tz="UTC")) & (wk.index <= pd.Timestamp("2026-07-05", tz="UTC"))]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def sintesi(m: dict) -> dict:
|
||||||
|
s = {}
|
||||||
|
for asset in ASSETS:
|
||||||
|
t = m["trades"][asset]
|
||||||
|
if t.empty:
|
||||||
|
s[asset] = None; continue
|
||||||
|
n_fuori = int((~t["in_geom"]).sum())
|
||||||
|
t = t[t["in_geom"]] # la griglia di strike dell'archivio e' rada: fuori geometria = altra struttura
|
||||||
|
if t.empty:
|
||||||
|
s[asset] = dict(n=0, n_fuori=n_fuori); continue
|
||||||
|
pre = t[t["ts"] < PRE] # ingressi PRIMA del crollo: il caso della regola
|
||||||
|
dur = t[(t["ts"] >= PRE) & (t["ts"] < CROLLO[1] - pd.Timedelta(days=1))] # DURANTE (02-04/06): lente secondaria
|
||||||
|
med = lambda c, d=t: float(np.nanmedian(d[c])) if len(d) else np.nan
|
||||||
|
b = lambda c, d=t: (float(np.nanpercentile(d[c], 10)), float(np.nanpercentile(d[c], 90))) if len(d) else (np.nan, np.nan)
|
||||||
|
s[asset] = dict(
|
||||||
|
n=int(len(t)), n_pre=int(len(pre)), n_fuori=n_fuori,
|
||||||
|
d_short=med("d_short", pre), d_long=med("d_long", pre), width=med("width", pre), ST=float(t["ST"].iloc[0]),
|
||||||
|
spot_pre=med("spot", pre),
|
||||||
|
dte=(float(t["dte"].min()), float(t["dte"].max())),
|
||||||
|
f_net=med("f_net"), f_net_b=b("f_net"),
|
||||||
|
ivrank=(float(t["ivrank"].min()), float(t["ivrank"].max())),
|
||||||
|
gate_aperto=float((t["ivrank"] >= VRP_CFG["gate_ivr"]).mean()),
|
||||||
|
perdita_su_risk=med("perdita_su_risk", pre), perdita_su_risk_b=b("perdita_su_risk", pre),
|
||||||
|
perdita_su_coll=med("perdita_su_coll", pre),
|
||||||
|
pnl_real=med("pnl_real", pre), pnl_mod=med("pnl_mod", pre),
|
||||||
|
f_exp=med("f_exp", pre), f_exp_b=b("f_exp", pre), n_f_exp=int(pre["f_exp"].notna().sum()),
|
||||||
|
f_mtm=med("f_mtm", pre), f_mtm_b=b("f_mtm", pre),
|
||||||
|
mtm_real_min=med("mtm_real_min", pre), mtm_mod_min=med("mtm_mod_min", pre),
|
||||||
|
n_tagli=int(pre["n_tagli"].sum()) if len(pre) else 0, n_ore=int(pre["n_ore"].sum()) if len(pre) else 0,
|
||||||
|
iv_s_at_min=med("iv_s_at_min", pre), dvol_at_min=med("dvol_at_min", pre),
|
||||||
|
q_itm_pieno=float(pre["itm_pieno"].mean()) if len(pre) else np.nan,
|
||||||
|
n_strutture=int(pre.groupby(["k_short", "k_long"]).ngroups) if len(pre) else 0,
|
||||||
|
f_put_in=med("f_put_in", pre), bleed_put=med("bleed_put", pre),
|
||||||
|
put_exp=med("put_exp", pre), put_exp_mod=med("put_exp_mod", pre),
|
||||||
|
put_best=med("put_best", pre), put_best_mod=med("put_best_mod", pre),
|
||||||
|
f_put_exp=med_ratio(pre, "put_exp", "put_exp_mod"), f_put_best=med_ratio(pre, "put_best", "put_best_mod"),
|
||||||
|
deb_exp=med("deb_exp", pre), deb_exp_mod=med("deb_exp_mod", pre),
|
||||||
|
deb_best=med("deb_best", pre), deb_best_mod=med("deb_best_mod", pre),
|
||||||
|
f_deb_exp=med_ratio(pre, "deb_exp", "deb_exp_mod"), f_deb_best=med_ratio(pre, "deb_best", "deb_best_mod"),
|
||||||
|
iv_s0=med("iv_s0", pre), iv_s_max=med("iv_s_max", pre), iv_l0=med("iv_l0", pre), iv_l_max=med("iv_l_max", pre),
|
||||||
|
und_ret=med("und_ret", pre), spot_min=med("spot_min", pre),
|
||||||
|
durante=dict(n=int(len(dur)), f_exp=med("f_exp", dur), f_mtm=med("f_mtm", dur),
|
||||||
|
perdita_su_risk=med("perdita_su_risk", dur), f_net=med("f_net", dur),
|
||||||
|
q_perde=float((dur["pnl_real"] < 0).mean()) if len(dur) else np.nan,
|
||||||
|
pnl_p10=float(np.nanpercentile(dur["pnl_real"], 10)) if len(dur) else np.nan,
|
||||||
|
pnl_p90=float(np.nanpercentile(dur["pnl_real"], 90)) if len(dur) else np.nan,
|
||||||
|
d_short=med("d_short", dur), d_long=med("d_long", dur), width=med("width", dur),
|
||||||
|
pnl_real=med("pnl_real", dur), pnl_mod=med("pnl_mod", dur)),
|
||||||
|
)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def med_ratio(d: pd.DataFrame, a: str, b: str, minimo: float = 5.0) -> float:
|
||||||
|
"""Mediana del rapporto reale/modello sulle righe in cui il modello vale almeno `minimo` $
|
||||||
|
(un payoff modellato ≈ 0 non da' un rapporto, da' un'altra domanda: si stampano i dollari)."""
|
||||||
|
x = d[(d[b] >= minimo)]
|
||||||
|
return float(np.nanmedian(x[a] / x[b])) if len(x) else np.nan
|
||||||
|
|
||||||
|
|
||||||
|
def verdetto(s: dict) -> str:
|
||||||
|
parti = []
|
||||||
|
for asset in ASSETS:
|
||||||
|
r = s.get(asset)
|
||||||
|
if not r or r.get("n", 0) == 0:
|
||||||
|
parti.append(f"{asset}: NON misurabile — {r.get('n_fuori', 0) if r else 0} snapshot tutti FUORI geometria (griglia di strike rada)"); continue
|
||||||
|
if not np.isfinite(r["f_exp"]):
|
||||||
|
parti.append(f"{asset}: nessun ingresso pre-crollo in geometria (solo DURANTE: f_net {r['durante']['f_net']:.2f}, perde il {r['durante']['q_perde']*100:.0f}%)"); continue
|
||||||
|
parti.append(f"{asset}: f_net {r['f_net']:.2f} (22/08 in rally 0,714); a scadenza f {r['f_exp']:.2f} ma TAUTOLOGICO nel "
|
||||||
|
f"{r['q_itm_pieno']*100:.0f}% dei trade (entrambe le gambe ITM ⇒ dipende solo da f_net; {r['n_strutture']} strutture, non {r['n_pre']} osservazioni); "
|
||||||
|
f"peggior MTM tagliato alla larghezza f {r['f_mtm']:.2f} (quote oltre la larghezza nel {r['n_tagli']/max(1, r['n_ore'])*100:.1f}% delle ore); "
|
||||||
|
f"gate IV-rank {'APERTO' if r['gate_aperto'] > 0.5 else 'CHIUSO'} nel {r['gate_aperto']*100:.0f}% delle ore")
|
||||||
|
esito = ("IL CROLLO E' CATTURATO MA A VOL BASSA E FUORI DAL GATE DI VRP01: il f di stress che la regola del 19/06 aspettava "
|
||||||
|
"NON e' misurato nella sostanza — cio' che e' misurato e' che il rapporto di credito f_net non cambia col regime")
|
||||||
|
return esito + " — " + "; ".join(parti) + " — n = 1 crollo, 1 scadenza per asset, famiglia INVERSE"
|
||||||
|
|
||||||
|
|
||||||
|
def pct(x): return f"{x*100:+.1f}%"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("=" * 104)
|
||||||
|
print(" r0909 — IL CROLLO CATTURATO (1-5 giugno 2026): f di stress su quote VERE, scadenza 19/06")
|
||||||
|
print("=" * 104)
|
||||||
|
m = misura(); s = sintesi(m)
|
||||||
|
for asset in ASSETS:
|
||||||
|
c = m["contesto"][asset]; r = s[asset]
|
||||||
|
print(f"\n [{asset}] CONTESTO — caduta {pct(c['caduta'])} dal 31/05 al minimo di 1-5/06; "
|
||||||
|
f"DVOL pre {c['dvol_pre']:.1f} (IV-rank causale {c['ivr_pre']:.2f}) → max {c['dvol_max_crollo']:.1f} "
|
||||||
|
f"(= {c['pctl_picco']*100:.0f}° percentile storico: il PICCO del crollo sta sotto la mediana); RV5 max {c['rv5_max']:.0f}")
|
||||||
|
print(" giorno spot ret DVOL RV5")
|
||||||
|
for d in pd.date_range("2026-05-28", "2026-06-08", tz="UTC"):
|
||||||
|
if d in c["spot"].index:
|
||||||
|
dv = c["dvol"].asof(d)
|
||||||
|
print(f" {d.date()} {c['spot'][d]:>8.0f} {pct(c['ret'].get(d, np.nan)):>7} {dv:>5.1f} {c['rv5'].get(d, np.nan):>5.0f}")
|
||||||
|
if r is None or r.get("n", 0) == 0:
|
||||||
|
print(f" nessun trade in geometria VRP01: {r.get('n_fuori', 0) if r else 0} snapshot fuori geometria "
|
||||||
|
f"(la griglia di strike dell'archivio in quei giorni non ha le δ−0,28/−0,10)"); continue
|
||||||
|
t = m["trades"][asset]; t = t[t["in_geom"]]
|
||||||
|
print(f"\n [{asset}] A. VRP01 a quote vere — {r['n']} snapshot d'ingresso in geometria ({r['n_pre']} prima del {PRE.date()}; "
|
||||||
|
f"{r['n_fuori']} scartati fuori geometria), DTE {r['dte'][0]:.0f}-{r['dte'][1]:.0f}, IV-rank {r['ivrank'][0]:.2f}-{r['ivrank'][1]:.2f}")
|
||||||
|
print(f" geometria mediana pre-crollo: δ corta {r['d_short']:+.2f} / δ lunga {r['d_long']:+.2f}, larghezza {r['width']:.0f}, "
|
||||||
|
f"spot d'ingresso {r['spot_pre']:.0f}, regolamento {EXP.date()} a {r['ST']:.0f}")
|
||||||
|
print(f" credito reale/modello f_net {r['f_net']:.2f} [{r['f_net_b'][0]:.2f}, {r['f_net_b'][1]:.2f}] "
|
||||||
|
f"(22/08: 0,714 in rally)")
|
||||||
|
print(f" ingressi PRE-crollo, mediana: P&L reale ${r['pnl_real']:+.0f} vs modello ${r['pnl_mod']:+.0f} per unita' "
|
||||||
|
f"→ perdita {r['perdita_su_risk']*100:.0f}% del rischio [{r['perdita_su_risk_b'][0]*100:.0f}, {r['perdita_su_risk_b'][1]*100:.0f}] "
|
||||||
|
f"(100% = perdita MASSIMA dello spread; oltre 100 sono le fee), "
|
||||||
|
f"{r['perdita_su_coll']*100:.1f}% del collaterale")
|
||||||
|
print(f" f a SCADENZA {r['f_exp']:.2f} [{r['f_exp_b'][0]:.2f}, {r['f_exp_b'][1]:.2f}] su {r['n_f_exp']} snapshot = {r['n_strutture']} strutture "
|
||||||
|
f"— TAUTOLOGICO nel {r['q_itm_pieno']*100:.0f}% (entrambe le gambe ITM: payoff = larghezza per reale e modello, resta solo f_net)")
|
||||||
|
print(f" peggior MTM (chiusura tagliata alla larghezza; quote oltre la larghezza nel {r['n_tagli']/max(1, r['n_ore'])*100:.1f}% delle ore-percorso): "
|
||||||
|
f"f {r['f_mtm']:.2f} [{r['f_mtm_b'][0]:.2f}, {r['f_mtm_b'][1]:.2f}] (reale ${r['mtm_real_min']:+.0f} / modello ${r['mtm_mod_min']:+.0f}); "
|
||||||
|
f"a quell'ora IV corta {r['iv_s_at_min']:.0f} vs DVOL {r['dvol_at_min']:.0f}")
|
||||||
|
print(f" IV gamba corta {r['iv_s0']:.0f} → max {r['iv_s_max']:.0f} (il max e' a T→0, non nel crollo); gamba lunga {r['iv_l0']:.0f} → max {r['iv_l_max']:.0f}; "
|
||||||
|
f"sottostante a scadenza {pct(r['und_ret'])}, minimo {r['spot_min']:.0f}")
|
||||||
|
du = r["durante"]
|
||||||
|
print(f" lente secondaria — ingressi DURANTE il crollo (02-04/06, {du['n']} snapshot): δ {du['d_short']:+.2f}/{du['d_long']:+.2f}, "
|
||||||
|
f"larghezza {du['width']:.0f}, f_net {du['f_net']:.2f}, P&L reale ${du['pnl_real']:+.0f} vs modello ${du['pnl_mod']:+.0f}, "
|
||||||
|
f"perde nel {du['q_perde']*100:.0f}% (p10 ${du['pnl_p10']:+.0f} / p90 ${du['pnl_p90']:+.0f}: bimodale, la mediana nasconde le perdite), "
|
||||||
|
f"f scadenza {du['f_exp']:.2f} / peggior MTM {du['f_mtm']:.2f}")
|
||||||
|
print(f"\n [{asset}] B. LO SPECCHIO LONG (ingressi PRE-crollo, mediana per unita')")
|
||||||
|
print(f" put δ−0,10 all'ask: costo reale/modello {r['f_put_in']:.2f}, bleed {r['bleed_put']*100:.2f}% dello spot")
|
||||||
|
print(f" a scadenza: reale ${r['put_exp']:+.0f} vs modello ${r['put_exp_mod']:+.0f} → f_payoff {r['f_put_exp']:.2f}")
|
||||||
|
print(f" al PICCO del percorso (look-ahead, limite sup.): reale ${r['put_best']:+.0f} vs modello ${r['put_best_mod']:+.0f} → f {r['f_put_best']:.2f}")
|
||||||
|
print(f" debit spread −0,28/−0,10: a scadenza reale ${r['deb_exp']:+.0f} vs modello ${r['deb_exp_mod']:+.0f} → f {r['f_deb_exp']:.2f}; "
|
||||||
|
f"al picco ${r['deb_best']:+.0f} vs ${r['deb_best_mod']:+.0f} → f {r['f_deb_best']:.2f}")
|
||||||
|
print(f"\n [{asset}] C. IL MODELLO DEL SLEEVE (`_vrp_weekly_asset`, 0.0 = settimana saltata dai gate) — rendimento settimanale su collaterale:")
|
||||||
|
print(" " + " ".join(f"{d.date()} {v*100:+.2f}%" for d, v in m["sleeve"][asset].items()))
|
||||||
|
print(f"\n snapshot d'ingresso (primi 6): " + " | ".join(
|
||||||
|
f"{row.ts.strftime('%m-%d %H')}h K{row.k_short:.0f}/{row.k_long:.0f} cred ${row.cred_real:.0f} pnl ${row.pnl_real:+.0f} mtm ${row.mtm_real_min:+.0f}"
|
||||||
|
for row in t.head(6).itertuples()))
|
||||||
|
print("\n VERDETTO: " + verdetto(s))
|
||||||
|
print("=" * 104)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
"""r0909 — L'UNIVERSO DERIBIT LETTO OGGI: quali "altre monete" esistono davvero sul venue.
|
||||||
|
|
||||||
|
Domanda dell'operatore: "anche piu' monete, ma sempre in Deribit". Prima di misurare una
|
||||||
|
moneta si legge il venue (N10: la verifica esterna a €0 si fa PRIMA, e sul venue). Questo
|
||||||
|
script interroga l'API PUBBLICA Deribit (nessuna credenziale, nessun ordine) e stampa:
|
||||||
|
1. i perpetual USDC-lineari aperti, con data di listing, lotto minimo in $, volume 24h,
|
||||||
|
spread top-of-book, open interest e funding — ordinati per volume;
|
||||||
|
2. quante delle 19 gambe di XS01 sono quotate (il 23/08 erano 14/19, 11 con ≥1 anno);
|
||||||
|
3. le famiglie di opzioni (inverse BTC/ETH; USDC-lineari per sottostante).
|
||||||
|
Una lettura e' UN istante (come r0822_alt_options): il file salva il JSON grezzo in
|
||||||
|
`data/_cache/` con l'ora della lettura, cosi' il numero stampato ha la sua fonte.
|
||||||
|
|
||||||
|
Cosa NON fa: nessun backtest, nessuna proposta. Dice solo quali strumenti esistono e quanto
|
||||||
|
sono negoziabili oggi — il filtro che decide cosa vale la pena misurare (C6).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
from collections import Counter
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402 (P1: l'universo si importa)
|
||||||
|
|
||||||
|
API = "https://www.deribit.com/api/v2/public/"
|
||||||
|
CACHE = ROOT / "data" / "_cache" / "r0909_deribit_universo.json"
|
||||||
|
SOGLIA_LIQUIDO_USD = 10e6 # volume 24h da cui una gamba e' "liquida" per un libro da $4-5k
|
||||||
|
|
||||||
|
|
||||||
|
def get(path: str) -> dict:
|
||||||
|
req = urllib.request.Request(API + path, headers={"User-Agent": "PythagorasGoal-research"})
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as r:
|
||||||
|
return json.load(r)["result"]
|
||||||
|
|
||||||
|
|
||||||
|
def leggi(usa_cache: bool = True) -> dict:
|
||||||
|
if usa_cache and CACHE.exists():
|
||||||
|
return json.loads(CACHE.read_text())
|
||||||
|
d = dict(letto_a=datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
||||||
|
fut=get("get_instruments?currency=USDC&kind=future"),
|
||||||
|
bs=get("get_book_summary_by_currency?currency=USDC&kind=future"),
|
||||||
|
opt_usdc=get("get_instruments?currency=USDC&kind=option"),
|
||||||
|
opt_inv={c: len(get(f"get_instruments?currency={c}&kind=option")) for c in ("BTC", "ETH")})
|
||||||
|
CACHE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
CACHE.write_text(json.dumps(d))
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def tabella(d: dict) -> list[dict]:
|
||||||
|
perps = {i["base_currency"]: i for i in d["fut"] if i["instrument_name"].endswith("PERPETUAL") and i["state"] == "open"}
|
||||||
|
bs = {b["instrument_name"]: b for b in d["bs"]}
|
||||||
|
rows = []
|
||||||
|
for c, i in perps.items():
|
||||||
|
b = bs.get(i["instrument_name"], {})
|
||||||
|
bid, ask, mark = b.get("bid_price"), b.get("ask_price"), b.get("mark_price") or 0.0
|
||||||
|
rows.append(dict(
|
||||||
|
coin=c, listato=time.strftime("%Y-%m-%d", time.gmtime(i["creation_timestamp"] / 1000)),
|
||||||
|
min_amt=i["min_trade_amount"], min_usd=i["min_trade_amount"] * mark,
|
||||||
|
vol_usd=b.get("volume_usd") or 0.0,
|
||||||
|
spread_bps=((ask - bid) / ((ask + bid) / 2) * 1e4) if bid and ask else float("nan"),
|
||||||
|
oi_usd=(b.get("open_interest") or 0.0) * mark, funding_8h=b.get("funding_8h"),
|
||||||
|
xs01=c in XS_UNIVERSE))
|
||||||
|
return sorted(rows, key=lambda r: -r["vol_usd"])
|
||||||
|
|
||||||
|
|
||||||
|
def sintesi(rows: list[dict], d: dict) -> dict:
|
||||||
|
listati = {r["coin"] for r in rows}
|
||||||
|
liquidi = [r["coin"] for r in rows if r["vol_usd"] >= SOGLIA_LIQUIDO_USD]
|
||||||
|
bs = {b["instrument_name"]: b for b in d["bs"]}
|
||||||
|
inattivi = sorted(i["instrument_name"] for i in d["fut"] if i["instrument_name"].endswith("PERPETUAL") and i["state"] != "open")
|
||||||
|
datati = [i for i in d["fut"] if not i["instrument_name"].endswith("PERPETUAL")]
|
||||||
|
dvol_fut = []
|
||||||
|
for i in datati:
|
||||||
|
if "DVOL" not in i["instrument_name"]:
|
||||||
|
continue
|
||||||
|
b = bs.get(i["instrument_name"], {})
|
||||||
|
dvol_fut.append(dict(nome=i["instrument_name"], stato=i["state"], min_amt=i["min_trade_amount"],
|
||||||
|
vol_usd=b.get("volume_usd") or 0.0, oi=b.get("open_interest") or 0.0,
|
||||||
|
bid=b.get("bid_price"), ask=b.get("ask_price")))
|
||||||
|
return dict(
|
||||||
|
n_perp=len(rows), liquidi=liquidi, inattivi=inattivi, n_datati=len(datati), dvol_fut=dvol_fut,
|
||||||
|
xs01_listate=sorted(c for c in XS_UNIVERSE if c in listati),
|
||||||
|
xs01_mancanti=[c for c in XS_UNIVERSE if c not in listati],
|
||||||
|
opt_inv=d["opt_inv"], opt_usdc=Counter(i["base_currency"] for i in d["opt_usdc"]).most_common(),
|
||||||
|
letto_a=d["letto_a"])
|
||||||
|
|
||||||
|
|
||||||
|
def verdetto(s: dict) -> str:
|
||||||
|
return (f"{s['n_perp']} perpetual USDC; LIQUIDI (≥${SOGLIA_LIQUIDO_USD/1e6:.0f}M/g): {', '.join(s['liquidi'])}; "
|
||||||
|
f"XS01 {len(s['xs01_listate'])}/19 quotate, mancano {', '.join(s['xs01_mancanti'])}; "
|
||||||
|
f"opzioni USDC-lineari su {len(s['opt_usdc'])} sottostanti — lettura del {s['letto_a']}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
d = leggi(usa_cache="--fresh" not in sys.argv)
|
||||||
|
rows = tabella(d); s = sintesi(rows, d)
|
||||||
|
print("=" * 110)
|
||||||
|
print(f" r0909 — UNIVERSO DERIBIT (API pubblica, lettura {s['letto_a']}; --fresh per rileggere)")
|
||||||
|
print("=" * 110)
|
||||||
|
print(f"\n [1] PERPETUAL USDC-LINEARI APERTI: {s['n_perp']}")
|
||||||
|
print(f" {'coin':<9}{'listato':>11}{'min $':>8}{'vol 24h M$':>12}{'spread bps':>12}{'OI M$':>8}{'funding 8h':>12} XS01")
|
||||||
|
for r in rows:
|
||||||
|
print(f" {r['coin']:<9}{r['listato']:>11}{r['min_usd']:>8.2f}{r['vol_usd']/1e6:>12.2f}{r['spread_bps']:>12.1f}"
|
||||||
|
f"{r['oi_usd']/1e6:>8.2f}{(r['funding_8h'] or 0)*1e4:>+11.2f}bp {'<--' if r['xs01'] else ''}")
|
||||||
|
print(f" perpetual NON aperti (stato ≠ open): {s['inattivi'] or 'nessuno'}")
|
||||||
|
print(f"\n [1bis] FUTURE USDC DATATI: {s['n_datati']} — fra cui il future sul DVOL, l'unico strumento long-vol DIRETTO del venue "
|
||||||
|
f"(visto dalla revisione del 09/09: non in memoria):")
|
||||||
|
for f in s["dvol_fut"]:
|
||||||
|
spr = (f["ask"] - f["bid"]) / ((f["ask"] + f["bid"]) / 2) * 100 if f["bid"] and f["ask"] else float("nan")
|
||||||
|
print(f" {f['nome']:<28} {f['stato']:<8} min {f['min_amt']} vol24h ${f['vol_usd']/1e6:.2f}M OI {f['oi']:.0f} "
|
||||||
|
f"bid/ask {f['bid']}/{f['ask']} (spread {spr:.0f}%) → non negoziabile a questi numeri; e in contango sanguina come la put")
|
||||||
|
print(f"\n [2] XS01: {len(s['xs01_listate'])}/19 gambe quotate; mancano {s['xs01_mancanti']}")
|
||||||
|
print(f" (23/08, §50: 14/19 quotate, 11 con ≥1 anno; sulle 11 il meccanismo collassa 1,265 → 0,116 — breadth, non capitale)")
|
||||||
|
print(f"\n [3] OPZIONI: inverse {s['opt_inv']}; USDC-lineari per sottostante: {s['opt_usdc']}")
|
||||||
|
print("\n VERDETTO: " + verdetto(s))
|
||||||
|
print("=" * 110)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
"""r0909 — IL LIBRO NEI CROLLI: revisione dei sistemi adottati, misurata invece che ricordata.
|
||||||
|
|
||||||
|
DOMANDA DELL'OPERATORE (2026-09-09): "trova una soluzione per guadagnare anche nei crolli".
|
||||||
|
Prima di cercare un meccanismo nuovo, questo script misura cosa fanno OGGI i due sleeve live
|
||||||
|
(TP01 long-flat 75% + SKH01 long/short 25%) nei crolli — giorno per giorno e per episodio —
|
||||||
|
e da dove viene, se c'e', il guadagno. La memoria dice tre cose che qui vengono RIPRODOTTE
|
||||||
|
(M23: far riprodurre alla macchina il numero vecchio prima di pubblicarne uno nuovo):
|
||||||
|
(a) beta del libro al sottostante +0,076 (§46) → "non si assicura un libro gia' piatto";
|
||||||
|
(b) nel sinistro TP01 va flat e SKH01 si gira short e guadagna (§26);
|
||||||
|
(c) il peggior giorno del libro: §33 lo trova in uno short squeeze di SKH01 (−15,77% di sleeve,
|
||||||
|
2020-03-13) sulla lente HOURLY — su questa lente (canonica, sleeve giornalieri) NON si
|
||||||
|
riproduce: §33 stesso nota che la canonica da' il 2025-10-10. Qui si stampa e si dichiara.
|
||||||
|
|
||||||
|
LENTE, dichiarata: sleeve di ricerca `sleeves._tp01_returns` / `_skyhook_returns` (ancora
|
||||||
|
canonica 00:00 UTC, fee 0,10% RT, funding NON modellato, come ogni numero del progetto).
|
||||||
|
⚠️ SKH01 e' MARCATO ALL'USCITA del trade (`harness.backtest_signals` muove l'equity alla
|
||||||
|
chiusura): la serie giornaliera e' zero nell'87,7% dei giorni, e uno short aperto in una finestra
|
||||||
|
e chiuso fuori e' accreditato FUORI. E' il motivo per cui la tabella per GIORNO mostra SKH01 ≈ 0
|
||||||
|
e quella per FINESTRA mostra il guadagno. NON e' la serie del libro live (armato dal 2026-06-20): i crolli del 2026 (5 febbraio
|
||||||
|
−14%/−15%, 1-5 giugno −25%/−30%) sono PRIMA dell'arming, quindi qui sono backtest, non fill.
|
||||||
|
|
||||||
|
DEFINIZIONI, scritte prima di guardare i numeri:
|
||||||
|
- indice = 50/50 BTC/ETH, ribilanciato ogni giorno (la stessa geometria del libro);
|
||||||
|
- giorno di crollo = indice ≤ −5% (severo: ≤ −10%);
|
||||||
|
- episodio = le 12 peggiori finestre di 20 giorni NON sovrapposte dell'indice, piu' un elenco
|
||||||
|
NOMINATO di episodi noti (COVID, maggio 2021, LUNA, FTX, agosto 2024, febbraio e giugno 2026);
|
||||||
|
- classi ESCLUSIVE, in quest'ordine: "immune" = |libro| < 1/10 dell'indice (col segno stampato);
|
||||||
|
altrimenti "POSITIVO" se libro > 0, "PERDE" se < 0. Si stampa anche il conteggio nudo libro > 0.
|
||||||
|
(Prima stesura: POSITIVO provato prima di immune → 8/12; con l'ordine dichiarato qui 4/12 + 6
|
||||||
|
immune. L'ordine delle regole ERA il verdetto: corretto in revisione, si stampano entrambi.)
|
||||||
|
- beta al ribasso = pendenza OLS del libro sull'indice nei soli giorni con indice < 0.
|
||||||
|
|
||||||
|
PESI: 75/25 e' decisione vincolante (confermata 3 volte, §3). Il 37,5/62,5 di §26 compare qui
|
||||||
|
SOLO come riga informativa negli stessi episodi — non e' una proposta: la riapre solo
|
||||||
|
`weights_tilt_null`, che finora fallisce.
|
||||||
|
|
||||||
|
Nessun file di produzione toccato. Nessun ordine.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from src.backtest.harness import backtest_signals # noqa: E402
|
||||||
|
from src.data.downloader import load_data # noqa: E402
|
||||||
|
from src.live.book import W_SKH, W_TP01 # noqa: E402 (i pesi si IMPORTANO, non si ridichiarano: P1)
|
||||||
|
from src.portfolio.sleeves import _skyhook_returns, _tp01_returns # noqa: E402
|
||||||
|
from src.strategies.skyhook import SKH01_V2_DD, build_frames, skyhook_entries # noqa: E402
|
||||||
|
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d # noqa: E402
|
||||||
|
|
||||||
|
ASSETS = ("BTC", "ETH")
|
||||||
|
SOGLIA_CROLLO = -0.05
|
||||||
|
SOGLIA_SEVERO = -0.10
|
||||||
|
FINESTRA = 20
|
||||||
|
N_EPISODI = 12
|
||||||
|
BETA_MEMORIA = 0.076 # §46, da riprodurre SULLA SUA FINESTRA (DVOL: 2021-03-24+)
|
||||||
|
FINESTRA_46 = pd.Timestamp("2021-03-24", tz="UTC")
|
||||||
|
TOL_BETA = 0.01
|
||||||
|
W_INFO = (0.375, 0.625) # §26: riga informativa, NON proposta
|
||||||
|
|
||||||
|
EPISODI_NOMINATI = {
|
||||||
|
"COVID 2020-03": ("2020-03-01", "2020-03-20"),
|
||||||
|
"maggio 2021": ("2021-05-08", "2021-05-24"),
|
||||||
|
"LUNA 2022-05": ("2022-05-04", "2022-05-14"),
|
||||||
|
"giugno 2022": ("2022-06-06", "2022-06-19"),
|
||||||
|
"FTX 2022-11": ("2022-11-06", "2022-11-22"),
|
||||||
|
"agosto 2024": ("2024-07-28", "2024-08-06"),
|
||||||
|
"febbraio 2025": ("2025-01-31", "2025-03-11"),
|
||||||
|
"5 febbraio 2026": ("2026-01-28", "2026-02-07"),
|
||||||
|
"1-5 giugno 2026": ("2026-05-28", "2026-06-06"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ serie
|
||||||
|
|
||||||
|
def indice_5050() -> pd.Series:
|
||||||
|
r = {}
|
||||||
|
for a in ASSETS:
|
||||||
|
d = resample_1d(load_data(a, "1h"))
|
||||||
|
s = pd.Series(d["close"].values.astype(float), index=pd.to_datetime(d["datetime"], utc=True))
|
||||||
|
r[a] = s.pct_change()
|
||||||
|
J = pd.concat(r, axis=1, join="inner").dropna()
|
||||||
|
return 0.5 * J["BTC"] + 0.5 * J["ETH"]
|
||||||
|
|
||||||
|
|
||||||
|
def tp01_esposizione() -> pd.Series:
|
||||||
|
"""Esposizione lorda media (BTC+ETH)/2 tenuta nel giorno (decisa a close[i-1])."""
|
||||||
|
tp = TrendPortfolio(**CANONICAL)
|
||||||
|
e = {}
|
||||||
|
for a in ASSETS:
|
||||||
|
d = resample_1d(load_data(a, "1h"))
|
||||||
|
tgt = tp.target_series(d)
|
||||||
|
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
|
||||||
|
e[a] = pd.Series(held, index=pd.to_datetime(d["datetime"], utc=True))
|
||||||
|
J = pd.concat(e, axis=1, join="inner")
|
||||||
|
return 0.5 * J["BTC"] + 0.5 * J["ETH"]
|
||||||
|
|
||||||
|
|
||||||
|
def skh_per_direzione() -> dict[str, pd.Series]:
|
||||||
|
"""SKH01-V2-DD 50/50 su griglia giornaliera, in tre versioni: intera, SOLO long, SOLO short.
|
||||||
|
|
||||||
|
Le versioni per direzione filtrano gli entry-dict prima del backtest: stesso motore, stessi
|
||||||
|
stop. In principio filtrare potrebbe cambiare il non-overlap; MISURATO (revisione 09/09):
|
||||||
|
long + short == intera su 2735/2735 giorni (max|diff| 3,5e-18) — la scomposizione e' ESATTA,
|
||||||
|
e `misura()` lo verifica con un assert (P1: la versione "tutto" e' confrontata bit-exact con
|
||||||
|
`sleeves._skyhook_returns`).
|
||||||
|
"""
|
||||||
|
out = {k: {} for k in ("tutto", "long", "short")}
|
||||||
|
for a in ASSETS:
|
||||||
|
ltf, htf = build_frames(load_data(a, "5m"))
|
||||||
|
ent = skyhook_entries(ltf, htf, SKH01_V2_DD)
|
||||||
|
for k, keep in (("tutto", (1, -1)), ("long", (1,)), ("short", (-1,))):
|
||||||
|
e2 = [x if (x is not None and x["dir"] in keep) else None for x in ent]
|
||||||
|
m = backtest_signals(ltf, e2, fee_rt=0.001, leverage=1.0, asset=a, tf="230m")
|
||||||
|
s = pd.Series(m.equity, index=pd.DatetimeIndex(pd.to_datetime(m.eq_index, utc=True)))
|
||||||
|
out[k][a] = s.resample("1D").last().ffill().pct_change().dropna()
|
||||||
|
res = {}
|
||||||
|
for k in out:
|
||||||
|
J = pd.concat(out[k], axis=1, join="inner").fillna(0.0)
|
||||||
|
res[k] = 0.5 * J["BTC"] + 0.5 * J["ETH"]
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ misure
|
||||||
|
|
||||||
|
def sh(s: pd.Series) -> float:
|
||||||
|
s = s.dropna()
|
||||||
|
return float(s.mean() / s.std() * np.sqrt(365.0)) if len(s) > 5 and s.std() > 0 else float("nan")
|
||||||
|
|
||||||
|
|
||||||
|
def maxdd(s: pd.Series) -> float:
|
||||||
|
eq = (1 + s.fillna(0)).cumprod(); return float((eq / eq.cummax() - 1).min())
|
||||||
|
|
||||||
|
|
||||||
|
def cum(s: pd.Series) -> float:
|
||||||
|
return float((1 + s.fillna(0)).prod() - 1)
|
||||||
|
|
||||||
|
|
||||||
|
def finestre_peggiori(idx: pd.Series, n: int = N_EPISODI, w: int = FINESTRA) -> list[tuple]:
|
||||||
|
"""Le n peggiori finestre di w giorni NON sovrapposte dell'indice (greedy dal peggiore)."""
|
||||||
|
roll = (1 + idx).rolling(w).apply(np.prod, raw=True) - 1
|
||||||
|
roll = roll.dropna().sort_values()
|
||||||
|
prese, occupati = [], []
|
||||||
|
for end, v in roll.items():
|
||||||
|
start = end - pd.Timedelta(days=w - 1)
|
||||||
|
if any(not (end < s or start > e) for s, e in occupati):
|
||||||
|
continue
|
||||||
|
prese.append((start, end, float(v))); occupati.append((start, end))
|
||||||
|
if len(prese) >= n:
|
||||||
|
break
|
||||||
|
return sorted(prese)
|
||||||
|
|
||||||
|
|
||||||
|
def classifica(libro: float, indice: float) -> str:
|
||||||
|
"""Classi esclusive nell'ordine dichiarato: immune (col segno) → POSITIVO → PERDE."""
|
||||||
|
if abs(libro) < abs(indice) / 10:
|
||||||
|
return "immune+" if libro > 0 else "immune-"
|
||||||
|
return "POSITIVO" if libro > 0 else "PERDE"
|
||||||
|
|
||||||
|
|
||||||
|
def beta_ribasso(libro: pd.Series, idx: pd.Series, soglia: float = 0.0) -> float:
|
||||||
|
J = pd.concat({"b": libro, "i": idx}, axis=1, join="inner").dropna()
|
||||||
|
J = J[J["i"] < soglia]
|
||||||
|
if len(J) < 20:
|
||||||
|
return float("nan")
|
||||||
|
x, y = J["i"].values, J["b"].values
|
||||||
|
return float(np.cov(x, y)[0, 1] / np.var(x, ddof=1))
|
||||||
|
|
||||||
|
|
||||||
|
def beta_pieno(libro: pd.Series, idx: pd.Series) -> float:
|
||||||
|
J = pd.concat({"b": libro, "i": idx}, axis=1, join="inner").dropna()
|
||||||
|
x, y = J["i"].values, J["b"].values
|
||||||
|
return float(np.cov(x, y)[0, 1] / np.var(x, ddof=1))
|
||||||
|
|
||||||
|
|
||||||
|
def misura() -> dict:
|
||||||
|
idx = indice_5050()
|
||||||
|
tp = _tp01_returns()
|
||||||
|
if tp.index.tz is None:
|
||||||
|
tp.index = tp.index.tz_localize("UTC")
|
||||||
|
skh = skh_per_direzione()
|
||||||
|
esp = tp01_esposizione()
|
||||||
|
J = pd.concat({"idx": idx, "tp": tp, "skh": skh["tutto"], "skl": skh["long"],
|
||||||
|
"sks": skh["short"], "esp": esp}, axis=1, join="inner").dropna()
|
||||||
|
ref = _skyhook_returns()
|
||||||
|
if ref.index.tz is None:
|
||||||
|
ref.index = ref.index.tz_localize("UTC")
|
||||||
|
comune = J.index.intersection(ref.index)
|
||||||
|
assert float((J.loc[comune, "skh"] - ref.loc[comune]).abs().max()) == 0.0, "SKH01 'tutto' non e' bit-exact col sleeve"
|
||||||
|
assert float((J["skl"] + J["sks"] - J["skh"]).abs().max()) < 1e-12, "long + short != intera: la scomposizione non e' esatta"
|
||||||
|
J["libro"] = W_TP01 * J["tp"] + W_SKH * J["skh"]
|
||||||
|
J["info"] = W_INFO[0] * J["tp"] + W_INFO[1] * J["skh"]
|
||||||
|
J["c_tp"] = W_TP01 * J["tp"]
|
||||||
|
J["c_skh"] = W_SKH * J["skh"]
|
||||||
|
J["c_sks"] = W_SKH * J["sks"]
|
||||||
|
|
||||||
|
# giorni di crollo
|
||||||
|
giorni = {}
|
||||||
|
for nome, soglia in (("≤ −5%", SOGLIA_CROLLO), ("≤ −10%", SOGLIA_SEVERO)):
|
||||||
|
g = J[J["idx"] <= soglia]
|
||||||
|
giorni[nome] = dict(n=int(len(g)), idx=float(g["idx"].mean()), libro=float(g["libro"].mean()),
|
||||||
|
tp=float(g["c_tp"].mean()), skh=float(g["c_skh"].mean()),
|
||||||
|
sks=float(g["c_sks"].mean()), esp=float(g["esp"].mean()),
|
||||||
|
libro_pos=float((g["libro"] > 0).mean()),
|
||||||
|
per_anno={int(y): dict(n=int(len(gg)), idx=float(gg["idx"].sum()),
|
||||||
|
libro=float(gg["libro"].sum()),
|
||||||
|
sks=float(gg["c_sks"].sum()))
|
||||||
|
for y, gg in g.groupby(g.index.year)})
|
||||||
|
|
||||||
|
# episodi: finestre peggiori + nominati
|
||||||
|
def riga_ep(nome, s, e):
|
||||||
|
w = J.loc[s:e]
|
||||||
|
if len(w) < 3:
|
||||||
|
return None
|
||||||
|
return dict(nome=nome, da=str(w.index[0].date()), a=str(w.index[-1].date()), n=int(len(w)),
|
||||||
|
idx=cum(w["idx"]), libro=cum(w["libro"]), tp=cum(w["c_tp"]), skh=cum(w["c_skh"]),
|
||||||
|
sks=cum(w["c_sks"]), skl=cum(W_SKH * w["skl"]), esp=float(w["esp"].mean()),
|
||||||
|
info=cum(w["info"]), classe=classifica(cum(w["libro"]), cum(w["idx"])))
|
||||||
|
ep_auto = [r for r in (riga_ep(f"peggiore #{i+1}", s, e)
|
||||||
|
for i, (s, e, _) in enumerate(finestre_peggiori(J["idx"]))) if r]
|
||||||
|
ep_nom = [r for r in (riga_ep(k, pd.Timestamp(a, tz="UTC"), pd.Timestamp(b, tz="UTC"))
|
||||||
|
for k, (a, b) in EPISODI_NOMINATI.items()) if r]
|
||||||
|
|
||||||
|
# peggior giorno del libro (riproduce §33: e' uno squeeze, non un crollo?)
|
||||||
|
peggiori = J.nsmallest(5, "libro")[["idx", "libro", "c_tp", "c_skh"]]
|
||||||
|
|
||||||
|
n_auto = len(ep_auto)
|
||||||
|
guad = sum(r["classe"] == "POSITIVO" for r in ep_auto)
|
||||||
|
imm = sum(r["classe"].startswith("immune") for r in ep_auto)
|
||||||
|
pos = sum(r["libro"] > 0 for r in ep_auto)
|
||||||
|
dal22 = [r for r in ep_auto if r["da"] >= "2022"]
|
||||||
|
J46 = J[J.index >= FINESTRA_46]
|
||||||
|
quota_short = (sum(r["sks"] for r in ep_auto if r["libro"] > 0)
|
||||||
|
/ max(1e-12, sum(r["libro"] for r in ep_auto if r["libro"] > 0)))
|
||||||
|
return dict(
|
||||||
|
finestra=(str(J.index[0].date()), str(J.index[-1].date()), int(len(J))),
|
||||||
|
beta=beta_pieno(J["libro"], J["idx"]), beta_46=beta_pieno(J46["libro"], J46["idx"]),
|
||||||
|
beta_giu=beta_ribasso(J["libro"], J["idx"]),
|
||||||
|
beta_giu3=beta_ribasso(J["libro"], J["idx"], -0.03),
|
||||||
|
sharpe=dict(libro=sh(J["libro"]), info=sh(J["info"]), tp=sh(J["tp"]), skh=sh(J["skh"]),
|
||||||
|
sks=sh(J["sks"]), skl=sh(J["skl"])),
|
||||||
|
maxdd=dict(libro=maxdd(J["libro"]), info=maxdd(J["info"]), idx=maxdd(J["idx"])),
|
||||||
|
giorni=giorni, episodi=ep_auto, nominati=ep_nom,
|
||||||
|
peggiori=[(str(t.date()), float(r.idx), float(r.libro), float(r.c_tp), float(r.c_skh))
|
||||||
|
for t, r in peggiori.iterrows()],
|
||||||
|
sks_anno={int(y): cum(g) for y, g in J["c_sks"].groupby(J.index.year)},
|
||||||
|
sintesi=dict(n_episodi=n_auto, guadagna=guad, immune=imm, perde=n_auto - guad - imm, positivi=pos,
|
||||||
|
dal22=(sum(r["libro"] > 0 for r in dal22), len(dal22)),
|
||||||
|
giorni_pos=giorni["≤ −5%"]["libro_pos"], giorni_libro=giorni["≤ −5%"]["libro"],
|
||||||
|
quota_short_nei_guadagni=float(quota_short),
|
||||||
|
info_batte_live=sum(r["info"] > r["libro"] for r in ep_auto)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def verdetto(m: dict) -> str:
|
||||||
|
"""Tre affermazioni separate, perche' rispondono a tre domande diverse:
|
||||||
|
(1) nel GIORNO del crollo (media sui giorni ≤ −5%); (2) per FINESTRA (classi esclusive);
|
||||||
|
(3) da dove viene il guadagno. Un solo aggettivo ("guadagna"/"immune") sarebbe l'ordine delle regole."""
|
||||||
|
s = m["sintesi"]
|
||||||
|
beta_ok = abs(m["beta_46"] - BETA_MEMORIA) < TOL_BETA
|
||||||
|
giorno = ("PERDE" if s["giorni_libro"] < 0 else "GUADAGNA")
|
||||||
|
if s["guadagna"] >= 2 * s["n_episodi"] / 3:
|
||||||
|
finestra = "POSITIVO per finestra"
|
||||||
|
elif s["guadagna"] + s["immune"] >= 2 * s["n_episodi"] / 3:
|
||||||
|
finestra = "IMMUNE-O-POSITIVO per finestra"
|
||||||
|
else:
|
||||||
|
finestra = "PERDE per finestra"
|
||||||
|
return (f"NEL GIORNO DEL CROLLO IL LIBRO {giorno} ({s['giorni_libro']*100:+.2f}%/g, positivo nel {s['giorni_pos']*100:.0f}% dei giorni ≤ −5%); "
|
||||||
|
f"{finestra}: {s['guadagna']} POSITIVO / {s['immune']} immune / {s['perde']} PERDE su {s['n_episodi']} (libro > 0 in {s['positivi']}, "
|
||||||
|
f"dal 2022 in {s['dal22'][0]}/{s['dal22'][1]}); beta {m['beta_46']:+.4f} sulla finestra di §46 "
|
||||||
|
f"({'riproduce' if beta_ok else 'NON riproduce'} il +{BETA_MEMORIA:.3f}); "
|
||||||
|
f"la gamba short di SKH01 (marcata all'uscita) fa il {s['quota_short_nei_guadagni']*100:.0f}% dei guadagni negli episodi con libro > 0")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ stampa
|
||||||
|
|
||||||
|
def pct(x: float) -> str:
|
||||||
|
return f"{x*100:+.2f}%"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("=" * 100)
|
||||||
|
print(" r0909 — IL LIBRO NEI CROLLI (TP01 75 / SKH01 25, lente di ricerca, ancora canonica)")
|
||||||
|
print("=" * 100)
|
||||||
|
m = misura()
|
||||||
|
a, b, n = m["finestra"]
|
||||||
|
print(f" finestra {a} → {b} ({n} giorni) pesi importati da src/live/book: TP01 {W_TP01} / SKH01 {W_SKH}")
|
||||||
|
|
||||||
|
print("\n [1] BETA DEL LIBRO ALL'INDICE 50/50")
|
||||||
|
print(f" pieno {m['beta']:+.3f} · sulla finestra di §46 (2021-03-24+) {m['beta_46']:+.4f} (memoria: +{BETA_MEMORIA:.3f}) "
|
||||||
|
f"al ribasso (idx<0) {m['beta_giu']:+.3f} nei giorni ≤ −3% {m['beta_giu3']:+.3f}")
|
||||||
|
|
||||||
|
print("\n [2] GIORNI DI CROLLO — media per giorno")
|
||||||
|
print(f" {'soglia':<8}{'n':>5}{'indice':>9}{'libro':>9}{'TP01':>9}{'SKH01':>9}{'SKH short':>11}{'esp TP01':>10}{'libro>0':>9}")
|
||||||
|
for k, g in m["giorni"].items():
|
||||||
|
print(f" {k:<8}{g['n']:>5}{pct(g['idx']):>9}{pct(g['libro']):>9}{pct(g['tp']):>9}"
|
||||||
|
f"{pct(g['skh']):>9}{pct(g['sks']):>11}{g['esp']:>10.3f}{g['libro_pos']*100:>8.0f}%")
|
||||||
|
print("\n per anno (giorni ≤ −5%, SOMMA nel gruppo):")
|
||||||
|
print(f" {'anno':<6}{'n':>4}{'indice':>10}{'libro':>10}{'SKH short':>11}")
|
||||||
|
for y, g in m["giorni"]["≤ −5%"]["per_anno"].items():
|
||||||
|
print(f" {y:<6}{g['n']:>4}{pct(g['idx']):>10}{pct(g['libro']):>10}{pct(g['sks']):>11}")
|
||||||
|
|
||||||
|
def tab(rows, titolo):
|
||||||
|
print(f"\n {titolo}")
|
||||||
|
print(f" {'episodio':<18}{'da':>11}{'a':>11}{'indice':>9}{'libro':>9}{'TP01':>8}{'SKH01':>8}"
|
||||||
|
f"{'SKH-S':>8}{'SKH-L':>8}{'esp':>6}{'37/63':>8} classe")
|
||||||
|
for r in rows:
|
||||||
|
print(f" {r['nome']:<18}{r['da']:>11}{r['a']:>11}{pct(r['idx']):>9}{pct(r['libro']):>9}"
|
||||||
|
f"{pct(r['tp']):>8}{pct(r['skh']):>8}{pct(r['sks']):>8}{pct(r['skl']):>8}{r['esp']:>6.2f}"
|
||||||
|
f"{pct(r['info']):>8} {r['classe']}")
|
||||||
|
tab(m["episodi"], f"[3] LE {N_EPISODI} PEGGIORI FINESTRE DI {FINESTRA} GIORNI DELL'INDICE (non sovrapposte)")
|
||||||
|
tab(m["nominati"], "[4] EPISODI NOMINATI (elenco scritto prima di misurare)")
|
||||||
|
|
||||||
|
print("\n [5] I 5 PEGGIORI GIORNI DEL LIBRO (§33 su lente hourly: squeeze SKH01 2020-03-13; qui lente canonica: NON si riproduce, e' un crollo con TP01 mezzo long)")
|
||||||
|
print(f" {'giorno':<12}{'indice':>9}{'libro':>9}{'TP01':>9}{'SKH01':>9}")
|
||||||
|
for t, i, l, tpv, sk in m["peggiori"]:
|
||||||
|
print(f" {t:<12}{pct(i):>9}{pct(l):>9}{pct(tpv):>9}{pct(sk):>9}")
|
||||||
|
|
||||||
|
print("\n [6] LA GAMBA SHORT DI SKH01 (peso 0,25 gia' applicato) — contributo per anno al libro")
|
||||||
|
print(" " + " ".join(f"{y} {pct(v)}" for y, v in m["sks_anno"].items()))
|
||||||
|
s = m["sharpe"]; d = m["maxdd"]
|
||||||
|
print(f"\n Sharpe: libro {s['libro']:.2f} · TP01 {s['tp']:.2f} · SKH01 {s['skh']:.2f} "
|
||||||
|
f"(solo long {s['skl']:.2f} / solo short {s['sks']:.2f}) maxDD libro {d['libro']*100:.1f}% · indice {d['idx']*100:.1f}%")
|
||||||
|
print(f" riga INFORMATIVA 37,5/62,5 (§26, NON proposta): Sharpe {s['info']:.2f}, maxDD {d['info']*100:.1f}%, "
|
||||||
|
f"batte il 75/25 in {m['sintesi']['info_batte_live']}/{m['sintesi']['n_episodi']} episodi")
|
||||||
|
|
||||||
|
print("\n VERDETTO: " + verdetto(m))
|
||||||
|
print("=" * 100)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
"""r0909 — XRP COME TERZA GAMBA DEL BOOK DERIBIT (meccanismi CONGELATI): la misura che manca.
|
||||||
|
|
||||||
|
PERCHE' ESISTE. L'operatore chiede "piu' monete, ma sempre in Deribit". Letto il venue OGGI
|
||||||
|
(09/09/2026, `public/get_book_summary_by_currency`): dei 32 perpetual USDC-lineari, solo
|
||||||
|
QUATTRO hanno volume a 8 cifre — BTC, ETH, **XRP ($38M/g, il PIU' scambiato del venue, spread
|
||||||
|
4 bps, listato 2022-03-16)** e SOL ($13M/g). Tutto il resto sta fra $0,01M e $7M al giorno.
|
||||||
|
SOL come terza gamba e' stato misurato il 22/08 e ESCLUSO dall'operatore (hold-out −0,166 in
|
||||||
|
0/24 ancore, guadagno di UN anno, il 2023). XRP non e' mai stato misurato come gamba
|
||||||
|
direzionale: e' l'unico candidato "piu' monete" del venue che sia liquido E non misurato.
|
||||||
|
|
||||||
|
IPOTESI A PRIORI, dichiarata prima di misurare: DILUISCE, come SOL — il trend multi-asset del
|
||||||
|
19/06 fu scartato a corr 0,74 con TP01, e SOL ha confermato (corr 0,40-0,56, in salita col dato
|
||||||
|
che migliora). Se XRP aggiunge, il primo sospetto e' l'anno del suo evento idiosincratico (la
|
||||||
|
sentenza SEC dell'estate 2023 e la corsa di fine 2024): M9, "24/24 ancore positive possono
|
||||||
|
essere un anno solo".
|
||||||
|
|
||||||
|
METODO — identico a `r0822_sol_leg.py`, da cui IMPORTA il harness (non riscritto): TP01 e
|
||||||
|
SKH01-V2-DD congelati, book a 2 vs 3 gambe, 24 ancore orarie, mediana delle differenze
|
||||||
|
APPAIATE (M7), null del de-levering a iso-maxDD (M5), scomposizione per anno (M9), due lenti
|
||||||
|
DICHIARATE PRIMA dalla certificazione del dato, non dal risultato:
|
||||||
|
- L-FULL: dal listing (2022-03-16);
|
||||||
|
- L-PULITA: dal primo anno in cui la quota di barre 1h a >1% da Coinbase USD scende sotto
|
||||||
|
lo 0,5% (SOL 2023 aveva 0,6% e fu giudicata sporca: e' il precedente che fissa la soglia).
|
||||||
|
Il dato XRP vive in `data/raw/alt_xrp_*.parquet` (namespace di ricerca, come SOL: fuori dal
|
||||||
|
feed attivo, NON rinfrescato dal cron, `load_data("XRP")` continua a fallire).
|
||||||
|
|
||||||
|
Nessun file di produzione toccato. Nessun ordine. Book/pesi/universo/cron INVARIATI.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||||
|
|
||||||
|
from r0822_sol_leg import ( # noqa: E402 — harness importato, non riscritto
|
||||||
|
DUE, HOLDOUT, W_SKH, W_TP, book, cagr, dati, k_iso_dd, maxdd, sh, skh_leg, taglia, tp01_leg,
|
||||||
|
)
|
||||||
|
|
||||||
|
ASSET = "XRP"
|
||||||
|
TRE = ("BTC", "ETH", ASSET)
|
||||||
|
LISTING = pd.Timestamp("2022-03-16", tz="UTC")
|
||||||
|
SOGLIA_PULITA = 0.005 # quota di barre 1h a >1% da Coinbase: sotto = anno pulito
|
||||||
|
CACHE = ROOT / "data" / "_cache" / "r0909_xrp_riferimento_1h.parquet"
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ certificazione (D1: Coinbase = audit)
|
||||||
|
|
||||||
|
def _fetch_1h(ex, symbol: str, start: pd.Timestamp, limit: int) -> pd.Series:
|
||||||
|
"""OHLCV 1h paginato in avanti; un batch vuoto NON ferma il ciclo (Coinbase ha un buco
|
||||||
|
2021-01 → 2023-07 su XRP: delistato dopo la causa SEC, rilistato dopo la sentenza)."""
|
||||||
|
out, since = [], int(start.timestamp() * 1000)
|
||||||
|
end = int(time.time() * 1000)
|
||||||
|
while since < end:
|
||||||
|
r = ex.fetch_ohlcv(symbol, "1h", since=since, limit=limit)
|
||||||
|
r = [x for x in r if x[0] >= since]
|
||||||
|
if not r:
|
||||||
|
since += limit * 3_600_000
|
||||||
|
continue
|
||||||
|
out.extend(r)
|
||||||
|
nxt = r[-1][0] + 3_600_000
|
||||||
|
since = nxt if nxt > since else since + limit * 3_600_000
|
||||||
|
df = pd.DataFrame(out, columns=["ts", "o", "h", "l", "close", "v"]).drop_duplicates("ts")
|
||||||
|
return pd.Series(df["close"].values.astype(float),
|
||||||
|
index=pd.to_datetime(df["ts"], unit="ms", utc=True)).sort_index()
|
||||||
|
|
||||||
|
|
||||||
|
def riferimento_1h(start: pd.Timestamp) -> pd.DataFrame:
|
||||||
|
"""Riferimento indipendente in USD (D1: audit, mai ancora): Coinbase dove c'e' (dal rilisting
|
||||||
|
2023-07), Bitstamp altrove — entrambi gia' usati dal progetto come venue di audit. Con cache."""
|
||||||
|
if CACHE.exists():
|
||||||
|
df = pd.read_parquet(CACHE); df.index = pd.to_datetime(df.index, utc=True)
|
||||||
|
if len(df):
|
||||||
|
return df
|
||||||
|
import ccxt
|
||||||
|
bs = _fetch_1h(ccxt.bitstamp({"enableRateLimit": True}), "XRP/USD", start, 1000)
|
||||||
|
cb = _fetch_1h(ccxt.coinbase({"enableRateLimit": True}), "XRP/USD", start, 300)
|
||||||
|
df = pd.concat({"bitstamp": bs, "coinbase": cb}, axis=1).sort_index()
|
||||||
|
df["close"] = df["coinbase"].where(df["coinbase"].notna(), df["bitstamp"])
|
||||||
|
df["fonte"] = np.where(df["coinbase"].notna(), "CB", np.where(df["bitstamp"].notna(), "BS", ""))
|
||||||
|
CACHE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
df.to_parquet(CACHE)
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def certifica() -> dict:
|
||||||
|
"""Per anno: quota di barre 1h a >1% da Coinbase, mediana in bps, quota di barre flat (1h e 5m)."""
|
||||||
|
d1 = dati(ASSET, "1h")
|
||||||
|
dz = pd.Series(d1["close"].values.astype(float), index=pd.to_datetime(d1["datetime"], utc=True))
|
||||||
|
ref = riferimento_1h(LISTING)
|
||||||
|
J = pd.concat({"d": dz, "c": ref["close"], "f": ref["fonte"]}, axis=1, join="inner").dropna(subset=["d", "c"])
|
||||||
|
dev = (J["d"] - J["c"]).abs() / J["c"]
|
||||||
|
d5 = dati(ASSET, "5m")
|
||||||
|
f5 = pd.Series((d5["high"].values == d5["low"].values), index=pd.to_datetime(d5["datetime"], utc=True))
|
||||||
|
f1 = pd.Series((d1["high"].values == d1["low"].values), index=dz.index)
|
||||||
|
per_anno = {}
|
||||||
|
for y in sorted(set(J.index.year)):
|
||||||
|
m = dev[dev.index.year == y]
|
||||||
|
fy = J["f"][J.index.year == y]
|
||||||
|
per_anno[int(y)] = dict(n=int(len(m)), q_1pct=float((m > 0.01).mean()), med_bps=float(m.median() * 1e4),
|
||||||
|
fonte=f"CB {float((fy == 'CB').mean())*100:.0f}% / BS {float((fy == 'BS').mean())*100:.0f}%",
|
||||||
|
flat_1h=float(f1[f1.index.year == y].mean()), flat_5m=float(f5[f5.index.year == y].mean()))
|
||||||
|
puliti = [y for y, r in per_anno.items() if r["q_1pct"] <= SOGLIA_PULITA]
|
||||||
|
# L-PULITA = dal primo anno pulito in poi SOLO se anche i successivi restano puliti (un anno pulito
|
||||||
|
# isolato non e' un regime); altrimenti dal primo della coda pulita finale.
|
||||||
|
anni = sorted(per_anno)
|
||||||
|
inizio = None
|
||||||
|
for y in anni:
|
||||||
|
if all(per_anno[z]["q_1pct"] <= SOGLIA_PULITA for z in anni if z >= y):
|
||||||
|
inizio = y; break
|
||||||
|
return dict(per_anno=per_anno, puliti=puliti,
|
||||||
|
pulita_da=pd.Timestamp(f"{inizio}-01-01", tz="UTC") if inizio else None)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ misura (stesso schema di r0822_sol_leg)
|
||||||
|
|
||||||
|
def gamba(asset: str) -> pd.Series:
|
||||||
|
return W_TP * tp01_leg(asset, 0).reindex(skh_leg(asset).index).fillna(0.0) + W_SKH * skh_leg(asset)
|
||||||
|
|
||||||
|
|
||||||
|
def differenze(anchors, start) -> dict:
|
||||||
|
d = {"sh": [], "hold": [], "dd": [], "cagr": []}
|
||||||
|
for h in anchors:
|
||||||
|
A, B = book(DUE, h), book(TRE, h)
|
||||||
|
idx = A.index.intersection(B.index)
|
||||||
|
A, B = taglia(A.loc[idx], start), taglia(B.loc[idx], start)
|
||||||
|
if len(A) < 200:
|
||||||
|
continue
|
||||||
|
d["sh"].append(sh(B) - sh(A)); d["hold"].append(sh(taglia(B, HOLDOUT)) - sh(taglia(A, HOLDOUT)))
|
||||||
|
d["dd"].append((maxdd(B) - maxdd(A)) * 100); d["cagr"].append((cagr(B) - cagr(A)) * 100)
|
||||||
|
return {k: np.array(v, float) for k, v in d.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def misura(n_anc: int = 24) -> dict:
|
||||||
|
anchors = list(range(0, 24, max(1, 24 // n_anc)))[:n_anc]
|
||||||
|
cert = certifica()
|
||||||
|
lenti = {"L-FULL": None}
|
||||||
|
if cert["pulita_da"] is not None:
|
||||||
|
lenti[f"L-PULITA ({cert['pulita_da'].year}+)"] = cert["pulita_da"]
|
||||||
|
lenti["2024+ (lente di SOL)"] = pd.Timestamp("2024-01-01", tz="UTC")
|
||||||
|
out = dict(cert=cert, lenti={}, gambe={}, anno={}, delev={}, corr={})
|
||||||
|
for a in TRE:
|
||||||
|
out["gambe"][f"TP01 {a}"] = tp01_leg(a, 0); out["gambe"][f"SKH01 {a}"] = skh_leg(a)
|
||||||
|
g = gamba(ASSET); b2 = book(DUE, 0)
|
||||||
|
com = g.index.intersection(b2.index)
|
||||||
|
for nome, start in lenti.items():
|
||||||
|
c = com if start is None else com[com >= start]
|
||||||
|
out["corr"][nome] = float(np.corrcoef(g.loc[c], b2.loc[c])[0, 1]) if len(c) > 100 else np.nan
|
||||||
|
out["lenti"][nome] = differenze(anchors, start)
|
||||||
|
A, B = book(DUE, 0), book(TRE, 0); idx = A.index.intersection(B.index)
|
||||||
|
A, B = taglia(A.loc[idx], start), taglia(B.loc[idx], start)
|
||||||
|
k = k_iso_dd(A, B)
|
||||||
|
out["delev"][nome] = dict(k=k, sh3=sh(B), sh2k=sh(k * A), cagr3=cagr(B), cagr2k=cagr(k * A))
|
||||||
|
A0, B0 = book(DUE, 0), book(TRE, 0); i0 = A0.index.intersection(B0.index); A0, B0 = A0.loc[i0], B0.loc[i0]
|
||||||
|
for y in sorted(set(A0.index.year)):
|
||||||
|
a_, b_ = A0[A0.index.year == y], B0[B0.index.year == y]
|
||||||
|
if len(a_) >= 60:
|
||||||
|
out["anno"][int(y)] = dict(sh2=sh(a_), sh3=sh(b_), dsh=sh(b_) - sh(a_), dcagr=(cagr(b_) - cagr(a_)) * 100,
|
||||||
|
sh_gamba=sh(g[g.index.year == y]))
|
||||||
|
out["gamba_xrp"] = g
|
||||||
|
out["n_anchors"] = len(anchors)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def verdetto(m: dict) -> str:
|
||||||
|
"""AGGIUNGE solo se, nella lente PULITA (o 2024+ se non c'e' una coda pulita): dSharpe hold-out
|
||||||
|
mediano > 0 in ≥ 75% delle ancore, E il null del de-levering passa (Sh 3 gambe > Sh 2 gambe
|
||||||
|
riscalate + 0,02), E il contributo per anno e' positivo in piu' anni di quanti sia negativo —
|
||||||
|
contati sugli anni DENTRO la lente (derivati dal suo nome, non cablati: P1).
|
||||||
|
Un campione vuoto e' "NON MISURABILE", non un verdetto (P5: "non vedo" non e' "va tutto bene")."""
|
||||||
|
nomi = [n for n in m["lenti"] if n.startswith("L-PULITA")] or ["2024+ (lente di SOL)"]
|
||||||
|
n = nomi[0]; d = m["lenti"][n]; dl = m["delev"][n]
|
||||||
|
import re
|
||||||
|
anno0 = int(re.search(r"(\d{4})\+", n).group(1)) # "L-PULITA (2024+)" / "2024+ (lente di SOL)"
|
||||||
|
if not len(d["hold"]) or not np.isfinite(dl["sh3"]) or not np.isfinite(dl["sh2k"]):
|
||||||
|
return f"XRP: NON MISURABILE — lente {n} senza ancore valide o de-levering non calcolabile"
|
||||||
|
c_hold = np.median(d["hold"]) > 0 and np.mean(d["hold"] > 0) >= 0.75
|
||||||
|
c_delev = dl["sh3"] > dl["sh2k"] + 0.02
|
||||||
|
anni = [r["dsh"] for y, r in m["anno"].items() if y >= anno0]
|
||||||
|
c_anni = sum(x > 0 for x in anni) > sum(x < 0 for x in anni)
|
||||||
|
esito = "XRP AGGIUNGE" if (c_hold and c_delev and c_anni) else ("XRP DILUISCE" if (not c_hold and not c_delev) else "XRP: PARI / NON SELEZIONABILE")
|
||||||
|
return (f"{esito} — lente {n}: dSharpe hold-out mediano {np.median(d['hold']):+.3f} "
|
||||||
|
f"(>0 nel {np.mean(d['hold'] > 0) * 100:.0f}% di {len(d['hold'])} ancore); "
|
||||||
|
f"de-levering: Sh 3 gambe {dl['sh3']:.2f} vs 2 gambe×{dl['k']:.3f} {dl['sh2k']:.2f}; "
|
||||||
|
f"anni {anno0}+ con dSh>0: {sum(x > 0 for x in anni)}/{len(anni)}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
n_anc = int(sys.argv[sys.argv.index("--anchors") + 1]) if "--anchors" in sys.argv else 24
|
||||||
|
print("=" * 104)
|
||||||
|
print(" r0909 — XRP COME TERZA GAMBA DEL BOOK DERIBIT (meccanismi CONGELATI; harness di r0822_sol_leg)")
|
||||||
|
print("=" * 104)
|
||||||
|
print(" ipotesi a priori dichiarata: DILUISCE (come SOL; trend multi-asset 19/06 corr 0,74)")
|
||||||
|
m = misura(n_anc)
|
||||||
|
c = m["cert"]
|
||||||
|
print(f"\n [0/5] CERTIFICAZIONE XRP vs Coinbase USD (1h) — soglia anno pulito: quota >1% ≤ {SOGLIA_PULITA*100:.1f}%")
|
||||||
|
print(f" {'anno':<6}{'barre':>7}{'>1%':>8}{'med bps':>9}{'flat 1h':>9}{'flat 5m':>9} riferimento")
|
||||||
|
for y, r in c["per_anno"].items():
|
||||||
|
print(f" {y:<6}{r['n']:>7}{r['q_1pct']*100:>7.2f}%{r['med_bps']:>9.1f}{r['flat_1h']*100:>8.1f}%{r['flat_5m']*100:>8.1f}% {r['fonte']}")
|
||||||
|
print(f" L-PULITA da: {c['pulita_da'].date() if c['pulita_da'] is not None else 'NESSUN anno pulito in coda'}")
|
||||||
|
|
||||||
|
print(f"\n [1/5] LE GAMBE DA SOLE (ancora canonica h=0)")
|
||||||
|
print(f"\n {'gamba':<26}{'barre':>7}{'Sharpe':>9}{'maxDD':>10}{'CAGR':>9}{'Sh hold':>10}")
|
||||||
|
for k, s in m["gambe"].items():
|
||||||
|
print(f" {k:<26}{len(s):>7}{sh(s):>9.2f}{maxdd(s)*100:>9.1f}%{cagr(s)*100:>8.1f}%{sh(taglia(s, HOLDOUT)):>10.2f}")
|
||||||
|
g = m["gamba_xrp"]
|
||||||
|
print(f"\n [2/5] LA GAMBA XRP (75/25) — Sharpe {sh(g):.2f}, maxDD {maxdd(g)*100:.1f}%, CAGR {cagr(g)*100:.1f}%, hold-out {sh(taglia(g, HOLDOUT)):.2f}")
|
||||||
|
for n, v in m["corr"].items():
|
||||||
|
print(f" corr(gamba XRP, book BTC/ETH) {n}: {v:+.3f} (SOL: +0,40 / +0,56; soglia del 19/06: 0,74)")
|
||||||
|
|
||||||
|
print(f"\n [3/5] BOOK A 2 vs 3 GAMBE — differenze APPAIATE per ancora ({m['n_anchors']} ancore)")
|
||||||
|
for n, d in m["lenti"].items():
|
||||||
|
if not len(d["sh"]):
|
||||||
|
print(f"\n {n}: campione insufficiente"); continue
|
||||||
|
print(f"\n {n} ({len(d['sh'])} ancore)")
|
||||||
|
for lab, v, u in (("dSharpe FULL", d["sh"], ""), ("dSharpe hold-out", d["hold"], ""), ("dMaxDD", d["dd"], " pp"), ("dCAGR", d["cagr"], " pp")):
|
||||||
|
print(f" {lab:<18} mediana {np.median(v):+7.3f}{u} banda [{np.percentile(v, 10):+.3f}, {np.percentile(v, 90):+.3f}] >0 nel {np.mean(v > 0)*100:.0f}% delle ancore")
|
||||||
|
|
||||||
|
print(f"\n [3bis] PER ANNO (h=0): {'anno':<6}{'2g Sh':>7}{'3g Sh':>7}{'dSh':>7}{'dCAGR':>9}{'gamba XRP':>11}")
|
||||||
|
for y, r in m["anno"].items():
|
||||||
|
print(f" {'':<19}{y:<6}{r['sh2']:>7.2f}{r['sh3']:>7.2f}{r['dsh']:>+7.2f}{r['dcagr']:>+8.1f}pp{r['sh_gamba']:>+11.2f}")
|
||||||
|
|
||||||
|
print(f"\n [4/5] NULL DEL DE-LEVERING (iso-maxDD): {'lente':<24}{'k':>7}{'Sh 3g':>8}{'Sh 2g×k':>9}{'CAGR 3g':>9}{'CAGR 2g×k':>11}")
|
||||||
|
for n, dl in m["delev"].items():
|
||||||
|
v = "AGGIUNGE" if dl["sh3"] > dl["sh2k"] + 0.02 else ("de-levering" if dl["sh2k"] > dl["sh3"] + 0.02 else "pari")
|
||||||
|
print(f" {'':<39}{n:<24}{dl['k']:>7.3f}{dl['sh3']:>8.2f}{dl['sh2k']:>9.2f}{dl['cagr3']*100:>8.1f}%{dl['cagr2k']*100:>10.1f}% {v}")
|
||||||
|
|
||||||
|
px = float(dati(ASSET, "1h")["close"].iloc[-1])
|
||||||
|
print(f"\n [5/5] ESEGUIBILITA': XRP_USDC-PERPETUAL min 1 XRP = ${px:.2f} < pavimento min_order $5 — non e' il vincolo (come SOL)")
|
||||||
|
print("\n VERDETTO: " + verdetto(m))
|
||||||
|
print("=" * 104)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""r0910_macro_riskoff.py — «risk-off» di TP01 nella finestra ±2h intorno a FOMC e CPI: vale qualcosa?
|
||||||
|
|
||||||
|
DOMANDA (operatore, 2026-09-10, da un articolo su EA forex con «agente fondamentale»): se TP01 si
|
||||||
|
mettesse FLAT nelle due ore prima e dopo la dichiarazione FOMC (14:00 ET) e la release CPI
|
||||||
|
(08:30 ET), guadagnerebbe Sharpe o drift? E' un risk-off, quindi per la regola di §3 («non
|
||||||
|
de-esporre il weekend») PARTE REFUTED e deve superare il null del de-levering (M5) e un null
|
||||||
|
location-matched (M18) prima di essere creduto.
|
||||||
|
|
||||||
|
VERDETTO PRE-REGISTRATO (scritto PRIMA di vedere un numero):
|
||||||
|
LEAD solo se TUTTE: (a) ΔSharpe(riskoff − base) > 0 con le fee di chiusura/riapertura;
|
||||||
|
(b) il Δ vero sta al ≥95° percentile del null location-matched (stesso numero di eventi per
|
||||||
|
anno, stesse ore del giorno, giorni estratti a caso); (c) Δ positivo in ≥70% degli anni con ≥5
|
||||||
|
eventi (M9). Altrimenti REFUTED. E l'effetto si cita anche in €/giorno all'equity di oggi.
|
||||||
|
|
||||||
|
COSA NON C'E' (dichiarato): il funding (mai in nessun backtest, −2,16%/anno di drift) — irrilevante
|
||||||
|
qui perche' identico nelle due varianti salvo ~1% delle ore; il rimbalzo di prezzo nella finestra
|
||||||
|
(slippage: TP01 chiude e riapre a mid, in un'ora in cui lo spread e' largo — se il risultato fosse
|
||||||
|
positivo andrebbe rimisurato col costo vero della finestra, C1). SKH01 non e' qui: e' un breakout su
|
||||||
|
barra 230m, appiattirlo cambierebbe il percorso del segnale.
|
||||||
|
|
||||||
|
CALENDARIO (letto dal web il 2026-09-10, non ricordato — regola globale):
|
||||||
|
FOMC: federalreserve.gov/monetarypolicy/fomccalendars.htm (2021-2026) e fomchistorical2019/2020.
|
||||||
|
Ore: 14:00 ET per le riunioni programmate (standard Fed); le due d'emergenza del 2020
|
||||||
|
(03/03 10:00 ET, 15/03 17:00 ET) con la loro ora; i «notation vote» ESCLUSI (nessuna
|
||||||
|
dichiarazione a orario). ET→UTC con America/New_York (DST).
|
||||||
|
CPI: bls.gov/bls/news-release/cpi.htm (archivio 2019-2026), 08:30 ET (bls.gov/schedule).
|
||||||
|
L'archivio raggruppa per anno di RIFERIMENTO: le release di gennaio/febbraio stanno sotto
|
||||||
|
l'anno prima — corrette qui (+1 anno), e la correzione e' verificata sul 2026 (13/01 e
|
||||||
|
13/02 stanno nella schedule ufficiale 2026).
|
||||||
|
|
||||||
|
TP01 ORARIO: posizione decisa alla chiusura giornaliera (00:00 UTC) e tenuta per le 24 barre 1h del
|
||||||
|
giorno (la stessa `held` di sleeves._tp01_returns, spalmata sulle ore); P&L orario = held × rendimento
|
||||||
|
1h; fee sul turnover giornaliero. RISK-OFF: held = 0 nelle barre 1h che intersecano [t−2h, t+2h],
|
||||||
|
con fee_side su chiusura e riapertura (|held| × 2 × fee).
|
||||||
|
|
||||||
|
uv run python scripts/research/r0910_macro_riskoff.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from src.data.downloader import load_data # noqa: E402
|
||||||
|
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d # noqa: E402
|
||||||
|
|
||||||
|
ASSETS = ("BTC", "ETH")
|
||||||
|
ET = ZoneInfo("America/New_York")
|
||||||
|
FINESTRA_H = 2.0
|
||||||
|
N_NULL = 1000
|
||||||
|
SEED = 20260910
|
||||||
|
EQUITY_OGGI = 4454.83 # trades_db --report 10/09 14:21Z, per il €/giorno
|
||||||
|
EURUSD = 1.08 # ordine di grandezza, dichiarato: serve solo per la riga in €/g
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ calendario
|
||||||
|
FOMC_PROGRAMMATE = """
|
||||||
|
2019-01-30 2019-03-20 2019-05-01 2019-06-19 2019-07-31 2019-09-18 2019-10-30 2019-12-11
|
||||||
|
2020-01-29 2020-04-29 2020-06-10 2020-07-29 2020-09-16 2020-11-05 2020-12-16
|
||||||
|
2021-01-27 2021-03-17 2021-04-28 2021-06-16 2021-07-28 2021-09-22 2021-11-03 2021-12-15
|
||||||
|
2022-01-26 2022-03-16 2022-05-04 2022-06-15 2022-07-27 2022-09-21 2022-11-02 2022-12-14
|
||||||
|
2023-02-01 2023-03-22 2023-05-03 2023-06-14 2023-07-26 2023-09-20 2023-11-01 2023-12-13
|
||||||
|
2024-01-31 2024-03-20 2024-05-01 2024-06-12 2024-07-31 2024-09-18 2024-11-07 2024-12-18
|
||||||
|
2025-01-29 2025-03-19 2025-05-07 2025-06-18 2025-07-30 2025-09-17 2025-10-29 2025-12-10
|
||||||
|
2026-01-28 2026-03-18 2026-04-29 2026-06-17 2026-07-29
|
||||||
|
""".split()
|
||||||
|
FOMC_EMERGENZA = [("2020-03-03", "10:00"), ("2020-03-15", "17:00")]
|
||||||
|
|
||||||
|
# archivio BLS, ordine della pagina (per anno di riferimento): le release di gen/feb vanno +1 anno
|
||||||
|
CPI_ARCHIVIO = """
|
||||||
|
2026-08-12 2026-07-14 2026-06-10 2026-05-12 2026-04-10 2026-03-11 2026-02-13
|
||||||
|
2025-01-13 2025-12-18 2025-10-24 2025-09-11 2025-08-12 2025-07-15 2025-06-11 2025-05-13 2025-04-10 2025-03-12 2025-02-12
|
||||||
|
2024-01-15 2024-12-11 2024-11-13 2024-10-10 2024-09-11 2024-08-14 2024-07-11 2024-06-12 2024-05-15 2024-04-10 2024-03-12 2024-02-13
|
||||||
|
2023-01-11 2023-12-12 2023-11-14 2023-10-12 2023-09-13 2023-08-10 2023-07-12 2023-06-13 2023-05-10 2023-04-12 2023-03-14
|
||||||
|
2022-02-14 2022-01-12 2022-12-13 2022-11-10 2022-10-13 2022-09-13 2022-08-10 2022-07-13 2022-06-10 2022-05-11 2022-04-12 2022-03-10
|
||||||
|
2021-02-10 2021-01-12 2021-12-10 2021-11-10 2021-10-13 2021-09-14 2021-08-11 2021-07-13 2021-06-10 2021-05-12 2021-04-13 2021-03-10
|
||||||
|
2020-02-10 2020-01-13 2020-12-10 2020-11-12 2020-10-13 2020-09-11 2020-08-12 2020-07-14 2020-06-10 2020-05-12 2020-04-10 2020-03-11
|
||||||
|
2019-02-13 2019-01-14 2019-12-11 2019-11-13 2019-10-10 2019-09-12 2019-08-13 2019-07-11 2019-06-12 2019-05-10 2019-04-10 2019-03-12
|
||||||
|
""".split()
|
||||||
|
|
||||||
|
|
||||||
|
def cpi_date_corrette(seq: list[str]) -> list[str]:
|
||||||
|
"""Una data di gen/feb che PRECEDE (nell'ordine della pagina, discendente) una data di
|
||||||
|
marzo-dicembre dello STESSO anno e' la release dell'anno dopo."""
|
||||||
|
out = []
|
||||||
|
for i, d in enumerate(seq):
|
||||||
|
y, m = int(d[:4]), int(d[5:7])
|
||||||
|
if m <= 2:
|
||||||
|
j = i + 1
|
||||||
|
while j < len(seq) and int(seq[j][5:7]) <= 2:
|
||||||
|
j += 1
|
||||||
|
if j < len(seq) and int(seq[j][:4]) == y:
|
||||||
|
d = f"{y + 1}{d[4:]}"
|
||||||
|
out.append(d)
|
||||||
|
return sorted(set(out))
|
||||||
|
|
||||||
|
|
||||||
|
def eventi() -> pd.DataFrame:
|
||||||
|
rows = []
|
||||||
|
for d in FOMC_PROGRAMMATE:
|
||||||
|
rows.append(("FOMC", datetime.fromisoformat(f"{d} 14:00").replace(tzinfo=ET)))
|
||||||
|
for d, hm in FOMC_EMERGENZA:
|
||||||
|
rows.append(("FOMC", datetime.fromisoformat(f"{d} {hm}").replace(tzinfo=ET)))
|
||||||
|
for d in cpi_date_corrette(CPI_ARCHIVIO):
|
||||||
|
rows.append(("CPI", datetime.fromisoformat(f"{d} 08:30").replace(tzinfo=ET)))
|
||||||
|
ev = pd.DataFrame(rows, columns=["tipo", "t_et"])
|
||||||
|
ev["t"] = ev["t_et"].map(lambda x: pd.Timestamp(x.astimezone(ZoneInfo("UTC"))))
|
||||||
|
return ev.sort_values("t").reset_index(drop=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ TP01 orario
|
||||||
|
def tp01_orario() -> pd.DataFrame:
|
||||||
|
"""-> DataFrame orario con held_BTC, held_ETH, r_BTC, r_ETH, fee_day (fee del turnover
|
||||||
|
giornaliero, sulla prima barra del giorno)."""
|
||||||
|
tp = TrendPortfolio(**CANONICAL)
|
||||||
|
parts = {}
|
||||||
|
for a in ASSETS:
|
||||||
|
h = load_data(a, "1h")
|
||||||
|
hidx = pd.to_datetime(h["datetime"], utc=True)
|
||||||
|
r1h = pd.Series(h["close"].values.astype(float), index=hidx).pct_change().fillna(0.0)
|
||||||
|
d = resample_1d(h)
|
||||||
|
tgt = tp.target_series(d)
|
||||||
|
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
|
||||||
|
didx = pd.to_datetime(d["datetime"], utc=True).dt.floor("D")
|
||||||
|
held_d = pd.Series(held, index=didx)
|
||||||
|
turn_d = pd.Series(np.abs(np.diff(held, prepend=0.0)) * tp.fee_side, index=didx)
|
||||||
|
day_of_h = hidx.dt.floor("D")
|
||||||
|
held_h = held_d.reindex(day_of_h).values
|
||||||
|
fee_h = np.where(hidx.dt.hour.values == 0, turn_d.reindex(day_of_h).values, 0.0)
|
||||||
|
parts[a] = pd.DataFrame({f"held_{a}": held_h, f"r_{a}": r1h.values, f"fee_{a}": fee_h}, index=hidx)
|
||||||
|
J = pd.concat(parts.values(), axis=1, join="inner").dropna()
|
||||||
|
return J
|
||||||
|
|
||||||
|
|
||||||
|
def pnl(J: pd.DataFrame, flat_mask: np.ndarray | None, fee_side: float) -> pd.Series:
|
||||||
|
"""Rendimento orario del portafoglio 50/50. `flat_mask` True = barra a esposizione zero
|
||||||
|
(con fee di chiusura all'ingresso nella finestra e di riapertura all'uscita)."""
|
||||||
|
out = np.zeros(len(J))
|
||||||
|
for a in ASSETS:
|
||||||
|
held = J[f"held_{a}"].values.copy()
|
||||||
|
fee = J[f"fee_{a}"].values.copy()
|
||||||
|
if flat_mask is not None:
|
||||||
|
h0 = held.copy()
|
||||||
|
held = np.where(flat_mask, 0.0, held)
|
||||||
|
# transizioni dentro/fuori finestra: |Δheld| × fee, oltre al turnover giornaliero
|
||||||
|
extra = np.abs(np.diff(held, prepend=held[0])) - np.abs(np.diff(h0, prepend=h0[0]))
|
||||||
|
fee = fee + np.clip(extra, 0, None) * fee_side
|
||||||
|
out += 0.5 * (held * J[f"r_{a}"].values - fee)
|
||||||
|
return pd.Series(out, index=J.index)
|
||||||
|
|
||||||
|
|
||||||
|
def maschera(J: pd.DataFrame, tempi: pd.Series, w_h: float = FINESTRA_H) -> np.ndarray:
|
||||||
|
"""Barre 1h (open-labeled) che intersecano [t−w, t+w] per almeno un evento."""
|
||||||
|
idx = J.index.values.astype("datetime64[ns]")
|
||||||
|
m = np.zeros(len(J), dtype=bool)
|
||||||
|
w = np.timedelta64(int(w_h * 3600), "s")
|
||||||
|
one_h = np.timedelta64(3600, "s")
|
||||||
|
for t in tempi:
|
||||||
|
t64 = np.datetime64(pd.Timestamp(t).tz_convert("UTC").tz_localize(None))
|
||||||
|
lo, hi = np.searchsorted(idx, t64 - w - one_h, side="right"), np.searchsorted(idx, t64 + w, side="left")
|
||||||
|
m[lo:hi] = True
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def sharpe_h(s: pd.Series) -> float:
|
||||||
|
return float(s.mean() / s.std() * np.sqrt(24 * 365.25)) if s.std() > 0 else float("nan")
|
||||||
|
|
||||||
|
|
||||||
|
def sharpe_d(s: pd.Series) -> float:
|
||||||
|
d = s.groupby(s.index.floor("D")).sum()
|
||||||
|
return float(d.mean() / d.std() * np.sqrt(365.25)) if d.std() > 0 else float("nan")
|
||||||
|
|
||||||
|
|
||||||
|
def drift_annuo(s: pd.Series) -> float:
|
||||||
|
return float(s.mean() * 24 * 365.25)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ misura
|
||||||
|
def misura() -> dict:
|
||||||
|
J = tp01_orario()
|
||||||
|
fee_side = TrendPortfolio(**CANONICAL).fee_side
|
||||||
|
ev = eventi()
|
||||||
|
ev = ev[(ev["t"] >= J.index[0]) & (ev["t"] <= J.index[-1])].reset_index(drop=True)
|
||||||
|
base = pnl(J, None, fee_side)
|
||||||
|
m_all = maschera(J, ev["t"])
|
||||||
|
ro = pnl(J, m_all, fee_side)
|
||||||
|
ro_nofee = pnl(J, m_all, 0.0)
|
||||||
|
|
||||||
|
out = dict(n_ore=len(J), da=str(J.index[0]), a=str(J.index[-1]), n_ev=len(ev),
|
||||||
|
n_fomc=int((ev.tipo == "FOMC").sum()), n_cpi=int((ev.tipo == "CPI").sum()),
|
||||||
|
ore_finestra=int(m_all.sum()), quota_ore=float(m_all.mean()),
|
||||||
|
base=dict(sh_d=sharpe_d(base), sh_h=sharpe_h(base), drift=drift_annuo(base)),
|
||||||
|
riskoff=dict(sh_d=sharpe_d(ro), sh_h=sharpe_h(ro), drift=drift_annuo(ro)),
|
||||||
|
riskoff_nofee=dict(sh_d=sharpe_d(ro_nofee), drift=drift_annuo(ro_nofee)))
|
||||||
|
out["d_sh_d"] = out["riskoff"]["sh_d"] - out["base"]["sh_d"]
|
||||||
|
out["d_drift"] = out["riskoff"]["drift"] - out["base"]["drift"]
|
||||||
|
# cosa succede DENTRO la finestra (il P&L che il risk-off rinuncia)
|
||||||
|
dentro = base[m_all]; fuori = base[~m_all]
|
||||||
|
esposto = (J.loc[m_all, [f"held_{a}" for a in ASSETS]].abs().sum(axis=1) > 0)
|
||||||
|
out["dentro"] = dict(n=int(m_all.sum()), n_esposte=int(esposto.sum()), media_h=float(dentro.mean()),
|
||||||
|
t=float(dentro.mean() / dentro.std() * np.sqrt(len(dentro))) if dentro.std() > 0 else float("nan"),
|
||||||
|
somma=float(dentro.sum()), media_fuori_h=float(fuori.mean()))
|
||||||
|
# per tipo
|
||||||
|
out["per_tipo"] = {}
|
||||||
|
for tipo in ("FOMC", "CPI"):
|
||||||
|
m = maschera(J, ev.loc[ev.tipo == tipo, "t"])
|
||||||
|
r = pnl(J, m, fee_side)
|
||||||
|
out["per_tipo"][tipo] = dict(n=int((ev.tipo == tipo).sum()), d_sh_d=sharpe_d(r) - out["base"]["sh_d"],
|
||||||
|
d_drift=drift_annuo(r) - out["base"]["drift"], somma_dentro=float(base[m].sum()))
|
||||||
|
# per anno (M9)
|
||||||
|
anni = {}
|
||||||
|
for y, g in base.groupby(base.index.year):
|
||||||
|
my = m_all[base.index.year == y]
|
||||||
|
ry = ro[base.index.year == y]
|
||||||
|
ney = int((ev["t"].dt.year == y).sum())
|
||||||
|
anni[int(y)] = dict(n_ev=ney, d_sh_d=sharpe_d(ry) - sharpe_d(g), somma_dentro=float(g[my].sum()),
|
||||||
|
d_cum=float((1 + ry).prod() - (1 + g).prod()))
|
||||||
|
out["anni"] = anni
|
||||||
|
# null location-matched (M18): stessi conteggi per anno e stesse ORE del giorno, giorni a caso
|
||||||
|
rng = np.random.default_rng(SEED)
|
||||||
|
giorni_per_anno = {y: np.array(sorted(set(J.index[J.index.year == y].normalize()))) for y in J.index.year.unique()}
|
||||||
|
d_null = np.empty(N_NULL)
|
||||||
|
for k in range(N_NULL):
|
||||||
|
t_fake = []
|
||||||
|
for y, g in ev.groupby(ev["t"].dt.year):
|
||||||
|
pool = giorni_per_anno.get(int(y))
|
||||||
|
if pool is None or len(pool) == 0:
|
||||||
|
continue
|
||||||
|
days = rng.choice(pool, size=len(g), replace=False)
|
||||||
|
for d, t in zip(days, g["t"]):
|
||||||
|
t_fake.append(pd.Timestamp(d) + (t - t.normalize()))
|
||||||
|
mk = maschera(J, pd.Series(t_fake))
|
||||||
|
d_null[k] = sharpe_d(pnl(J, mk, fee_side)) - out["base"]["sh_d"]
|
||||||
|
out["null"] = dict(n=N_NULL, media=float(d_null.mean()), p50=float(np.median(d_null)),
|
||||||
|
p95=float(np.percentile(d_null, 95)), p05=float(np.percentile(d_null, 5)),
|
||||||
|
pctl_vero=float((d_null < out["d_sh_d"]).mean() * 100))
|
||||||
|
# €/giorno all'equity di oggi
|
||||||
|
out["eur_giorno"] = out["d_drift"] * EQUITY_OGGI / EURUSD / 365.25
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def verdetto(m: dict) -> tuple[str, list[str]]:
|
||||||
|
a = m["d_sh_d"] > 0
|
||||||
|
b = m["null"]["pctl_vero"] >= 95.0
|
||||||
|
anni_ok = [y for y, v in m["anni"].items() if v["n_ev"] >= 5]
|
||||||
|
pos = sum(1 for y in anni_ok if m["anni"][y]["d_sh_d"] > 0)
|
||||||
|
c = anni_ok and pos / len(anni_ok) >= 0.70
|
||||||
|
righe = [f"(a) ΔSharpe giornaliero con fee > 0: {m['d_sh_d']:+.3f} → {'PASS' if a else 'FAIL'}",
|
||||||
|
f"(b) Δ vero al ≥95° pctl del null location-matched: {m['null']['pctl_vero']:.1f}° → {'PASS' if b else 'FAIL'}",
|
||||||
|
f"(c) Δ > 0 in ≥70% degli anni con ≥5 eventi: {pos}/{len(anni_ok)} → {'PASS' if c else 'FAIL'}"]
|
||||||
|
return ("LEAD" if (a and b and c) else "REFUTED"), righe
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
print("=" * 88)
|
||||||
|
print(" r0910 — RISK-OFF ±2h intorno a FOMC e CPI su TP01 (lente oraria, posizione giornaliera)")
|
||||||
|
print("=" * 88)
|
||||||
|
m = misura()
|
||||||
|
print(f" ore: {m['n_ore']} {m['da'][:10]} → {m['a'][:10]} eventi: {m['n_ev']} "
|
||||||
|
f"(FOMC {m['n_fomc']}, CPI {m['n_cpi']}) ore in finestra: {m['ore_finestra']} = {m['quota_ore']:.2%}")
|
||||||
|
print(f"\n {'':14s} {'Sh giorn.':>10s} {'Sh orario':>10s} {'drift/anno':>11s}")
|
||||||
|
for k in ("base", "riskoff"):
|
||||||
|
v = m[k]; print(f" {k:14s} {v['sh_d']:>10.3f} {v['sh_h']:>10.3f} {v['drift']:>+11.2%}")
|
||||||
|
v = m["riskoff_nofee"]; print(f" {'riskoff nofee':14s} {v['sh_d']:>10.3f} {'':>10s} {v['drift']:>+11.2%}")
|
||||||
|
print(f"\n Δ riskoff − base: Sharpe giornaliero {m['d_sh_d']:+.3f} · drift {m['d_drift']:+.2%}/anno "
|
||||||
|
f"· ≈ {m['eur_giorno']:+.3f} €/giorno a ${EQUITY_OGGI:,.0f}")
|
||||||
|
d = m["dentro"]
|
||||||
|
print(f" dentro la finestra: {d['n']} ore ({d['n_esposte']} esposte), media {d['media_h']*1e4:+.2f} bp/h "
|
||||||
|
f"(fuori {d['media_fuori_h']*1e4:+.2f} bp/h), t = {d['t']:+.2f}, somma {d['somma']:+.2%}")
|
||||||
|
print("\n per tipo:")
|
||||||
|
for t, v in m["per_tipo"].items():
|
||||||
|
print(f" {t:5s} n={v['n']:3d} ΔSh {v['d_sh_d']:+.3f} Δdrift {v['d_drift']:+.2%} P&L dentro {v['somma_dentro']:+.2%}")
|
||||||
|
print("\n per anno (M9):")
|
||||||
|
print(f" {'anno':>4s} {'ev':>3s} {'ΔSh':>7s} {'P&L dentro':>11s} {'Δcum':>8s}")
|
||||||
|
for y, v in m["anni"].items():
|
||||||
|
print(f" {y:>4d} {v['n_ev']:>3d} {v['d_sh_d']:>+7.3f} {v['somma_dentro']:>+11.2%} {v['d_cum']:>+8.2%}")
|
||||||
|
n = m["null"]
|
||||||
|
print(f"\n null location-matched (M18, {n['n']} estrazioni): ΔSh mediana {n['p50']:+.3f} "
|
||||||
|
f"[p05 {n['p05']:+.3f}, p95 {n['p95']:+.3f}] — il Δ vero sta al {n['pctl_vero']:.1f}° percentile")
|
||||||
|
v, righe = verdetto(m)
|
||||||
|
print("\n VERDETTO PRE-REGISTRATO:")
|
||||||
|
for r in righe:
|
||||||
|
print(" " + r)
|
||||||
|
print(f"\n ⇒ {v}")
|
||||||
|
print("\n non modellato: funding (identico nelle due varianti salvo ~1% delle ore), slippage della "
|
||||||
|
"finestra (spread largo: se il risultato fosse positivo andrebbe rimisurato col costo vero, C1).")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -205,7 +205,10 @@ def testata(voce: dict) -> str:
|
|||||||
r = [f"<b>📓 Giornale — {_b(voce['giorno'])}</b>"]
|
r = [f"<b>📓 Giornale — {_b(voce['giorno'])}</b>"]
|
||||||
r.append(" equity " + (f"<b>${eq:,.2f}</b>" if eq is not None else "n/d")
|
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 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"):
|
if "errore" not in lb and lb.get("asset"):
|
||||||
pos = " · ".join(f"{a} ${v['posizione']:+,.0f}" for a, v in sorted(lb["asset"].items()))
|
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")
|
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
|
||||||
+157
-30
@@ -25,6 +25,12 @@ from src.live import tradesdb as T
|
|||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
JOURNAL_DIR = PROJECT_ROOT / "docs" / "journal"
|
JOURNAL_DIR = PROJECT_ROOT / "docs" / "journal"
|
||||||
|
# Movimenti di capitale DICHIARATI dall'operatore (append-only, dentro il perimetro di backup —
|
||||||
|
# P11). Servono per i movimenti che il rilevatore non puo' vedere: sotto `EQUITY_JUMP_ALERT`
|
||||||
|
# (il versamento di prova da ~$25 del 2026-08-25 08:00Z, +3,3% su $647, restato nel P&L per
|
||||||
|
# 12 giorni) o dentro un'ora di mercato non misurabile. Una riga per movimento:
|
||||||
|
# {"ts_utc": "...", "delta_usd": 24.9, "banda_usd": 0.5, "nota": "...", "dichiarato_il": "..."}
|
||||||
|
MOVIMENTI_DICHIARATI = PROJECT_ROOT / "data" / "live" / "movimenti_dichiarati.jsonl"
|
||||||
ASSETS = ("BTC", "ETH")
|
ASSETS = ("BTC", "ETH")
|
||||||
# Gli orizzonti del segnale TP01 in produzione (trend_portfolio: TSMOM 30/90/180 giorni).
|
# Gli orizzonti del segnale TP01 in produzione (trend_portfolio: TSMOM 30/90/180 giorni).
|
||||||
ORIZZONTI = (30, 90, 180)
|
ORIZZONTI = (30, 90, 180)
|
||||||
@@ -118,7 +124,34 @@ def _px_1h(asset: str) -> pd.Series | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def movimenti_capitale(con, fino_ts: str) -> dict:
|
def movimenti_dichiarati(path: Path | None = None) -> tuple[list[dict], list[str]]:
|
||||||
|
"""Righe di `MOVIMENTI_DICHIARATI` -> (movimenti validi, errori di riga). Mai solleva.
|
||||||
|
Una riga rotta non azzera le altre e NON sparisce: torna in `errori` (P3 — si registra nel
|
||||||
|
punto in cui si ingoia)."""
|
||||||
|
path = MOVIMENTI_DICHIARATI if path is None else path
|
||||||
|
if not path.exists():
|
||||||
|
return [], []
|
||||||
|
validi, errori = [], []
|
||||||
|
for n, riga in enumerate(path.read_text().splitlines(), 1):
|
||||||
|
if not riga.strip():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
d = json.loads(riga)
|
||||||
|
ts = pd.Timestamp(d["ts_utc"])
|
||||||
|
if ts.tzinfo is None:
|
||||||
|
raise ValueError("ts_utc senza fuso")
|
||||||
|
delta = float(d["delta_usd"])
|
||||||
|
if delta == 0:
|
||||||
|
raise ValueError("delta_usd zero")
|
||||||
|
validi.append(dict(ts=ts, delta=delta, banda=float(d.get("banda_usd") or 0.0),
|
||||||
|
nota=str(d.get("nota") or ""), dichiarato_il=d.get("dichiarato_il")))
|
||||||
|
except Exception as e: # noqa: BLE001 — la riga rotta si riporta
|
||||||
|
errori.append(f"{path.name}:{n}: {type(e).__name__}: {e}")
|
||||||
|
validi.sort(key=lambda d: d["ts"])
|
||||||
|
return validi, errori
|
||||||
|
|
||||||
|
|
||||||
|
def movimenti_capitale(con, fino_ts: str, rows=None) -> dict:
|
||||||
"""Salti di equity fra letture consecutive: versamenti e prelievi, da scorporare dal P&L.
|
"""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
|
PERCHE' ESISTE. Il P&L di giornale e' un delta di equity, quindi un versamento ci finisce
|
||||||
@@ -137,45 +170,101 @@ def movimenti_capitale(con, fino_ts: str) -> dict:
|
|||||||
|
|
||||||
⚠️ LIMITE DICHIARATO (D5): un movimento sotto soglia (es. $150 su un conto da $2.000)
|
⚠️ LIMITE DICHIARATO (D5): un movimento sotto soglia (es. $150 su un conto da $2.000)
|
||||||
non si distingue dal mercato e RESTA nel P&L. E' lo stesso limite del rilevatore live.
|
non si distingue dal mercato e RESTA nel P&L. E' lo stesso limite del rilevatore live.
|
||||||
|
Il rimedio NON e' abbassare la soglia (ogni ora di mercato diventerebbe un candidato): e'
|
||||||
|
la DICHIARAZIONE dell'operatore in `MOVIMENTI_DICHIARATI` — l'unica fonte che sa l'importo.
|
||||||
|
Un movimento dichiarato e' `movimento` con `fonte="dichiarato"`, l'importo e' quello
|
||||||
|
dichiarato (non il salto di equity), e `dopo` = `prima` + importo: il P&L di mercato di
|
||||||
|
quell'ora resta nel rendimento invece di uscire con il movimento. Se coincide con un salto
|
||||||
|
rilevato, l'importo dichiarato VINCE sul salto (fonte "dichiarato+rilevato"); un movimento
|
||||||
|
dichiarato fuori dalle letture, o dopo `fino_ts`, non entra e finisce in `avvisi`.
|
||||||
"""
|
"""
|
||||||
from src.live import book as _book # lazy: prende la soglia viva, anche monkeypatchata
|
from src.live import book as _book # lazy: prende la soglia viva, anche monkeypatchata
|
||||||
rows = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc <= ? ORDER BY ts_utc",
|
# `rows` si puo' passare gia' letto: due SELECT sulla stessa tabella a un'ora del cron
|
||||||
(fino_ts,)).fetchall()
|
# 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)
|
soglia = float(_book.EQUITY_JUMP_ALERT)
|
||||||
candidati = [(a, b) for a, b in zip(rows, rows[1:])
|
candidati = [(a, b) for a, b in zip(rows, rows[1:])
|
||||||
if a["equity"] and abs(b["equity"] - a["equity"]) / a["equity"] >= soglia]
|
if a["equity"] and abs(b["equity"] - a["equity"]) / a["equity"] >= soglia]
|
||||||
if not candidati:
|
dichiarati, avvisi = movimenti_dichiarati()
|
||||||
return dict(eventi=[], certi=0.0, ambigui=0.0, soglia=soglia)
|
if not candidati and not dichiarati:
|
||||||
|
return dict(eventi=[], certi=0.0, ambigui=0.0, soglia=soglia, avvisi=avvisi)
|
||||||
try:
|
try:
|
||||||
cfg = json.loads((PROJECT_ROOT / "config" / "live.json").read_text())
|
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:
|
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}
|
px = {a: _px_1h(a) for a in ASSETS} if candidati else {}
|
||||||
eventi = []
|
eventi = []
|
||||||
for prev, cur in candidati:
|
for prev, cur in candidati:
|
||||||
t0, t1 = pd.Timestamp(prev["ts_utc"]), pd.Timestamp(cur["ts_utc"])
|
t0, t1 = pd.Timestamp(prev["ts_utc"]), pd.Timestamp(cur["ts_utc"])
|
||||||
delta = float(cur["equity"] - prev["equity"])
|
delta = float(cur["equity"] - prev["equity"])
|
||||||
pct = delta / 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:
|
for a in ASSETS:
|
||||||
s = px.get(a)
|
s = px.get(a)
|
||||||
if s is None or s.empty:
|
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)
|
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:
|
||||||
mosse.append(abs(float(p1) / float(p0) - 1.0))
|
non_mis.append(f"{a}: barra assente"); continue
|
||||||
max_mkt = max(mosse) * leva_tetto if mosse else None
|
mosse.append(abs(float(p1) / float(p0) - 1.0))
|
||||||
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"],
|
eventi.append(dict(ts_prima=prev["ts_utc"], ts_dopo=cur["ts_utc"],
|
||||||
prima=float(prev["equity"]), dopo=float(cur["equity"]),
|
prima=float(prev["equity"]), dopo=float(cur["equity"]),
|
||||||
delta=delta, pct=float(pct),
|
delta=delta, pct=float(pct),
|
||||||
mercato_max_pct=(None if max_mkt is None else float(max_mkt)),
|
mercato_max_pct=(None if max_mkt is None else float(max_mkt)),
|
||||||
classe=classe))
|
mercato_nota=nota, classe=classe, fonte="rilevato"))
|
||||||
|
# --- dichiarati dall'operatore: l'importo lo sa solo lui ------------------------------
|
||||||
|
ts_rows = [pd.Timestamp(r["ts_utc"]) for r in rows]
|
||||||
|
for d in dichiarati:
|
||||||
|
if not rows or d["ts"] <= ts_rows[0] or d["ts"] > ts_rows[-1]:
|
||||||
|
avvisi.append(f"movimento dichiarato {d['ts']:%Y-%m-%dT%H:%M}Z ${d['delta']:+,.2f} "
|
||||||
|
f"fuori dalle letture [{rows[0]['ts_utc'][:16] if rows else '-'} .. "
|
||||||
|
f"{rows[-1]['ts_utc'][:16] if rows else '-'}]: non applicato")
|
||||||
|
continue
|
||||||
|
i = next(k for k in range(1, len(rows)) if ts_rows[k - 1] < d["ts"] <= ts_rows[k])
|
||||||
|
prev, cur = rows[i - 1], rows[i]
|
||||||
|
nota_d = (f"dichiarato dall'operatore"
|
||||||
|
+ (f" il {d['dichiarato_il']}" if d.get("dichiarato_il") else "")
|
||||||
|
+ (f": {d['nota']}" if d["nota"] else "")
|
||||||
|
+ (f" (banda ±${d['banda']:,.2f})" if d["banda"] else ""))
|
||||||
|
gia = next((e for e in eventi if e["ts_dopo"] == cur["ts_utc"]), None)
|
||||||
|
if gia is not None: # rilevato E dichiarato: l'importo e' quello dichiarato
|
||||||
|
gia.update(delta=d["delta"], pct=d["delta"] / float(prev["equity"]) if prev["equity"] else None,
|
||||||
|
classe="movimento", fonte="dichiarato+rilevato",
|
||||||
|
dopo=float(prev["equity"]) + d["delta"], dichiarazione=nota_d)
|
||||||
|
continue
|
||||||
|
eventi.append(dict(ts_prima=prev["ts_utc"], ts_dopo=cur["ts_utc"],
|
||||||
|
prima=float(prev["equity"]), dopo=float(prev["equity"]) + d["delta"],
|
||||||
|
delta=d["delta"],
|
||||||
|
pct=(d["delta"] / float(prev["equity"]) if prev["equity"] else None),
|
||||||
|
mercato_max_pct=None, mercato_nota=None, classe="movimento",
|
||||||
|
fonte="dichiarato", dichiarazione=nota_d))
|
||||||
|
eventi.sort(key=lambda e: e["ts_dopo"])
|
||||||
return dict(eventi=eventi,
|
return dict(eventi=eventi,
|
||||||
certi=float(sum(e["delta"] for e in eventi if e["classe"] == "movimento")),
|
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")),
|
ambigui=float(sum(e["delta"] for e in eventi if e["classe"] == "ambiguo")),
|
||||||
soglia=soglia, leva_tetto=leva_tetto)
|
soglia=soglia, leva_tetto=leva_tetto, avvisi=avvisi)
|
||||||
|
|
||||||
|
|
||||||
def rendimento_twr(con, fino_ts: str) -> dict:
|
def rendimento_twr(con, fino_ts: str) -> dict:
|
||||||
@@ -191,35 +280,61 @@ def rendimento_twr(con, fino_ts: str) -> dict:
|
|||||||
apre uno all'equity DOPO; il TWR e' il prodotto dei rendimenti di segmento, meno uno. Gli
|
apre uno all'equity DOPO; il TWR e' il prodotto dei rendimenti di segmento, meno uno. Gli
|
||||||
eventi `ambiguo` NON spezzano niente (P12: restano nel rendimento, dichiarati accanto).
|
eventi `ambiguo` NON spezzano niente (P12: restano nel rendimento, dichiarati accanto).
|
||||||
|
|
||||||
TRE STATI: `twr` e' un numero, oppure None con `motivo` (serie vuota, base zero).
|
⚠️ LIMITE DICHIARATO (D5). L'intervallo che CONTIENE un movimento certo esce INTERO dal
|
||||||
|
rendimento: il suo P&L di mercato (marcatura delle posizioni aperte, funding; in un buco di
|
||||||
|
lettura — fino a 10h nella serie vera — l'intero buco) finisce in `certi`, quindi anche fuori
|
||||||
|
da `trading`. E' limitato per costruzione dal margine del classificatore (|salto| >
|
||||||
|
2·max_mkt·leva·equity), percio' l'errore massimo vale max_mkt·leva·equity: fino a META' del
|
||||||
|
movimento riconosciuto. Il 25/08: libro a ~$263 lordi, mercato a ±0,2% -> ~$0,5 su $1.399,39.
|
||||||
|
Non si stima e non si corregge (P12): si dichiara. Un intervallo di sola marcatura fra due
|
||||||
|
movimenti consecutivi, o prima del primo, ha lunghezza zero e NON produce un segmento.
|
||||||
|
|
||||||
|
TRE STATI: `twr` e `trading` sono numeri, oppure None con `motivo`. 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",
|
rows = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc <= ? ORDER BY ts_utc",
|
||||||
(fino_ts,)).fetchall()
|
(fino_ts,)).fetchall()
|
||||||
if not rows:
|
if not rows:
|
||||||
return dict(twr=None, motivo="nessuna lettura di equity", segmenti=[], eventi=[],
|
return dict(twr=None, motivo="nessuna lettura di equity", segmenti=[], eventi=[],
|
||||||
certi=0.0, ambigui=0.0, delta_equity=None, trading=None, letture=0)
|
certi=0.0, ambigui=0.0, delta_equity=None, trading=None, letture=0)
|
||||||
mv = movimenti_capitale(con, fino_ts)
|
mv = movimenti_capitale(con, fino_ts, rows=rows) # stessa lettura, un solo istante
|
||||||
certi = [e for e in mv["eventi"] if e["classe"] == "movimento"]
|
|
||||||
e0, e1 = float(rows[0]["equity"]), float(rows[-1]["equity"])
|
e0, e1 = float(rows[0]["equity"]), float(rows[-1]["equity"])
|
||||||
|
if e0 <= 0:
|
||||||
|
return dict(twr=None, trading=None,
|
||||||
|
motivo="base di equity nulla: il primo versamento non e' classificabile",
|
||||||
|
segmenti=[], eventi=mv["eventi"], certi=mv["certi"], ambigui=mv["ambigui"],
|
||||||
|
delta_equity=e1 - e0, letture=len(rows))
|
||||||
|
non_mis = [e for e in mv["eventi"] if e.get("mercato_nota") and e["classe"] != "movimento"]
|
||||||
|
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
|
segmenti, da_ts, da_eq = [], rows[0]["ts_utc"], e0
|
||||||
for ev in certi: # gia' in ordine di tempo
|
for ev in certi: # gia' in ordine di tempo
|
||||||
if da_eq:
|
if da_eq and ev["ts_prima"] != da_ts: # lunghezza zero = niente segmento
|
||||||
segmenti.append(dict(da=da_ts, a=ev["ts_prima"], e_da=da_eq, e_a=ev["prima"],
|
segmenti.append(dict(da=da_ts, a=ev["ts_prima"], e_da=da_eq, e_a=ev["prima"],
|
||||||
ret=ev["prima"] / da_eq - 1.0))
|
ret=ev["prima"] / da_eq - 1.0))
|
||||||
da_ts, da_eq = ev["ts_dopo"], ev["dopo"]
|
da_ts, da_eq = ev["ts_dopo"], ev["dopo"]
|
||||||
if da_eq:
|
if da_eq and rows[-1]["ts_utc"] != da_ts:
|
||||||
segmenti.append(dict(da=da_ts, a=rows[-1]["ts_utc"], e_da=da_eq, e_a=e1,
|
segmenti.append(dict(da=da_ts, a=rows[-1]["ts_utc"], e_da=da_eq, e_a=e1,
|
||||||
ret=e1 / da_eq - 1.0))
|
ret=e1 / da_eq - 1.0))
|
||||||
if not segmenti or e0 <= 0:
|
|
||||||
return dict(twr=None, motivo="base di equity nulla", segmenti=segmenti, eventi=mv["eventi"],
|
|
||||||
certi=mv["certi"], ambigui=mv["ambigui"], delta_equity=e1 - e0,
|
|
||||||
trading=e1 - e0 - mv["certi"], letture=len(rows))
|
|
||||||
twr = 1.0
|
twr = 1.0
|
||||||
for sg in segmenti:
|
for sg in segmenti: # nessun segmento = nessun tempo a mercato = 0
|
||||||
twr *= 1.0 + sg["ret"]
|
twr *= 1.0 + sg["ret"]
|
||||||
return dict(twr=twr - 1.0, motivo=None, segmenti=segmenti, eventi=mv["eventi"],
|
return dict(twr=twr - 1.0, motivo=None, segmenti=segmenti, eventi=mv["eventi"],
|
||||||
certi=mv["certi"], ambigui=mv["ambigui"], delta_equity=e1 - e0,
|
certi=mv["certi"], ambigui=mv["ambigui"], delta_equity=e1 - e0,
|
||||||
trading=e1 - e0 - mv["certi"], letture=len(rows))
|
trading=e1 - e0 - mv["certi"], letture=len(rows), avvisi=mv.get("avvisi") or [])
|
||||||
|
|
||||||
|
|
||||||
def pnl_giorno(con, giorno: date) -> dict:
|
def pnl_giorno(con, giorno: date) -> dict:
|
||||||
@@ -240,7 +355,10 @@ def pnl_giorno(con, giorno: date) -> dict:
|
|||||||
e_arm = con.execute("SELECT equity FROM equity ORDER BY ts_utc LIMIT 1").fetchone()
|
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
|
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
|
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]
|
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")
|
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")
|
amb_g = sum(e["delta"] for e in ev_g if e["classe"] == "ambiguo")
|
||||||
@@ -254,7 +372,8 @@ def pnl_giorno(con, giorno: date) -> dict:
|
|||||||
movimenti_giorno=ev_g, mov_giorno=float(mov_g), mov_giorno_ambiguo=float(amb_g),
|
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,
|
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"],
|
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:
|
def salute(con, giorno: date) -> dict:
|
||||||
@@ -362,7 +481,11 @@ def lettura(con, voce: dict) -> list[dict]:
|
|||||||
# --- movimenti di capitale: prima del P&L, perche' lo ridefiniscono ----------------------
|
# --- movimenti di capitale: prima del P&L, perche' lo ridefiniscono ----------------------
|
||||||
for e in p.get("movimenti_giorno") or []:
|
for e in p.get("movimenti_giorno") or []:
|
||||||
h0, h1 = e["ts_prima"][11:16], e["ts_dopo"][11:16]
|
h0, h1 = e["ts_prima"][11:16], e["ts_dopo"][11:16]
|
||||||
if e["classe"] == "movimento":
|
if e["classe"] == "movimento" and e.get("fonte", "rilevato") != "rilevato":
|
||||||
|
dire("movimento", f"**Movimento di capitale ${e['delta']:+,.2f}** fra le {h0} e le "
|
||||||
|
f"{h1} UTC, {e.get('dichiarazione') or 'dichiarato'}. "
|
||||||
|
"Scorporato dal P&L di trading qui sotto.", "nota")
|
||||||
|
elif e["classe"] == "movimento":
|
||||||
dire("movimento", f"**Movimento di capitale ${e['delta']:+,.2f}** fra le {h0} e le "
|
dire("movimento", f"**Movimento di capitale ${e['delta']:+,.2f}** fra le {h0} e le "
|
||||||
f"{h1} UTC: salto {100*e['pct']:+.1f}% contro un massimo spiegabile "
|
f"{h1} UTC: salto {100*e['pct']:+.1f}% contro un massimo spiegabile "
|
||||||
f"dal mercato di ±{100*e['mercato_max_pct']:.1f}% al tetto di leva. "
|
f"dal mercato di ±{100*e['mercato_max_pct']:.1f}% al tetto di leva. "
|
||||||
@@ -626,6 +749,10 @@ def rendi_markdown(con, voce: dict) -> str:
|
|||||||
if p.get("mov_arming"):
|
if p.get("mov_arming"):
|
||||||
riga_c += (f" — di cui ${p['mov_arming']:+,.2f} versati/prelevati -> trading "
|
riga_c += (f" — di cui ${p['mov_arming']:+,.2f} versati/prelevati -> trading "
|
||||||
f"**{_n(p.get('trading_da_arming'),'${:+,.2f}')}**")
|
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"):
|
if p.get("mov_arming_ambiguo"):
|
||||||
riga_c += f" — ⚠️ ${p['mov_arming_ambiguo']:+,.2f} ambigui non attribuiti"
|
riga_c += f" — ⚠️ ${p['mov_arming_ambiguo']:+,.2f} ambigui non attribuiti"
|
||||||
L += ["", "## P&L", "",
|
L += ["", "## P&L", "",
|
||||||
|
|||||||
@@ -92,6 +92,12 @@ MONITORS: tuple[MonitorSpec, ...] = (
|
|||||||
MonitorSpec("usde_watch", "live", "usde_watch.jsonl", 24.0, max_age_h=30.0,
|
MonitorSpec("usde_watch", "live", "usde_watch.jsonl", 24.0, max_age_h=30.0,
|
||||||
gate="allarmi depeg/quota USDE + verdetto eligibilita' 29/08",
|
gate="allarmi depeg/quota USDE + verdetto eligibilita' 29/08",
|
||||||
open_labeled=False),
|
open_labeled=False),
|
||||||
|
# Cuscino USDC di regolamento (cron :53, ORARIO, debito §5.17 / issue #3): se si ferma nessuno
|
||||||
|
# vede il saldo USDC scendere sotto il cuscino, e la riconversione automatica non parte.
|
||||||
|
# max_age 3h = due giri persi; timbri di esecuzione -> open_labeled=False.
|
||||||
|
MonitorSpec("cuscino_watch", "live", "cuscino_watch.jsonl", 1.0, max_age_h=3.0,
|
||||||
|
gate="cuscino USDC di regolamento: allerta + riconversione automatica",
|
||||||
|
open_labeled=False),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -87,9 +87,15 @@ def notify(title: str, data: dict | None = None, tentativi: int = 1) -> bool:
|
|||||||
ver = f" <code>v{APP_VERSION}</code>"
|
ver = f" <code>v{APP_VERSION}</code>"
|
||||||
except Exception:
|
except Exception:
|
||||||
ver = ""
|
ver = ""
|
||||||
lines = [f"<b>{title}</b>{ver}"]
|
# Telegram e' in parse_mode=HTML: un '<' o '&' nudo in un valore fa fallire l'INTERO invio
|
||||||
|
# (HTTP 400, "can't parse entities"). Successo il 2026-09-10 13:53Z: la prima allerta di
|
||||||
|
# cuscino_watch conteneva «(< 10% del cuscino)» ed e' andata persa. L'escape sta QUI, nel sink,
|
||||||
|
# perche' nessuno dei 45 chiamanti passa tag di proposito (verificato con grep) e un chiamante
|
||||||
|
# nuovo non deve ricordarsene. `send()` resta grezzo per chi costruisce il proprio HTML.
|
||||||
|
import html
|
||||||
|
lines = [f"<b>{html.escape(str(title), quote=False)}</b>{ver}"]
|
||||||
for k, v in (data or {}).items():
|
for k, v in (data or {}).items():
|
||||||
lines.append(f" {k}: {v}")
|
lines.append(f" {html.escape(str(k), quote=False)}: {html.escape(str(v), quote=False)}")
|
||||||
return send("\n".join(lines), tentativi=tentativi)
|
return send("\n".join(lines), tentativi=tentativi)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,346 @@
|
|||||||
|
"""Revisione settimanale in SOLA LETTURA: un revisore diverso dall'autore rilegge il sistema.
|
||||||
|
|
||||||
|
PERCHE' ESISTE (2026-09-09). Nove sorveglianti in cron guardano ognuno la propria grandezza
|
||||||
|
(venue, fee, edge, scala, USDE, VRP-f, health, balance, paper-monitor); il giornale e l'analista
|
||||||
|
leggono UN giorno. Nessuno rilegge il sistema nel suo insieme: stato, gate con le date, decisioni
|
||||||
|
vincolanti, debiti aperti, cosa e' cambiato nei diari e nei commit. La revisione con un secondo
|
||||||
|
modello (regola del 2026-09-03: chi rivede non e' chi ha scritto) ha trovato in una sessione due
|
||||||
|
conclusioni false e un look-ahead che l'autore aveva riletto tre volte. Questo modulo la schedula.
|
||||||
|
|
||||||
|
COSA FA. Raccoglie il materiale (CLAUDE.md, config, crontab, log git, monitor_health, giornale
|
||||||
|
degli ultimi giorni, diari recenti, stato dei sorveglianti), lo da' a un modello DIVERSO da quello
|
||||||
|
che scrive il codice, e salva il rapporto FIRMATO in `docs/revisioni/<data>.md`, con una sintesi su
|
||||||
|
Telegram. Il rapporto e' l'opinione di un lettore fallibile (P13): dice cosa non torna, cosa e'
|
||||||
|
maturo da decidere, cosa propone. Non e' parte del registro delle misure.
|
||||||
|
|
||||||
|
COSA NON FA, PER COSTRUZIONE. Non tocca config, libro, cron, codice, dati: l'unico file che
|
||||||
|
scrive e' il rapporto. Non esegue proposte: le scrive, e l'operatore decide. Non riapre le
|
||||||
|
decisioni vincolanti (CLAUDE.md §3) se la colonna «cosa la riapre» non e' soddisfatta, e non
|
||||||
|
propone «idee nuove» senza citare il file di memoria che ha gia' ucciso l'idea simile. Un ciclo
|
||||||
|
che si autoregola e' esattamente cio' che `scale_watch` e `venue_watch` trattano come anomalia.
|
||||||
|
|
||||||
|
Il silenzio non e' una revisione: se il modello non risponde, il rapporto lo dice e Telegram lo
|
||||||
|
dice, invece di lasciare un lunedi' vuoto che si legge come «tutto bene».
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import html
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from datetime import date, datetime, timedelta, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from src.live import analista as A
|
||||||
|
from src.live import monitor_health as MH
|
||||||
|
|
||||||
|
MODELLO = "claude-fable-5-1" # il revisore: NON il modello che scrive il codice
|
||||||
|
CADENZA = "settimanale"
|
||||||
|
FINESTRA_GIORNALE_G = 7 # pagine del giornale lette
|
||||||
|
FINESTRA_DIARI_G = 7 # diari letti (la settimana: e' una revisione settimanale)
|
||||||
|
FINESTRA_COMMIT_G = 14 # commit letti
|
||||||
|
CARTELLA = Path("docs") / "revisioni"
|
||||||
|
MAX_CHARS_VOCE = 90_000 # CLAUDE.md sta sotto; il resto e' molto piu' piccolo
|
||||||
|
MAX_CHARS_TOTALE = 400_000
|
||||||
|
MAX_NUMERI_LIBERI = 12 # una revisione confronta e sottrae: qualche numero suo e' lecito
|
||||||
|
LIMITE_TELEGRAM = 3_500 # sotto i 4.096 di Telegram, con la testata
|
||||||
|
TIMEOUT_S = 900 # il materiale e' grande: 5 minuti non bastano sempre
|
||||||
|
|
||||||
|
TITOLI = ("## Sintesi", "## Cosa non torna", "## Cosa e' maturo da decidere",
|
||||||
|
"## Proposte (nessuna azione eseguita)", "## Cosa ho letto e cosa mi mancava")
|
||||||
|
|
||||||
|
ISTRUZIONI = """Sei il REVISORE settimanale di un progetto di trading quantitativo (BTC/ETH su Deribit, libro
|
||||||
|
live con soldi veri, ~$4.500). Sei un modello DIVERSO da quello che scrive codice e analisi del
|
||||||
|
progetto: il tuo valore e' che non condividi il suo ragionamento. Leggi il materiale qui sotto e
|
||||||
|
scrivi la revisione, in italiano, per l'operatore (che decide) e per l'agente (che esegue).
|
||||||
|
|
||||||
|
REGOLE VINCOLANTI:
|
||||||
|
1. SOLA LETTURA. Non proponi comandi da eseguire ora, non scrivi codice: descrivi cosa non torna
|
||||||
|
e cosa andrebbe verificato o deciso, e da chi.
|
||||||
|
2. OGNI SEGNALAZIONE CITA LA FONTE: file e sezione (es. «CLAUDE.md §4, gate STATARB»), data della
|
||||||
|
pagina di giornale, nome del diario, hash del commit. Una segnalazione senza fonte non vale.
|
||||||
|
3. USA SOLO I NUMERI DEL MATERIALE. Puoi sottrarre e confrontare, ma ogni cifra che scrivi deve
|
||||||
|
essere rintracciabile qui sotto; un controllo automatico elenchera' quelle che non lo sono.
|
||||||
|
4. LE DECISIONI VINCOLANTI (CLAUDE.md §3) NON SI RIPROPONGONO. Puoi dire che una condizione della
|
||||||
|
colonna «cosa la riapre» sembra soddisfatta, citando il dato; non puoi consigliare di cambiare
|
||||||
|
la decisione per altri motivi.
|
||||||
|
5. UN'IDEA «NUOVA» SI PROPONE SOLO citando il file di memoria (docs/memory/…) o il paragrafo che ha
|
||||||
|
gia' ucciso l'idea piu' simile, e dicendo in che cosa questa differisce. Se non lo sai, non la
|
||||||
|
proponi: scrivi «da verificare in memoria prima di proporla».
|
||||||
|
6. I GATE HANNO UNA DATA (CLAUDE.md §4). Elenca quelli la cui data cade entro 30 giorni o e' gia'
|
||||||
|
passata, con il criterio scritto e lo stato che il materiale mostra. Non anticipare un verdetto:
|
||||||
|
un gate si decide alla sua data, coi criteri scritti prima.
|
||||||
|
7. DISTINGUI misurato da dedotto. «Non determinabile da questo materiale» e' una risposta buona.
|
||||||
|
8. NIENTE PREVISIONI di mercato, niente consigli di direzione, niente target.
|
||||||
|
9. Se qualcosa richiede l'operatore SUBITO (fondi, credenziali, un sorvegliante fermo che copre un
|
||||||
|
gate, una modifica di config non registrata), mettilo nella Sintesi con la parola URGENTE.
|
||||||
|
10. Massimo ~1.000 parole. Usa ESATTAMENTE questi titoli, in quest'ordine, e nient'altro prima:
|
||||||
|
%s
|
||||||
|
Sotto «Cosa ho letto e cosa mi mancava» di' quali fonti hai usato e quale dato ti sarebbe
|
||||||
|
servito e non c'era (e' l'elenco da cui il materiale della prossima settimana migliora).
|
||||||
|
""" % "\n ".join(TITOLI)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- materiale
|
||||||
|
|
||||||
|
def _tronca(testo: str, n: int | None = None) -> tuple[str, bool]:
|
||||||
|
n = MAX_CHARS_VOCE if n is None else n # letto a runtime: un default e' catturato alla definizione (debito 12)
|
||||||
|
if len(testo) <= n:
|
||||||
|
return testo, False
|
||||||
|
return testo[:n] + f"\n[… troncato a {n:,} caratteri su {len(testo):,}]", True
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd(args: list[str], cwd: Path) -> str:
|
||||||
|
"""Uscita di un comando di SOLA LETTURA, o il motivo per cui non si legge (P5: «non vedo»
|
||||||
|
non e' «va tutto bene»)."""
|
||||||
|
try:
|
||||||
|
p = subprocess.run(args, cwd=str(cwd), capture_output=True, text=True, timeout=60)
|
||||||
|
except Exception as exc: # noqa: BLE001 — un comando di lettura che esplode si dichiara
|
||||||
|
return f"non leggibile: {type(exc).__name__}: {str(exc)[:120]}"
|
||||||
|
if p.returncode != 0:
|
||||||
|
return f"non leggibile: uscita {p.returncode}: {(p.stderr or p.stdout).strip()[:200]}"
|
||||||
|
return p.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def _coda(path: Path, n: int) -> str:
|
||||||
|
if not path.exists():
|
||||||
|
return "(assente)"
|
||||||
|
righe = path.read_text().splitlines()
|
||||||
|
return "\n".join(righe[-n:]) if righe else "(vuoto)"
|
||||||
|
|
||||||
|
|
||||||
|
def _data_nel_nome(p: Path) -> date | None:
|
||||||
|
try:
|
||||||
|
return date.fromisoformat(p.name[:10])
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _monitor_health(root: Path, adesso: datetime) -> str:
|
||||||
|
try:
|
||||||
|
righe = MH.check_all(root, now=adesso)
|
||||||
|
except Exception as exc: # noqa: BLE001 — si dichiara, non si tace (P3)
|
||||||
|
return f"non leggibile: {type(exc).__name__}: {exc}"
|
||||||
|
out = ["monitor | stato | perche' | gate a rischio"]
|
||||||
|
for r in righe:
|
||||||
|
out.append(f"{r.get('name')} | {r.get('status')} | {r.get('why')} | {r.get('gate') or '-'}")
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def materiale(root: Path, adesso: datetime) -> list[tuple[str, str, bool]]:
|
||||||
|
"""Le voci (nome, testo, troncato) date al revisore. Tutto in SOLA LETTURA.
|
||||||
|
|
||||||
|
L'ordine e' quello in cui un lettore umano le aprirebbe: prima le regole e lo stato dichiarato,
|
||||||
|
poi cio' che gira (sorveglianti: piccoli, e per questo PRIMA delle voci grandi, cosi' il tetto
|
||||||
|
totale non li taglia mai), poi cio' che e' successo (giornale, diari).
|
||||||
|
"""
|
||||||
|
oggi = adesso.date()
|
||||||
|
voci: list[tuple[str, str]] = []
|
||||||
|
|
||||||
|
voci.append(("CLAUDE.md (stato, book, numeri, decisioni vincolanti, gate, debiti, regole)",
|
||||||
|
(root / "CLAUDE.md").read_text() if (root / "CLAUDE.md").exists() else "(assente)"))
|
||||||
|
cfg = root / "config" / "live.json"
|
||||||
|
voci.append(("config/live.json (unica autorita' dei guardrail live)",
|
||||||
|
cfg.read_text() if cfg.exists() else "(assente)"))
|
||||||
|
voci.append(("crontab installata (crontab -l)", _cmd(["crontab", "-l"], root)))
|
||||||
|
since = (oggi - timedelta(days=FINESTRA_COMMIT_G)).isoformat()
|
||||||
|
voci.append((f"git log dal {since} + stato del working tree",
|
||||||
|
_cmd(["git", "log", f"--since={since}", "--format=%h %ad %s", "--date=short"], root)
|
||||||
|
+ "\n--- git status --short ---\n" + _cmd(["git", "status", "--short"], root)))
|
||||||
|
voci.append(("monitor_health (i forward-monitor stanno registrando?)", _monitor_health(root, adesso)))
|
||||||
|
|
||||||
|
live = root / "data" / "live"
|
||||||
|
stati = {
|
||||||
|
"scale_watch_state.json": (live / "scale_watch_state.json", 999),
|
||||||
|
"scale_history.jsonl (ultime 3)": (live / "scale_history.jsonl", 3),
|
||||||
|
"vrp_f_watch/state.json": (root / "data" / "vrp_f_watch" / "state.json", 999),
|
||||||
|
"usde_watch.jsonl (ultime 3)": (live / "usde_watch.jsonl", 3),
|
||||||
|
"usde_convert.jsonl (ultime 3)": (live / "usde_convert.jsonl", 3),
|
||||||
|
"balance_watch.jsonl (ultime 2)": (live / "balance_watch.jsonl", 2),
|
||||||
|
"venue_news.jsonl (ultime 5)": (live / "venue_news.jsonl", 5),
|
||||||
|
"movimenti_dichiarati.jsonl": (live / "movimenti_dichiarati.jsonl", 50),
|
||||||
|
"equity_seen.json (watermark)": (live / "equity_seen.json", 999),
|
||||||
|
}
|
||||||
|
for nome, (p, n) in stati.items():
|
||||||
|
voci.append((f"stato {nome}", _coda(p, n)))
|
||||||
|
|
||||||
|
for i in range(FINESTRA_GIORNALE_G, 0, -1):
|
||||||
|
g = oggi - timedelta(days=i)
|
||||||
|
p = root / "docs" / "journal" / f"{g.isoformat()}.md"
|
||||||
|
if p.exists():
|
||||||
|
voci.append((f"giornale {g.isoformat()}", p.read_text()))
|
||||||
|
diari = sorted(p for p in (root / "docs" / "diary").glob("*.md")
|
||||||
|
if (_data_nel_nome(p) or date.min) >= oggi - timedelta(days=FINESTRA_DIARI_G))
|
||||||
|
for p in diari:
|
||||||
|
voci.append((f"diario {p.name}", p.read_text()))
|
||||||
|
|
||||||
|
out, tot = [], 0
|
||||||
|
for nome, testo in voci:
|
||||||
|
t, tr = _tronca(testo)
|
||||||
|
if tot + len(t) > MAX_CHARS_TOTALE:
|
||||||
|
t, tr = _tronca(t, max(0, MAX_CHARS_TOTALE - tot)), True
|
||||||
|
tot += len(t)
|
||||||
|
out.append((nome, t, tr))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def costruisci_prompt(voci: list[tuple[str, str, bool]], adesso: datetime) -> str:
|
||||||
|
parti = [ISTRUZIONI, f"\nDATA DELLA REVISIONE: {adesso:%Y-%m-%d %H:%M}Z (usa questa per «oggi» e per le date dei gate).\n"]
|
||||||
|
for nome, testo, tr in voci:
|
||||||
|
parti.append(f"\n=== {nome}{' [TRONCATO]' if tr else ''} ===\n{testo}\n")
|
||||||
|
parti.append("\nScrivi ora la revisione, coi cinque titoli, e nient'altro.")
|
||||||
|
return "\n".join(parti)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- guardia
|
||||||
|
|
||||||
|
def valida(testo: str, fonte: str) -> tuple[str, list[str]]:
|
||||||
|
"""(stato, motivi) — stato in {ok, sospetta, rifiutata}. Rifiutata = non e' una revisione
|
||||||
|
(vuota o senza i titoli); sospetta = numeri non rintracciabili nel materiale, elencati."""
|
||||||
|
motivi: list[str] = []
|
||||||
|
t = testo.strip()
|
||||||
|
if not t:
|
||||||
|
return "rifiutata", ["risposta vuota"]
|
||||||
|
mancanti = [x for x in TITOLI if x not in t]
|
||||||
|
if mancanti:
|
||||||
|
return "rifiutata", [f"titoli mancanti: {', '.join(mancanti)}"]
|
||||||
|
fuori = A.numeri_non_supportati(t, fonte)
|
||||||
|
if fuori:
|
||||||
|
motivi.append(f"{len(fuori)} numeri non presenti nel materiale: " + ", ".join(fuori[:30]))
|
||||||
|
if len(fuori) > MAX_NUMERI_LIBERI:
|
||||||
|
return "sospetta", motivi
|
||||||
|
return ("sospetta" if motivi else "ok"), motivi
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- rapporto
|
||||||
|
|
||||||
|
def rapporto(testo: str | None, errore: str | None, stato: str, motivi: list[str], modello: str,
|
||||||
|
adesso: datetime, voci: list[tuple[str, str, bool]]) -> str:
|
||||||
|
testa = [f"# Revisione {CADENZA} — {adesso:%Y-%m-%d}", ""]
|
||||||
|
if errore:
|
||||||
|
testa += [f"## Revisione NON eseguita",
|
||||||
|
"",
|
||||||
|
f"Il modello `{modello}` non ha risposto: **{errore}**. Non c'e' una revisione questa "
|
||||||
|
"settimana, e questo file esiste perche' un lunedi' senza rapporto si leggerebbe "
|
||||||
|
"come «tutto bene» (P5).", ""]
|
||||||
|
corpo = []
|
||||||
|
else:
|
||||||
|
testa += [f"*Scritta il {adesso:%Y-%m-%dT%H:%M:%S}Z dal modello `{modello}`, revisore diverso "
|
||||||
|
"dall'autore del codice, in SOLA LETTURA: nessun file del repo, nessuna config, nessun "
|
||||||
|
"ordine e' stato toccato. E' l'opinione di un lettore fallibile (P13): ogni numero e "
|
||||||
|
"ogni proposta vanno verificati alla fonte prima di agire. Guardia sui numeri: "
|
||||||
|
f"**{stato}**" + (f" — {'; '.join(motivi)}" if motivi else "") + ".*", ""]
|
||||||
|
corpo = [testo.strip(), ""]
|
||||||
|
coda = ["---", "## Firma e materiale", "",
|
||||||
|
f"- revisore: `{modello}` · cadenza {CADENZA} · giornale {FINESTRA_GIORNALE_G} g · "
|
||||||
|
f"diari {FINESTRA_DIARI_G} g · commit {FINESTRA_COMMIT_G} g",
|
||||||
|
"- scritto da `scripts/live/revisione.py` (cron `scripts/cron_review.sh`); l'unico file "
|
||||||
|
"scritto e' questo", "",
|
||||||
|
"| voce | caratteri | troncata |", "|---|---|---|"]
|
||||||
|
coda += [f"| {n} | {len(t):,} | {'si' if tr else ''} |" for n, t, tr in voci]
|
||||||
|
return "\n".join(testa + corpo + coda) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _sezione(testo: str, titolo: str) -> str:
|
||||||
|
if titolo not in testo:
|
||||||
|
return ""
|
||||||
|
dopo = testo.split(titolo, 1)[1]
|
||||||
|
for t in TITOLI:
|
||||||
|
if t != titolo and t in dopo:
|
||||||
|
dopo = dopo.split(t, 1)[0]
|
||||||
|
return dopo.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def per_telegram(testo: str | None, errore: str | None, stato: str, adesso: datetime,
|
||||||
|
path: Path) -> str:
|
||||||
|
"""La Sintesi, con la testata e il taglio DICHIARATO (un taglio silenzioso e' peggio di
|
||||||
|
un'assenza: si legge come un pensiero finito)."""
|
||||||
|
if errore:
|
||||||
|
return (f"<b>🚨 Revisione {CADENZA} NON eseguita</b> {adesso:%Y-%m-%d}\n"
|
||||||
|
f"{html.escape(errore)}\n<code>{html.escape(str(path))}</code>")
|
||||||
|
sintesi = _sezione(testo or "", TITOLI[0]) or "(nessuna Sintesi nel rapporto)"
|
||||||
|
corpo = html.escape(sintesi, quote=False)
|
||||||
|
tagliato = ""
|
||||||
|
if len(corpo) > LIMITE_TELEGRAM:
|
||||||
|
corpo, tagliato = corpo[:LIMITE_TELEGRAM], "\n[… sintesi tagliata: il resto e' nel file]"
|
||||||
|
return (f"<b>🔍 Revisione {CADENZA}</b> {adesso:%Y-%m-%d} · guardia numeri: {stato}\n\n"
|
||||||
|
f"{corpo}{tagliato}\n\n<code>{html.escape(str(path))}</code>")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- modello
|
||||||
|
|
||||||
|
def interroga(prompt: str, modello: str = MODELLO, timeout: int = TIMEOUT_S) -> tuple[str, str | None]:
|
||||||
|
"""(testo, errore) dalla CLI `claude -p`, col prompt su STDIN.
|
||||||
|
|
||||||
|
Non su argv: al primo giro reale (09/09, 195k caratteri) `analista.interroga` e' morto con
|
||||||
|
`OSError: Argument list too long` PRIMA di chiamare il modello — e l'eccezione, non catturata,
|
||||||
|
ha lasciato zero rapporto e zero Telegram: il silenzio che questo modulo esiste per vietare.
|
||||||
|
Qui ogni eccezione della chiamata e' un errore RESTITUITO (P3), mai propagato.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
p = subprocess.run(
|
||||||
|
["claude", "-p", "--model", modello, "--allowed-tools", "", "--max-turns", "1"],
|
||||||
|
input=prompt, capture_output=True, text=True, timeout=timeout)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return "", "CLI `claude` non trovata"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return "", f"timeout dopo {timeout}s"
|
||||||
|
except Exception as exc: # noqa: BLE001 — si restituisce, non si tace
|
||||||
|
return "", f"{type(exc).__name__}: {str(exc)[:200]}"
|
||||||
|
if p.returncode != 0:
|
||||||
|
return "", A.motivo_uscita(p.returncode, p.stdout, p.stderr)
|
||||||
|
return p.stdout.strip(), None # NON `analista.pulisci`: toglierebbe i titoli «## …» che la guardia richiede
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- orchestrazione
|
||||||
|
|
||||||
|
def scrivi(root: Path, adesso: datetime | None = None, modello: str = MODELLO, *,
|
||||||
|
secco: bool = False, telegram: bool = True, quiet: bool = False,
|
||||||
|
interroga=None, invia=None) -> dict:
|
||||||
|
"""Un giro: materiale → modello → rapporto → Telegram. Ritorna cosa e' successo.
|
||||||
|
|
||||||
|
`interroga(prompt, modello, timeout) -> (testo, errore)` e `invia(html) -> bool` sono
|
||||||
|
INIETTATI (default: `interroga` di questo modulo — prompt su stdin — e `notifier.send`) cosi'
|
||||||
|
i test non spendono e non mandano. Se uno dei due SOLLEVA, l'eccezione diventa un errore nel
|
||||||
|
rapporto: un giro che muore a meta' non deve lasciare un lunedi' vuoto. Con `secco` non si chiama il modello e non si scrive niente: si stampa la taglia.
|
||||||
|
"""
|
||||||
|
adesso = adesso or datetime.now(timezone.utc)
|
||||||
|
voci = materiale(root, adesso)
|
||||||
|
prompt = costruisci_prompt(voci, adesso)
|
||||||
|
esito = {"data": adesso.date().isoformat(), "voci": len(voci), "caratteri": len(prompt),
|
||||||
|
"modello": modello, "secco": secco}
|
||||||
|
if not quiet:
|
||||||
|
print(f" revisione {esito['data']} modello {modello} materiale {len(voci)} voci, "
|
||||||
|
f"{len(prompt):,} caratteri" + (" [SECCO: nessuna chiamata, nessuna scrittura]" if secco else ""))
|
||||||
|
for n, t, tr in voci:
|
||||||
|
print(f" {len(t):>8,} {n}{' [troncata]' if tr else ''}")
|
||||||
|
if secco:
|
||||||
|
return esito
|
||||||
|
|
||||||
|
interroga = interroga or globals()["interroga"]
|
||||||
|
try:
|
||||||
|
testo, errore = interroga(prompt, modello, TIMEOUT_S)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
testo, errore = "", f"eccezione nella chiamata al modello: {type(exc).__name__}: {str(exc)[:200]}"
|
||||||
|
stato, motivi = ("rifiutata", [errore]) if errore else valida(testo, prompt)
|
||||||
|
if not errore and stato == "rifiutata":
|
||||||
|
errore, testo = "risposta non e' una revisione: " + "; ".join(motivi), None
|
||||||
|
|
||||||
|
cartella = root / CARTELLA
|
||||||
|
cartella.mkdir(parents=True, exist_ok=True)
|
||||||
|
path = cartella / f"{adesso:%Y-%m-%d}.md"
|
||||||
|
path.write_text(rapporto(testo, errore, stato, motivi, modello, adesso, voci))
|
||||||
|
esito.update(stato=stato, errore=errore, motivi=motivi, file=str(path))
|
||||||
|
|
||||||
|
if telegram:
|
||||||
|
from src.live import notifier as N
|
||||||
|
invia = invia or (lambda h: N.send(h, tentativi=2))
|
||||||
|
try:
|
||||||
|
esito["telegram"] = bool(invia(per_telegram(testo, errore, stato, adesso, path)))
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
esito["telegram"] = False
|
||||||
|
esito["telegram_errore"] = f"{type(exc).__name__}: {str(exc)[:200]}"
|
||||||
|
if not quiet:
|
||||||
|
tg = "" if "telegram" not in esito else (" telegram: inviato" if esito["telegram"] else
|
||||||
|
f" telegram: NON inviato ({esito.get('telegram_errore', 'send False')})")
|
||||||
|
print(f" -> {stato}" + (f" ({errore})" if errore else "") + f" {path}{tg}")
|
||||||
|
return esito
|
||||||
@@ -36,6 +36,10 @@ _DEFAULTS = {
|
|||||||
"venue_cap_misurato": "2026-08-31",
|
"venue_cap_misurato": "2026-08-31",
|
||||||
"depeg_warn": 0.99,
|
"depeg_warn": 0.99,
|
||||||
"depeg_crit": 0.95,
|
"depeg_crit": 0.95,
|
||||||
|
# bande del cuscino USDC (frazioni del cuscino richiesto), issue #7 — vedi `_nota_cuscino`
|
||||||
|
"cuscino_preavviso_frac": 0.10,
|
||||||
|
"cuscino_margine_frac": 0.20,
|
||||||
|
"cuscino_riacquisto_frac": 0.40,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -93,3 +97,54 @@ def valuta(eq_usde: float, px: float | None) -> tuple[float, str]:
|
|||||||
return eq_usde, f"USDE {eq_usde:,.0f} valutato 1.0000 (indice non leggibile)"
|
return eq_usde, f"USDE {eq_usde:,.0f} valutato 1.0000 (indice non leggibile)"
|
||||||
p = min(px, 1.0)
|
p = min(px, 1.0)
|
||||||
return eq_usde * p, f"USDE {eq_usde:,.0f} @ {p:.4f}"
|
return eq_usde * p, f"USDE {eq_usde:,.0f} @ {p:.4f}"
|
||||||
|
|
||||||
|
|
||||||
|
# --- CUSCINO USDC DI REGOLAMENTO ------------------------------------------------------------
|
||||||
|
# P&L e funding dei perp USDC-lineari si regolano in USDC, non nel collaterale: a quota USDE alta
|
||||||
|
# il saldo USDC va negativo alla prima perdita del libro e Deribit lo finanzia allo 0,05%/GIORNO
|
||||||
|
# (KB "Cross collateral specifications": 18,25%/anno, 4,3x la resa USDE). Il cuscino richiesto e'
|
||||||
|
# il disaster-SL sulla MASSIMA esposizione lorda che il libro puo' prendere:
|
||||||
|
# n_asset x frac x disaster_sl_pct x equity_totale
|
||||||
|
# tutto letto da config/live.json e src/live/book.py, MAI ridichiarato (P1). Prima del 10/09 la
|
||||||
|
# formula viveva in scripts/live/usde_convert.py; il sorvegliante cuscino_watch la deriva da qui,
|
||||||
|
# e usde_convert pure — due lettori, una formula.
|
||||||
|
|
||||||
|
N_ASSET = 2
|
||||||
|
|
||||||
|
|
||||||
|
def frazione_cuscino() -> tuple[float, str]:
|
||||||
|
"""PURA sulla config. -> (frazione dell'equity totale che deve restare in USDC, come)."""
|
||||||
|
cfg = json.loads(CFG_PATH.read_text())
|
||||||
|
sl = float(cfg["disaster_sl_pct"])
|
||||||
|
from src.live import book as B # lazy: book importa shadow, che importa questo modulo
|
||||||
|
frac = float(B.WEIGHT)
|
||||||
|
return N_ASSET * frac * sl, f"{N_ASSET} asset x frac {frac} x SL {sl:.0%}"
|
||||||
|
|
||||||
|
|
||||||
|
def cuscino_richiesto_usd(equity_tot: float) -> tuple[float, str]:
|
||||||
|
"""Cuscino USDC di REGOLAMENTO in dollari, per un'equity totale data. -> (usd, come)."""
|
||||||
|
f, come = frazione_cuscino()
|
||||||
|
return equity_tot * f, f"{come} su ${equity_tot:,.2f}"
|
||||||
|
|
||||||
|
|
||||||
|
def bande_cuscino(c: dict | None = None) -> dict:
|
||||||
|
"""PURA sulla config. Le tre frazioni del cuscino che governano cuscino_watch, con l'ordine
|
||||||
|
verificato: 0 < preavviso < margine < riacquisto. Fuori ordine la banda di isteresi non
|
||||||
|
esiste (vendita e acquisto si inseguirebbero): si solleva, non si indovina."""
|
||||||
|
c = c or cfg()
|
||||||
|
b = dict(preavviso=float(c["cuscino_preavviso_frac"]), margine=float(c["cuscino_margine_frac"]),
|
||||||
|
riacquisto=float(c["cuscino_riacquisto_frac"]))
|
||||||
|
if not (0.0 < b["preavviso"] < b["margine"] < b["riacquisto"]):
|
||||||
|
raise ValueError(f"bande del cuscino fuori ordine (serve 0 < preavviso < margine < riacquisto): {b}")
|
||||||
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
def quota_ripristino(margine_frac: float | None = None) -> float:
|
||||||
|
"""PURA sulla config. Quota USDE che lascia il cuscino PIU' un margine (frazione del cuscino):
|
||||||
|
e' il bersaglio UNICO di ogni conversione automatica — vendita quando il cuscino e' scoperto,
|
||||||
|
acquisto quando lo slack eccede la banda. Il margine serve a non tornare sul bordo alla prima
|
||||||
|
ora in perdita (P6: la persistenza e' latenza). Default: `cuscino_margine_frac` di config."""
|
||||||
|
if margine_frac is None:
|
||||||
|
margine_frac = bande_cuscino()["margine"]
|
||||||
|
f, _ = frazione_cuscino()
|
||||||
|
return max(0.0, 1.0 - f * (1.0 + margine_frac))
|
||||||
|
|||||||
@@ -122,6 +122,11 @@ def _isola_file_operativi_vivi(tmp_path, monkeypatch):
|
|||||||
# non deve poter sporcare il working tree (P16: il cron gira dal working tree).
|
# non deve poter sporcare il working tree (P16: il cron gira dal working tree).
|
||||||
import src.live.journal as journal
|
import src.live.journal as journal
|
||||||
monkeypatch.setattr(journal, "JOURNAL_DIR", tmp_path / "journal")
|
monkeypatch.setattr(journal, "JOURNAL_DIR", tmp_path / "journal")
|
||||||
|
# --- data/live/movimenti_dichiarati.jsonl (2026-09-06) ------------------------------------
|
||||||
|
# `journal.movimenti_capitale` LEGGE i movimenti dichiarati dall'operatore: se un test
|
||||||
|
# vedesse quelli veri, un versamento reale entrerebbe in una serie di fixture e il test
|
||||||
|
# passerebbe o fallirebbe per un motivo che non e' suo. Qui il file non esiste.
|
||||||
|
monkeypatch.setattr(journal, "MOVIMENTI_DICHIARATI", tmp_path / "movimenti_dichiarati.jsonl")
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
def test_config_default_disarmed(tmp_path, monkeypatch):
|
||||||
"""load_config di book_execute mette execution_enabled=False di default (fail-safe)."""
|
"""load_config di book_execute mette execution_enabled=False di default (fail-safe)."""
|
||||||
import importlib
|
# `scripts/` non e' un package importabile per nome: si carica via percorso
|
||||||
be = importlib.import_module("scripts.live.book_execute") if False else None
|
from helpers import carica_script
|
||||||
# carica il modulo via path (scripts/ non e' un package importabile per nome)
|
mod = carica_script("scripts/live/book_execute.py")
|
||||||
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)
|
|
||||||
monkeypatch.setattr(mod, "CONFIG", tmp_path / "nope.json") # config assente
|
monkeypatch.setattr(mod, "CONFIG", tmp_path / "nope.json") # config assente
|
||||||
assert mod.load_config()["execution_enabled"] is False
|
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):
|
def test_book_execute_surfaces_skh_error(monkeypatch, capsys):
|
||||||
"""Se il report porta skh_error, _run() lo stampa E chiama notify (niente flat silenzioso)."""
|
"""Se il report porta skh_error, _run() lo stampa E chiama notify (niente flat silenzioso)."""
|
||||||
import importlib.util
|
from helpers import carica_script
|
||||||
spec = importlib.util.spec_from_file_location("book_execute", PROJECT_ROOT / "scripts/live/book_execute.py")
|
mod = carica_script("scripts/live/book_execute.py")
|
||||||
mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
|
|
||||||
|
|
||||||
canned = dict(
|
canned = dict(
|
||||||
last_data=_fresh_bar(), online=True, real_equity=600.0, equity=600.0, eq_basis="test",
|
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):
|
def test_book_execute_halts_on_unreadable_position(monkeypatch, capsys):
|
||||||
"""ARMATO + --execute + ordine presente: il gate DEVE fermarsi PRIMA di costruire il trader."""
|
"""ARMATO + --execute + ordine presente: il gate DEVE fermarsi PRIMA di costruire il trader."""
|
||||||
import importlib.util
|
from helpers import carica_script
|
||||||
spec = importlib.util.spec_from_file_location("book_execute", PROJECT_ROOT / "scripts/live/book_execute.py")
|
mod = carica_script("scripts/live/book_execute.py")
|
||||||
mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
|
|
||||||
|
|
||||||
canned = dict(
|
canned = dict(
|
||||||
last_data=_fresh_bar(), online=True, real_equity=598.0, equity=598.0, eq_basis="mainnet USDC",
|
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):
|
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)."""
|
"""eq_fallback: avvisa + notify MA PROSEGUE (non e' un halt, a differenza di pos_error)."""
|
||||||
import importlib.util
|
from helpers import carica_script
|
||||||
spec = importlib.util.spec_from_file_location("book_execute", PROJECT_ROOT / "scripts/live/book_execute.py")
|
mod = carica_script("scripts/live/book_execute.py")
|
||||||
mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
|
|
||||||
|
|
||||||
canned = dict(
|
canned = dict(
|
||||||
last_data=_fresh_bar(), online=True, real_equity=None, equity=2000.0,
|
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().
|
# B. Isolamento per asset dentro book_execute._run().
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
def _carica_book_execute():
|
def _carica_book_execute():
|
||||||
spec = importlib.util.spec_from_file_location(
|
from helpers import carica_script
|
||||||
"book_execute", PROJECT_ROOT / "scripts/live/book_execute.py")
|
return carica_script("scripts/live/book_execute.py")
|
||||||
mod = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(mod)
|
|
||||||
return mod
|
|
||||||
|
|
||||||
|
|
||||||
def _report_due_asset():
|
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
|
"""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.
|
esatta del 2026-07-14 — ma feed vecchio. Non deve partire nessun ordine e deve arrivare l'alert.
|
||||||
"""
|
"""
|
||||||
import importlib.util
|
from helpers import carica_script
|
||||||
spec = importlib.util.spec_from_file_location(
|
mod = carica_script("scripts/live/book_execute.py", "book_execute_iso")
|
||||||
"book_execute_iso", ROOT / "scripts" / "live" / "book_execute.py")
|
|
||||||
mod = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(mod)
|
|
||||||
|
|
||||||
canned = dict(
|
canned = dict(
|
||||||
last_data="2026-07-08", online=True, real_equity=598.0, equity=598.0,
|
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):
|
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
|
"""Una config priva di max_data_age_days deve ricadere sul default, non sollevare KeyError
|
||||||
dentro il percorso con soldi veri."""
|
dentro il percorso con soldi veri."""
|
||||||
import importlib.util
|
from helpers import carica_script
|
||||||
spec = importlib.util.spec_from_file_location(
|
mod = carica_script("scripts/live/book_execute.py", "book_execute_iso2")
|
||||||
"book_execute_iso2", ROOT / "scripts" / "live" / "book_execute.py")
|
|
||||||
mod = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(mod)
|
|
||||||
canned = dict(last_data=str(pd.Timestamp.now(tz="UTC").normalize().date()), online=True,
|
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,
|
real_equity=598.0, equity=598.0, eq_basis="t", cap_per_asset=300.0,
|
||||||
skh_error=None, pos_error=None, assets=[], orders=[])
|
skh_error=None, pos_error=None, assets=[], orders=[])
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ sys.path.insert(0, str(ROOT))
|
|||||||
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||||
sys.path.insert(0, str(ROOT / "scripts" / "analysis"))
|
sys.path.insert(0, str(ROOT / "scripts" / "analysis"))
|
||||||
|
|
||||||
from cblib import bs_put, f_factors, pick_legs # noqa: E402
|
import cblib # noqa: E402
|
||||||
|
from cblib import bs_put, causale, f_factors, pick_legs # noqa: E402
|
||||||
from certify_cb_chain import ( # noqa: E402
|
from certify_cb_chain import ( # noqa: E402
|
||||||
crossed_rate, hollow_rate, monotonicity_violations, worst_day_hollow,
|
crossed_rate, hollow_rate, monotonicity_violations, worst_day_hollow,
|
||||||
)
|
)
|
||||||
@@ -196,3 +197,61 @@ def test_nel_sleeve_f_moltiplica_il_credito_NETTO():
|
|||||||
assert SL.VRP_CFG["f"] == 1.0, (
|
assert SL.VRP_CFG["f"] == 1.0, (
|
||||||
"il sleeve gira ancora a f=1.0: se cambia, aggiornare il diario 2026-07-30 e CLAUDE.md"
|
"il sleeve gira ancora a f=1.0: se cambia, aggiornare il diario 2026-07-30 e CLAUDE.md"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# C. CAUSALITA' delle serie di contesto (debito §5.18, 09/09): `asof(ts)` non guarda avanti
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
def test_causale_rietichetta_alla_chiusura_e_non_tocca_i_valori():
|
||||||
|
"""Il feed e' etichettato all'APERTURA: la chiusura della barra T e' nota a T+cadenza.
|
||||||
|
DEVE FALLIRE SE: qualcuno "semplifica" togliendo lo spostamento, o sposta i VALORI."""
|
||||||
|
idx = pd.date_range("2026-06-01", periods=5, freq="h", tz="UTC")
|
||||||
|
S = pd.Series([10.0, 11.0, 12.0, 13.0, 14.0], index=idx)
|
||||||
|
Sc = causale(S, "1h")
|
||||||
|
assert list(Sc.index) == list(idx + pd.Timedelta(hours=1))
|
||||||
|
assert Sc.asof(idx[2]) == 11.0, "a 02:00 e' nota la chiusura della barra 01:00"
|
||||||
|
assert S.asof(idx[2]) == 12.0, "il difetto che la funzione ripara (controllo positivo)"
|
||||||
|
assert np.isnan(Sc.asof(idx[0])), "prima della prima chiusura nota non c'e' spot"
|
||||||
|
assert list(Sc.values) == list(S.values), "i VALORI non si toccano, solo le etichette"
|
||||||
|
|
||||||
|
|
||||||
|
def test_spot_series_e_causale_sul_feed_1h(monkeypatch):
|
||||||
|
"""`cblib.spot_series(a).asof(ts)` = ultima chiusura ORARIA nota a ts. Controllo positivo:
|
||||||
|
la serie grezza del feed (etichettata all'apertura) a ts da' la chiusura di ts+1h."""
|
||||||
|
import scripts.analysis.research_lab as rl
|
||||||
|
idx = pd.date_range("2026-06-01", periods=6, freq="h", tz="UTC")
|
||||||
|
epoch = pd.Timestamp("1970-01-01", tz="UTC") # NON `asi8 // 10**6`: in pandas 3 e' in `us` (D6)
|
||||||
|
px = pd.DataFrame({"timestamp": ((idx - epoch) // pd.Timedelta(milliseconds=1)).astype("int64"),
|
||||||
|
"close": [100.0, 101.0, 102.0, 103.0, 104.0, 105.0]})
|
||||||
|
monkeypatch.setattr(rl, "load_tf", lambda asset, tf: px)
|
||||||
|
cblib.spot_series.cache_clear()
|
||||||
|
try:
|
||||||
|
S = cblib.spot_series("ZZZ")
|
||||||
|
ts = idx[3] + pd.Timedelta(minutes=17) # 03:17: la barra 03:00 e' ancora aperta
|
||||||
|
assert S.asof(ts) == 102.0, "a 03:17 l'ultima chiusura nota e' quella della barra 02:00"
|
||||||
|
assert S.asof(idx[3]) == 102.0, "a 03:00 esatte la barra 02:00 e' appena chiusa"
|
||||||
|
assert S.asof(idx[5] + pd.Timedelta(hours=1)) == 105.0, "l'ultima chiusura arriva un'ora dopo"
|
||||||
|
grezza = pd.Series(px["close"].values, index=idx)
|
||||||
|
assert grezza.asof(ts) == 103.0, "il difetto riparato: la serie del feed a 03:17 da' la chiusura delle 04:00"
|
||||||
|
finally:
|
||||||
|
cblib.spot_series.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
|
def test_dvol_series_e_causale_sul_feed_giornaliero(monkeypatch, tmp_path):
|
||||||
|
"""La riga DVOL del giorno D e' la chiusura delle 23:00 di D: dentro D non e' ancora nota.
|
||||||
|
`cblib.dvol_series(a).asof(ts in D)` deve dare la chiusura di D−1."""
|
||||||
|
days = pd.date_range("2026-06-01", periods=4, freq="D", tz="UTC")
|
||||||
|
epoch = pd.Timestamp("1970-01-01", tz="UTC")
|
||||||
|
pd.DataFrame({"timestamp": ((days - epoch) // pd.Timedelta(milliseconds=1)).astype("int64"),
|
||||||
|
"close": [36.0, 37.0, 43.0, 47.0]}).to_parquet(tmp_path / "dvol_zzz.parquet")
|
||||||
|
monkeypatch.setattr(cblib, "RAW", tmp_path)
|
||||||
|
cblib.dvol_series.cache_clear()
|
||||||
|
try:
|
||||||
|
V = cblib.dvol_series("ZZZ")
|
||||||
|
ts = days[2] + pd.Timedelta(hours=10) # 03/06 10:00
|
||||||
|
assert V.asof(ts) == 37.0, "dentro il 03/06 l'ultimo DVOL noto e' la chiusura del 02/06"
|
||||||
|
assert V.asof(days[3]) == 43.0, "alle 00:00 del 04/06 e' nota la chiusura del 03/06"
|
||||||
|
assert np.isnan(V.asof(days[0] + pd.Timedelta(hours=5))), "nel primo giorno non c'e' storia"
|
||||||
|
finally:
|
||||||
|
cblib.dvol_series.cache_clear()
|
||||||
|
|||||||
@@ -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}"
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
"""cuscino_watch: il cuscino USDC e' sorvegliato; sotto zero si vende USDE, sopra la banda si ricompra.
|
||||||
|
|
||||||
|
Blindano: (a) il cuscino e' DERIVATO da config + book (P1), la stessa formula di usde_convert;
|
||||||
|
(b) i tre stati e la soglia di preavviso; (c) la transizione non si consuma se l'invio fallisce
|
||||||
|
(debito §5.2); (d) le guardie della riconversione automatica — interruttore del libro, depeg,
|
||||||
|
cadenza — e la quota di ripristino che lascia margine."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from src.live import usde as U # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _carica(nome):
|
||||||
|
sp = importlib.util.spec_from_file_location(nome, ROOT / "scripts" / "live" / f"{nome}.py")
|
||||||
|
m = importlib.util.module_from_spec(sp)
|
||||||
|
sp.loader.exec_module(m)
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
W = _carica("cuscino_watch")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------ il cuscino e' derivato, non ridichiarato ------------------------------
|
||||||
|
|
||||||
|
def test_cuscino_derivato_da_config_e_book():
|
||||||
|
cfg = json.loads((ROOT / "config" / "live.json").read_text())
|
||||||
|
from src.live import book as B
|
||||||
|
f, come = U.frazione_cuscino()
|
||||||
|
assert abs(f - 2 * float(B.WEIGHT) * float(cfg["disaster_sl_pct"])) < 1e-12
|
||||||
|
usd, _ = U.cuscino_richiesto_usd(1000.0)
|
||||||
|
assert abs(usd - 1000.0 * f) < 1e-9
|
||||||
|
assert "SL" in come
|
||||||
|
|
||||||
|
|
||||||
|
def test_usde_convert_usa_la_stessa_formula():
|
||||||
|
C = _carica("usde_convert")
|
||||||
|
assert C.cuscino_richiesto_usd(4469.46) == U.cuscino_richiesto_usd(4469.46)
|
||||||
|
|
||||||
|
|
||||||
|
def test_quota_ripristino_lascia_margine_sul_cuscino():
|
||||||
|
f, _ = U.frazione_cuscino()
|
||||||
|
q0, q10 = U.quota_ripristino(0.0), U.quota_ripristino(0.10)
|
||||||
|
assert abs(q0 - (1 - f)) < 1e-12 # a margine zero e' il complemento del cuscino
|
||||||
|
assert q10 < q0 # col margine si vende UN PO' di piu'
|
||||||
|
# dopo la riconversione a q10 lo slack e' esattamente il 10% del cuscino
|
||||||
|
E = 4000.0
|
||||||
|
usdc_dopo = E * (1 - q10)
|
||||||
|
assert abs((usdc_dopo - E * f) - 0.10 * E * f) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------ i quattro stati (issue #7) ------------------------------
|
||||||
|
|
||||||
|
B = dict(preavviso=0.10, margine=0.20, riacquisto=0.40)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stati_scoperto_preavviso_ok_eccedente():
|
||||||
|
assert W.giudica(1199.0, 4000.0, 1200.0, B)["stato"] == "SCOPERTO"
|
||||||
|
g = W.giudica(1300.0, 4000.0, 1200.0, B)
|
||||||
|
assert g["stato"] == "PREAVVISO" and abs(g["slack"] - 100.0) < 1e-9 and g["preavviso_usd"] == 120.0
|
||||||
|
assert W.giudica(1500.0, 4000.0, 1200.0, B)["stato"] == "OK" # slack 300: dentro [120, 480]
|
||||||
|
assert W.giudica(1680.0, 4000.0, 1200.0, B)["stato"] == "OK" # slack 480: sul bordo, non compra
|
||||||
|
g = W.giudica(1700.0, 4000.0, 1200.0, B)
|
||||||
|
assert g["stato"] == "ECCEDENTE" and g["riacquisto_usd"] == 480.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_le_bande_vengono_dalla_config_e_sono_ordinate():
|
||||||
|
b = U.bande_cuscino()
|
||||||
|
assert 0 < b["preavviso"] < b["margine"] < b["riacquisto"]
|
||||||
|
assert W.giudica(1500.0, 4000.0, 1200.0)["stato"] == W.giudica(1500.0, 4000.0, 1200.0, b)["stato"]
|
||||||
|
import pytest
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
U.bande_cuscino({"cuscino_preavviso_frac": 0.3, "cuscino_margine_frac": 0.2, "cuscino_riacquisto_frac": 0.4})
|
||||||
|
|
||||||
|
|
||||||
|
def test_isteresi_vendita_e_acquisto_convergono_allo_stesso_bersaglio():
|
||||||
|
"""Dopo una vendita da SCOPERTO o un acquisto da ECCEDENTE lo slack e' margine x cuscino: OK,
|
||||||
|
a distanza da entrambe le soglie. Un giro non ne innesca un altro."""
|
||||||
|
f, _ = U.frazione_cuscino(); b = U.bande_cuscino()
|
||||||
|
E = 4450.0; cusc = E * f
|
||||||
|
q = U.quota_ripristino(b["margine"])
|
||||||
|
usdc_dopo = E * (1 - q)
|
||||||
|
g = W.giudica(usdc_dopo, E, cusc, b)
|
||||||
|
assert g["stato"] == "OK"
|
||||||
|
assert g["slack"] > g["preavviso_usd"] * 1.5 and g["slack"] < g["riacquisto_usd"] / 1.5
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_bonifico_fa_scattare_il_riacquisto_da_solo():
|
||||||
|
"""Conto a bersaglio (slack = 0,20 x cuscino), bonifico D in USDC: slack +0,7 D. Con D = 12%
|
||||||
|
dell'equity si supera 0,40 x cuscino e si ricompra; con D = 3% no (resta in USDC)."""
|
||||||
|
f, _ = U.frazione_cuscino(); b = U.bande_cuscino()
|
||||||
|
E = 4450.0; cusc = E * f; usdc = cusc * (1 + b["margine"])
|
||||||
|
for D, atteso in ((0.03 * E, "OK"), (0.12 * E, "ECCEDENTE")):
|
||||||
|
E2 = E + D; cusc2 = E2 * f
|
||||||
|
assert W.giudica(usdc + D, E2, cusc2, b)["stato"] == atteso, D
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_vedo_e_blind_non_ok():
|
||||||
|
assert W.giudica(None, 4000.0, 1200.0, B)["stato"] == "BLIND"
|
||||||
|
assert W.giudica(1500.0, None, None, B)["stato"] == "BLIND"
|
||||||
|
|
||||||
|
|
||||||
|
def test_gli_stati_che_convertono_sono_due_e_hanno_un_verso():
|
||||||
|
assert W.AZIONE == {"SCOPERTO": "vendita", "ECCEDENTE": "acquisto"}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------ transizione e trasporto ------------------------------
|
||||||
|
|
||||||
|
def test_ok_non_allerta_mai():
|
||||||
|
assert not W.transizione(None, "OK") and not W.transizione({"stato": "SCOPERTO"}, "OK")
|
||||||
|
|
||||||
|
|
||||||
|
def test_transizione_alla_prima_e_al_cambio_di_stato():
|
||||||
|
assert W.transizione(None, "PREAVVISO")
|
||||||
|
assert W.transizione({"stato": "OK", "allertato": None}, "ECCEDENTE")
|
||||||
|
assert W.transizione({"stato": "OK", "allertato": None}, "PREAVVISO")
|
||||||
|
assert W.transizione({"stato": "PREAVVISO", "allertato": True}, "SCOPERTO")
|
||||||
|
assert not W.transizione({"stato": "PREAVVISO", "allertato": True}, "PREAVVISO")
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_invio_fallito_non_consuma_la_transizione():
|
||||||
|
"""Debito §5.2: il marcatore 'gia' detto' e' l'esito dell'invio."""
|
||||||
|
assert W.transizione({"stato": "SCOPERTO", "allertato": False}, "SCOPERTO")
|
||||||
|
assert W.transizione({"stato": "SCOPERTO", "allertato": None}, "SCOPERTO")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------ guardie della riconversione ------------------------------
|
||||||
|
|
||||||
|
def test_disarmare_il_libro_disarma_la_riconversione():
|
||||||
|
ok, m = W.puo_riconvertire([], 0, False, 1.0, 0.99)
|
||||||
|
assert not ok and "execution_enabled" in m
|
||||||
|
|
||||||
|
|
||||||
|
def test_sotto_depeg_non_si_vende_al_buio():
|
||||||
|
ok, m = W.puo_riconvertire([], 0, True, 0.985, 0.99)
|
||||||
|
assert not ok and "depeg" in m
|
||||||
|
ok, m = W.puo_riconvertire([], 0, True, None, 0.99)
|
||||||
|
assert not ok and "leggibile" in m
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_tentativo_ogni_sei_ore():
|
||||||
|
now = 1_789_000_000_000
|
||||||
|
prev = [{"ts": now - 2 * 3_600_000, "riconversione": {"tentata": True}}]
|
||||||
|
ok, m = W.puo_riconvertire(prev, now, True, 1.0, 0.99)
|
||||||
|
assert not ok and "non si insiste" in m
|
||||||
|
prev = [{"ts": now - 7 * 3_600_000, "riconversione": {"tentata": True}}]
|
||||||
|
assert W.puo_riconvertire(prev, now, True, 1.0, 0.99)[0]
|
||||||
|
# un record SENZA riconversione non conta come tentativo
|
||||||
|
assert W.puo_riconvertire([{"ts": now - 1000, "riconversione": None}], now, True, 1.0, 0.99)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_riconverti_lancia_lo_script_vero_con_esegui(monkeypatch):
|
||||||
|
chiamate = []
|
||||||
|
|
||||||
|
class R:
|
||||||
|
returncode, stdout, stderr = 0, " → piano valido\n ESITO: 10 USDE\n", ""
|
||||||
|
|
||||||
|
def finto_run(cmd, **kw):
|
||||||
|
chiamate.append(cmd)
|
||||||
|
return R()
|
||||||
|
|
||||||
|
monkeypatch.setattr(W.subprocess, "run", finto_run)
|
||||||
|
e = W.riconverti(0.67)
|
||||||
|
assert e["ok"] and chiamate and chiamate[0][1].endswith("usde_convert.py")
|
||||||
|
assert "--esegui" in chiamate[0] and "0.6700" in chiamate[0]
|
||||||
|
assert e["coda"][-1].startswith(" ESITO")
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_monitor_e_registrato_in_monitor_health():
|
||||||
|
from src.live.monitor_health import MONITORS
|
||||||
|
spec = [m for m in MONITORS if m.name == "cuscino_watch"]
|
||||||
|
assert spec and spec[0].cadence_h == 1.0 and spec[0].open_labeled is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_non_tentativo_non_consuma_il_budget():
|
||||||
|
"""Prezzo illeggibile, --secco, interruttore spento: `tentata=False` non blocca il giro dopo."""
|
||||||
|
now = 1_789_000_000_000
|
||||||
|
prev = [{"ts": now - 3_600_000, "riconversione": {"tentata": False, "motivo": "--secco"}}]
|
||||||
|
assert W.puo_riconvertire(prev, now, True, 1.0, 0.99)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_dopo_il_ripristino_lo_stato_e_OK_non_preavviso():
|
||||||
|
"""Il margine di ripristino deve superare la soglia di preavviso, o ogni 🚨 e' seguito da un ⚠️
|
||||||
|
per costruzione (P14). Include il floor di 1 USDE e la tolleranza di quota di usde_convert."""
|
||||||
|
b = U.bande_cuscino()
|
||||||
|
assert b["margine"] > b["preavviso"]
|
||||||
|
f, _ = U.frazione_cuscino()
|
||||||
|
E = 4450.0
|
||||||
|
C = _carica("usde_convert")
|
||||||
|
q = U.quota_ripristino(b["margine"]) + C.TOLL # usde_convert si ferma entro TOLL (importato, P1)
|
||||||
|
usdc_dopo = E * (1 - q) - 1.0 # e vende fino a 1 USDE in meno (passo del venue)
|
||||||
|
assert W.giudica(usdc_dopo, E, E * f, b)["stato"] == "OK"
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_sink_telegram_fa_escape_di_minore_e_ampersand(monkeypatch):
|
||||||
|
"""Telegram in parse_mode HTML rifiuta un '<' nudo: il primo giro vero (13:53Z del 10/09) ha
|
||||||
|
perso l'allerta PREAVVISO per «(< 10% del cuscino)». L'escape sta nel SINK (`notify`), non nei
|
||||||
|
45 chiamanti — qui si cattura il corpo della POST e si verifica che il '<' sia diventato <."""
|
||||||
|
from src.live import notifier as N
|
||||||
|
catturato = {}
|
||||||
|
|
||||||
|
def finto_urlopen(url, data, timeout=10):
|
||||||
|
import urllib.parse
|
||||||
|
catturato.update(urllib.parse.parse_qs(data.decode()))
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr(N, "_cfg", lambda: ("tok", "chat"))
|
||||||
|
monkeypatch.setattr(N.urllib.request, "urlopen", finto_urlopen)
|
||||||
|
assert N.notify("⚠️ prova <x>", {"slack": "$+30 (< 10% del cuscino)", "a&b": "c"})
|
||||||
|
testo = catturato["text"][0]
|
||||||
|
assert "(< 10%" in testo and "<x>" in testo and "a&b" in testo
|
||||||
|
assert testo.startswith("<b>") # il grassetto del titolo resta HTML vero
|
||||||
|
|
||||||
|
|
||||||
|
def test_bande_fuori_ordine_danno_BLIND_con_motivo_non_un_crash():
|
||||||
|
import pytest
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
U.bande_cuscino({"cuscino_preavviso_frac": 0.5, "cuscino_margine_frac": 0.2, "cuscino_riacquisto_frac": 0.4})
|
||||||
|
monkey = {"cuscino_preavviso_frac": 0.5, "cuscino_margine_frac": 0.2, "cuscino_riacquisto_frac": 0.4}
|
||||||
|
orig = U.cfg
|
||||||
|
try:
|
||||||
|
U.cfg = lambda: {**orig(), **monkey}
|
||||||
|
g = W.giudica(1500.0, 4000.0, 1200.0) # bande=None -> config rotta
|
||||||
|
finally:
|
||||||
|
U.cfg = orig
|
||||||
|
assert g["stato"] == "BLIND" and "fuori ordine" in g["motivo"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_da_eccedente_un_fallimento_si_ritenta_una_volta_al_giorno():
|
||||||
|
"""USDC fermo non e' un rischio: un guasto persistente (tetto del venue, book illeggibile) non
|
||||||
|
deve diventare 4 Telegram al giorno per sempre. Da SCOPERTO il ritmo resta 6h."""
|
||||||
|
now = 1_789_000_000_000
|
||||||
|
fallito = [{"ts": now - 8 * 3_600_000, "riconversione": {"tentata": True, "ok": False}}]
|
||||||
|
ok, m = W.puo_riconvertire(fallito, now, True, 1.0, 0.99, stato="ECCEDENTE")
|
||||||
|
assert not ok and "una volta al giorno" in m
|
||||||
|
assert W.puo_riconvertire(fallito, now, True, 1.0, 0.99, stato="SCOPERTO")[0]
|
||||||
|
riuscito = [{"ts": now - 8 * 3_600_000, "riconversione": {"tentata": True, "ok": True}}]
|
||||||
|
assert W.puo_riconvertire(riuscito, now, True, 1.0, 0.99, stato="ECCEDENTE")[0]
|
||||||
|
tardi = [{"ts": now - 25 * 3_600_000, "riconversione": {"tentata": True, "ok": False}}]
|
||||||
|
assert W.puo_riconvertire(tardi, now, True, 1.0, 0.99, stato="ECCEDENTE")[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_motivi_delle_guardie_non_hanno_minore_nudo():
|
||||||
|
"""Difesa in profondita' oltre al sink: i motivi finiscono nel campo `azione` delle allerte."""
|
||||||
|
now = 1_789_000_000_000
|
||||||
|
casi = [W.puo_riconvertire([], now, False, 1.0, 0.99), W.puo_riconvertire([], now, True, None, 0.99),
|
||||||
|
W.puo_riconvertire([], now, True, 0.98, 0.99),
|
||||||
|
W.puo_riconvertire([{"ts": now - 1000, "riconversione": {"tentata": True, "ok": True}}], now, True, 1.0, 0.99)]
|
||||||
|
assert all("<" not in m for _, m in casi)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cron_installato_e_dichiarato_fuori_dal_tondo_e_dopo_il_book():
|
||||||
|
"""Legge la crontab VIVA (come test_book_cadenza), non un commento nello .sh (P1)."""
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
s = (ROOT / "scripts" / "cron_cuscino.sh").read_text()
|
||||||
|
assert "cuscino_watch.py --quiet" in s
|
||||||
|
if not shutil.which("crontab"):
|
||||||
|
return # SALTATO: distinto da 'assente'
|
||||||
|
out = subprocess.run(["crontab", "-l"], capture_output=True, text=True, timeout=10)
|
||||||
|
if out.returncode != 0 or "PythagorasGoal" not in out.stdout:
|
||||||
|
return # altra macchina / altro utente
|
||||||
|
righe = [ln for ln in out.stdout.splitlines() if "cron_cuscino.sh" in ln and not ln.startswith("#")]
|
||||||
|
assert len(righe) == 1, "cron_cuscino.sh deve essere installato UNA volta"
|
||||||
|
minuto, ora = righe[0].split()[:2]
|
||||||
|
assert minuto == "53" and ora == "*" # orario, dopo il book (:47)
|
||||||
@@ -568,3 +568,205 @@ def test_twr_riproduce_il_numero_del_diario_01_09(db, monkeypatch):
|
|||||||
r = J.rendimento_twr(db, "2026-09-01T23:59:59+00:00")
|
r = J.rendimento_twr(db, "2026-09-01T23:59:59+00:00")
|
||||||
assert round(100 * r["twr"], 2) == 10.80
|
assert round(100 * r["twr"], 2) == 10.80
|
||||||
assert r["certi"] == pytest.approx(1399.39)
|
assert r["certi"] == pytest.approx(1399.39)
|
||||||
|
|
||||||
|
|
||||||
|
def test_twr_a_base_zero_e_None_anche_sul_trading(db, monkeypatch):
|
||||||
|
# revisione 02/09: a base zero `movimenti_capitale` non vede il salto 0 -> X del primo
|
||||||
|
# versamento, quindi `trading = e1-e0-certi` varrebbe l'intero conto. Tre stati, non due.
|
||||||
|
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
|
||||||
|
_serie_equity(db, [("2026-08-24T09:47:01+00:00", 0.0),
|
||||||
|
("2026-08-24T10:47:01+00:00", 598.06),
|
||||||
|
("2026-08-25T10:47:01+00:00", 610.0)])
|
||||||
|
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
|
||||||
|
assert r["twr"] is None and r["trading"] is None and "base" in r["motivo"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_twr_due_movimenti_consecutivi_non_producono_segmenti_a_lunghezza_zero(db, monkeypatch):
|
||||||
|
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
|
||||||
|
_serie_equity(db, [("2026-08-24T10:47:01+00:00", 600.0),
|
||||||
|
("2026-08-24T11:47:01+00:00", 2000.0), # movimento nel PRIMO intervallo
|
||||||
|
("2026-08-24T12:47:01+00:00", 3400.0), # movimento consecutivo
|
||||||
|
("2026-08-25T10:47:01+00:00", 3434.0)]) # +1% di trading
|
||||||
|
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
|
||||||
|
assert len(r["segmenti"]) == 1 and r["segmenti"][0]["ret"] == pytest.approx(0.01)
|
||||||
|
assert r["twr"] == pytest.approx(0.01)
|
||||||
|
assert all(s["da"] != s["a"] for s in r["segmenti"])
|
||||||
|
assert r["certi"] == pytest.approx(1400.0 + 1400.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_twr_senza_tempo_a_mercato_e_zero_non_None(db, monkeypatch):
|
||||||
|
# due letture, la seconda e' un versamento: nessun segmento, rendimento 0, dichiarato numero
|
||||||
|
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
|
||||||
|
_serie_equity(db, [("2026-08-24T10:47:01+00:00", 600.0),
|
||||||
|
("2026-08-24T11:47:01+00:00", 2000.0)])
|
||||||
|
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
|
||||||
|
assert r["twr"] == 0.0 and r["segmenti"] == [] and r["trading"] == pytest.approx(0.0)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------- 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"
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------- movimenti DICHIARATI dall'operatore (06/09)
|
||||||
|
# Il versamento di prova del 2026-08-25 08:00Z (~$25 su $647, +3,3%) e' restato 12 giorni nel P&L:
|
||||||
|
# sotto EQUITY_JUMP_ALERT il rilevatore non puo' vederlo, e abbassare la soglia farebbe di ogni
|
||||||
|
# ora di mercato un candidato. L'unica fonte che sa l'importo e' l'operatore.
|
||||||
|
|
||||||
|
def _dichiara(monkeypatch, tmp_path, righe):
|
||||||
|
f = tmp_path / "mov.jsonl"
|
||||||
|
f.write_text("\n".join(json.dumps(r) if isinstance(r, dict) else r for r in righe) + "\n")
|
||||||
|
monkeypatch.setattr(J, "MOVIMENTI_DICHIARATI", f)
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_versamento_dichiarato_sotto_soglia_viene_scorporato(db, monkeypatch, tmp_path):
|
||||||
|
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
|
||||||
|
_serie_equity(db, [("2026-08-25T07:07:01+00:00", 646.77),
|
||||||
|
("2026-08-25T08:07:01+00:00", 667.88), # +3,3%: sotto soglia
|
||||||
|
("2026-08-25T09:07:01+00:00", 667.68)])
|
||||||
|
_dichiara(monkeypatch, tmp_path, [dict(ts_utc="2026-08-25T08:00:00+00:00", delta_usd=24.9,
|
||||||
|
banda_usd=0.5, nota="versamento di prova",
|
||||||
|
dichiarato_il="2026-09-06")])
|
||||||
|
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
|
||||||
|
ev = r["eventi"]
|
||||||
|
assert len(ev) == 1 and ev[0]["classe"] == "movimento" and ev[0]["fonte"] == "dichiarato"
|
||||||
|
assert ev[0]["delta"] == pytest.approx(24.9) # l'IMPORTO dichiarato, non il salto
|
||||||
|
assert ev[0]["dopo"] == pytest.approx(646.77 + 24.9) # il mercato dell'ora resta nel rendimento
|
||||||
|
assert r["certi"] == pytest.approx(24.9)
|
||||||
|
assert r["trading"] == pytest.approx(667.68 - 646.77 - 24.9)
|
||||||
|
# TWR: segmento 1 di lunghezza zero (una sola lettura prima), segmento 2 da 671,67 a 667,68
|
||||||
|
assert r["twr"] == pytest.approx(667.68 / (646.77 + 24.9) - 1.0)
|
||||||
|
assert "versamento di prova" in ev[0]["dichiarazione"] and "±$0.50" in ev[0]["dichiarazione"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_dichiarato_e_rilevato_insieme_vince_l_importo_dichiarato(db, monkeypatch, tmp_path):
|
||||||
|
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
|
||||||
|
_serie_equity(db, [("2026-08-25T10:47:01+00:00", 667.49),
|
||||||
|
("2026-08-25T11:47:01+00:00", 2066.88),
|
||||||
|
("2026-08-25T12:47:01+00:00", 2064.56)])
|
||||||
|
_dichiara(monkeypatch, tmp_path, [dict(ts_utc="2026-08-25T11:20:00+00:00", delta_usd=1399.39,
|
||||||
|
nota="bonifico")])
|
||||||
|
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
|
||||||
|
assert len(r["eventi"]) == 1 # UN evento, non due
|
||||||
|
e = r["eventi"][0]
|
||||||
|
assert e["fonte"] == "dichiarato+rilevato" and e["delta"] == pytest.approx(1399.39)
|
||||||
|
assert e["mercato_max_pct"] is not None # la misura di mercato resta
|
||||||
|
assert r["certi"] == pytest.approx(1399.39)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dichiarato_fuori_dalle_letture_non_entra_ma_si_vede(db, monkeypatch, tmp_path):
|
||||||
|
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", 601.0)])
|
||||||
|
_dichiara(monkeypatch, tmp_path, [dict(ts_utc="2026-08-20T08:00:00+00:00", delta_usd=25.0)])
|
||||||
|
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
|
||||||
|
assert r["eventi"] == [] and r["certi"] == 0.0
|
||||||
|
assert r["avvisi"] and "non applicato" in r["avvisi"][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_una_riga_rotta_non_azzera_le_altre_e_non_sparisce(db, monkeypatch, tmp_path):
|
||||||
|
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
|
||||||
|
_serie_equity(db, [("2026-08-25T07:07:01+00:00", 646.77),
|
||||||
|
("2026-08-25T08:07:01+00:00", 667.88)])
|
||||||
|
_dichiara(monkeypatch, tmp_path, ["{non json", dict(ts_utc="2026-08-25T08:00:00", delta_usd=1.0),
|
||||||
|
dict(ts_utc="2026-08-25T08:00:00+00:00", delta_usd=24.9)])
|
||||||
|
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
|
||||||
|
assert r["certi"] == pytest.approx(24.9)
|
||||||
|
assert len(r["avvisi"]) == 2 and "senza fuso" in r["avvisi"][1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_senza_file_dei_dichiarati_niente_cambia(db, monkeypatch):
|
||||||
|
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", 612.0)])
|
||||||
|
assert not J.MOVIMENTI_DICHIARATI.exists() # conftest lo isola
|
||||||
|
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
|
||||||
|
assert r["eventi"] == [] and r["avvisi"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_nota_di_giornale_dice_che_e_dichiarato(db, monkeypatch, tmp_path):
|
||||||
|
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
|
||||||
|
_serie_equity(db, [("2026-08-25T07:07:01+00:00", 646.77),
|
||||||
|
("2026-08-25T08:07:01+00:00", 667.88)])
|
||||||
|
_dichiara(monkeypatch, tmp_path, [dict(ts_utc="2026-08-25T08:00:00+00:00", delta_usd=24.9,
|
||||||
|
nota="versamento di prova")])
|
||||||
|
p = J.pnl_giorno(db, date(2026, 8, 25))
|
||||||
|
assert p["mov_giorno"] == pytest.approx(24.9)
|
||||||
|
righe = J.lettura(db, _voce(pnl=p, giorno="2026-08-25"))
|
||||||
|
mv = [r for r in righe if r["id"] == "movimento"]
|
||||||
|
assert mv and "dichiarato dall'operatore" in mv[0]["testo"] and "versamento di prova" in mv[0]["testo"]
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
"""GATE PREVDAY-01 cablato nel monitor (issue #4): kill, veto d'integrita', data.
|
||||||
|
|
||||||
|
Prima del 10/09 il gate esisteva solo nel testo (RESULTS-0822 §54) e la revisione settimanale lo
|
||||||
|
ha letto come «senza data». Qui si blinda che: (a) le costanti sono quelle scritte il 23/08;
|
||||||
|
(b) il kill non e' leggibile sotto 180 giorni attivi e scatta solo sotto -0,50; (c) il veto
|
||||||
|
blocca sotto l'80% di barre ricostruibili e su una crescita MATERIALE delle divergenze — non su
|
||||||
|
una barra sola (P14); (d) la ricostruzione riproduce una serie generata con la stessa aritmetica
|
||||||
|
di `advance`; (e) la lente del kill e' GIORNALIERA."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
sp = importlib.util.spec_from_file_location("paper_prevday", ROOT / "scripts" / "live" / "paper_prevday.py")
|
||||||
|
PP = importlib.util.module_from_spec(sp)
|
||||||
|
sp.loader.exec_module(PP)
|
||||||
|
|
||||||
|
|
||||||
|
def test_le_costanti_sono_quelle_del_gate_scritto_il_23_08():
|
||||||
|
assert PP.GATE_DATE == "2027-06-21"
|
||||||
|
assert PP.KILL_SHARPE == -0.50 and PP.KILL_MIN_ACTIVE_DAYS == 180
|
||||||
|
assert PP.MIN_RECON_FRAC == 0.80
|
||||||
|
# e CLAUDE.md §4 cita la stessa data (era assente fino al 10/09)
|
||||||
|
assert "2027-06-21" in (ROOT / "CLAUDE.md").read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def _serie(n_giorni: int, mu_h: float, sd_h: float, seed: int = 0, pos: float = 0.3) -> pd.DataFrame:
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
n = n_giorni * 24
|
||||||
|
ts = 1_782_000_000_000 + np.arange(n) * 3_600_000
|
||||||
|
x = rng.normal(mu_h, sd_h, n)
|
||||||
|
return pd.DataFrame(dict(ts=ts, dt=pd.to_datetime(ts, unit="ms", utc=True), net_modeled=x,
|
||||||
|
net_real=x, pos_btc=pos, pos_eth=pos))
|
||||||
|
|
||||||
|
|
||||||
|
def test_kill_non_leggibile_sotto_180_giorni_attivi():
|
||||||
|
r = _serie(81, -0.001, 0.004)
|
||||||
|
sh, n = PP.sharpe_giornaliero(r)
|
||||||
|
assert n == 81 and sh < PP.KILL_SHARPE # Sharpe pessimo...
|
||||||
|
stato, motivo = PP.kill_check(sh, PP.giorni_attivi(r), pd.Timestamp("2026-09-10", tz="UTC"))
|
||||||
|
assert stato == "NON_MATURO" and "99" in motivo # ...ma il kill non e' leggibile
|
||||||
|
|
||||||
|
|
||||||
|
def test_kill_scatta_sopra_180_giorni_solo_sotto_la_soglia():
|
||||||
|
r_male = _serie(200, -0.001, 0.004)
|
||||||
|
sh, _ = PP.sharpe_giornaliero(r_male)
|
||||||
|
assert PP.kill_check(sh, PP.giorni_attivi(r_male), pd.Timestamp("2027-01-01", tz="UTC"))[0] == "KILL"
|
||||||
|
oggi = pd.Timestamp("2027-01-01", tz="UTC")
|
||||||
|
assert PP.kill_check(-0.49, 200, oggi)[0] == "SUPERATO" # la soglia e' STRETTA: -0,49 passa
|
||||||
|
assert PP.kill_check(-0.51, 200, oggi)[0] == "KILL"
|
||||||
|
assert PP.kill_check(float("nan"), 200, oggi)[0] == "NON_MISURABILE"
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_giorni_flat_non_contano_come_attivi():
|
||||||
|
r = _serie(200, 0.0, 0.004)
|
||||||
|
r.loc[r.index[: 50 * 24], ["pos_btc", "pos_eth"]] = 0.0
|
||||||
|
assert PP.giorni_attivi(r) == 150
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_lente_del_kill_e_giornaliera_non_oraria():
|
||||||
|
"""§54: sulla lente oraria lo stesso forward vale +2,04 contro +1,56. Con autocorrelazione
|
||||||
|
positiva intragiornaliera la lente oraria SOVRASTIMA: il kill usa la giornaliera."""
|
||||||
|
rng = np.random.default_rng(3)
|
||||||
|
n = 120 * 24
|
||||||
|
base = np.repeat(rng.normal(0.0004, 0.004, 120), 24) # shock comune al giorno
|
||||||
|
x = base + rng.normal(0, 0.0005, n)
|
||||||
|
ts = 1_782_000_000_000 + np.arange(n) * 3_600_000
|
||||||
|
r = pd.DataFrame(dict(ts=ts, dt=pd.to_datetime(ts, unit="ms", utc=True), net_modeled=x,
|
||||||
|
net_real=x, pos_btc=0.3, pos_eth=0.3))
|
||||||
|
sh_g, _ = PP.sharpe_giornaliero(r)
|
||||||
|
assert PP._sharpe_orario(r) > sh_g * 2 # la lente oraria gonfia; il kill non la usa
|
||||||
|
|
||||||
|
|
||||||
|
def test_veto_sotto_80_pct_ricostruibili():
|
||||||
|
ok, m = PP.veto_check(0.79, 0.5, 0.5, 10)
|
||||||
|
assert not ok and "79.0%" in m
|
||||||
|
assert PP.veto_check(0.80, 0.5, 0.5, 10)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_veto_su_crescita_materiale_non_su_una_barra(monkeypatch):
|
||||||
|
"""Il 10/09 la serie reale aveva UNA barra divergente su 1943 (26/08 00:00, il giorno del fix
|
||||||
|
di advance()) e la prima stesura del veto la leggeva come 'crescita' (0,03/g contro 0,01/g)."""
|
||||||
|
assert PP.veto_check(0.999, 0.012, 0.032, 1)[0] # 1 barra: non e' crescita
|
||||||
|
ok, m = PP.veto_check(0.95, 0.10, 0.50, 12) # 12 recenti, tasso 5x: crescita
|
||||||
|
assert not ok and "crescita" in m
|
||||||
|
assert PP.veto_check(0.95, 0.30, 0.50, 12)[0] # tasso < 2x: no
|
||||||
|
|
||||||
|
|
||||||
|
def test_ricostruzione_riproduce_una_serie_generata_come_advance(monkeypatch):
|
||||||
|
"""Feed sintetico, target congelato deterministico (monkeypatch su prevday_target): la serie
|
||||||
|
registrata con l'aritmetica di advance si ricostruisce al 100%; una barra manomessa no."""
|
||||||
|
n = 24 * 10
|
||||||
|
ts = 1_782_000_000_000 + np.arange(n) * 3_600_000
|
||||||
|
rng = np.random.default_rng(7)
|
||||||
|
dfs = {}
|
||||||
|
for a, p0 in (("BTC", 60000.0), ("ETH", 2000.0)):
|
||||||
|
close = p0 * np.cumprod(1 + rng.normal(0, 0.003, n))
|
||||||
|
dfs[a] = pd.DataFrame(dict(timestamp=ts, datetime=pd.to_datetime(ts, unit="ms", utc=True), close=close))
|
||||||
|
tgt = {a: np.where((np.arange(n) // 24) % 2 == 0, 0.3, -0.3) for a in dfs}
|
||||||
|
monkeypatch.setattr(PP, "prevday_target",
|
||||||
|
lambda df: tgt["BTC"] if float(df["close"].iloc[0]) > 10000 else tgt["ETH"])
|
||||||
|
start = int(ts[23])
|
||||||
|
# registrazione con la stessa aritmetica di advance (ribilanciamento continuo)
|
||||||
|
pos = {a: float(tgt[a][23]) for a in dfs}
|
||||||
|
rows = []
|
||||||
|
for i in range(24, n):
|
||||||
|
net = 0.0
|
||||||
|
for a in dfs:
|
||||||
|
c = dfs[a]["close"].values
|
||||||
|
rr = c[i] / c[i - 1] - 1
|
||||||
|
net += PP.WEIGHT * (pos[a] * rr - PP.FEE_SIDE * abs(tgt[a][i] - pos[a]))
|
||||||
|
pos[a] = float(tgt[a][i])
|
||||||
|
rows.append(dict(ts=int(ts[i]), net_modeled=round(net, 6), pos_btc=pos["BTC"], pos_eth=pos["ETH"]))
|
||||||
|
r = pd.DataFrame(rows); r["dt"] = pd.to_datetime(r["ts"], unit="ms", utc=True)
|
||||||
|
rc = PP.ricostruzione(r, dfs, start)
|
||||||
|
assert rc["ricostruibili"] == rc["n"] == n - 24 and rc["assenti_nel_feed"] == 0
|
||||||
|
r2 = r.copy(); r2.loc[r2.index[-1], "net_modeled"] += 0.01
|
||||||
|
rc2 = PP.ricostruzione(r2, dfs, start)
|
||||||
|
assert rc2["ricostruibili"] == rc2["n"] - 1 and rc2["div_recenti"] == 1
|
||||||
@@ -0,0 +1,512 @@
|
|||||||
|
"""Test di `scripts/research/r0909_crash_catturato.py` (2026-09-09) — il `f` di stress su quote vere.
|
||||||
|
|
||||||
|
Questo script porta l'UNICA condizione che riapre la decisione vincolante del 19/06 ("niente
|
||||||
|
short-vol da modello in deploy"): un `f` di stress reale misurato su un crash catturato. Un
|
||||||
|
numero con quel peso ha bisogno di guardie sul MECCANISMO, non solo sull'esito:
|
||||||
|
|
||||||
|
* il prezzatore vettorizzato dev'essere *lo stesso* di `cblib.bs_put`, elemento per elemento e
|
||||||
|
nei casi degeneri (T≤0, sig=0 → intrinseco): se diverge, il denominatore di ogni `f` e' un
|
||||||
|
altro modello e il rapporto non significa piu' niente;
|
||||||
|
* **lo spot dev'essere CAUSALE.** Il feed 1h e' etichettato all'APERTURA: la barra 20:00 chiude
|
||||||
|
alle 21:00, quindi `S.asof(ts)` guardava un'ora AVANTI. `cblib.causale` sposta l'indice di
|
||||||
|
+1h. E' un look-ahead di un'ora dentro una misura di stress — piccolo e letale;
|
||||||
|
* **il costo di chiusura si TAGLIA alla larghezza.** Uno spread a rischio definito non costa
|
||||||
|
mai piu' della larghezza per chiudersi: si lascia regolare. Senza il taglio il "peggior MTM"
|
||||||
|
era un artefatto di quote (2,6× il modello; col taglio 1,08);
|
||||||
|
* la geometria fuori tolleranza va MARCATA, non aggiustata: `pick_legs` prende sempre la delta
|
||||||
|
piu' vicina, quindi senza `in_geom` una griglia rada produrrebbe silenziosamente un'altra
|
||||||
|
struttura (e' cio' che ha reso BTC non misurabile);
|
||||||
|
* un `f` fra un reale negativo e un modello positivo NON e' un `f`: dev'essere NaN (P12).
|
||||||
|
|
||||||
|
I test sintetici costruiscono quote a mano, dove ogni numero atteso si calcola a matita. Il test
|
||||||
|
d'integrazione (~11 s) riproduce la misura pubblicata su ETH.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||||
|
|
||||||
|
import cblib as CB # noqa: E402
|
||||||
|
import r0909_crash_catturato as C # noqa: E402
|
||||||
|
|
||||||
|
INS_S = "ETH-19JUN26-1900-P"
|
||||||
|
INS_L = "ETH-19JUN26-1600-P"
|
||||||
|
K_S, K_L = 1900.0, 1600.0
|
||||||
|
LARGHEZZA = K_S - K_L
|
||||||
|
TS0 = pd.Timestamp("2026-06-05 00:00", tz="UTC")
|
||||||
|
ORE = pd.date_range(TS0, periods=4, freq="h")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ impalcature sintetiche
|
||||||
|
|
||||||
|
def _serie_spot(entrata: float = 2000.0, scadenza: float | None = None) -> pd.Series:
|
||||||
|
"""Spot orario dal 01/06 a EXP: `entrata` fino a 12h dopo l'ingresso, poi `scadenza`
|
||||||
|
(cosi' `S.asof(EXP)` e' pilotabile senza toccare le quote della finestra d'ingresso)."""
|
||||||
|
idx = pd.date_range(pd.Timestamp("2026-06-01", tz="UTC"), C.EXP, freq="h")
|
||||||
|
fine = TS0 + pd.Timedelta(hours=12)
|
||||||
|
v = np.where(idx <= fine, entrata, entrata if scadenza is None else scadenza)
|
||||||
|
return pd.Series(v.astype(float), index=idx)
|
||||||
|
|
||||||
|
|
||||||
|
def _serie_dvol(dvol: float = 50.0) -> pd.Series:
|
||||||
|
idx = pd.date_range(pd.Timestamp("2026-06-01", tz="UTC"), C.EXP, freq="h")
|
||||||
|
return pd.Series(float(dvol), index=idx)
|
||||||
|
|
||||||
|
|
||||||
|
def _seg(bid_s=0.030, ask_s=0.032, bid_l=0.010, ask_l=0.012,
|
||||||
|
ore=ORE, salta_lunga=()) -> pd.DataFrame:
|
||||||
|
"""Tabella di quote (ts × strumento). `salta_lunga` toglie la gamba LUNGA a quelle ore:
|
||||||
|
e' il caso "una gamba sola", che non e' meta' struttura ma un'altra strategia."""
|
||||||
|
righe = []
|
||||||
|
for k, t in enumerate(ore):
|
||||||
|
righe.append(dict(ts=t, instrument_name=INS_S, bid=bid_s, ask=ask_s, iv=48.0 + k))
|
||||||
|
if k not in salta_lunga:
|
||||||
|
righe.append(dict(ts=t, instrument_name=INS_L, bid=bid_l, ask=ask_l, iv=57.0 + k))
|
||||||
|
return pd.DataFrame(righe)
|
||||||
|
|
||||||
|
|
||||||
|
def _quote(bid_s=0.030, ask_s=0.032, bid_l=0.010, ask_l=0.012, salta_lunga=()) -> C.Quote:
|
||||||
|
return C.Quote(_seg(bid_s, ask_s, bid_l, ask_l, salta_lunga=salta_lunga),
|
||||||
|
_serie_spot(), _serie_dvol())
|
||||||
|
|
||||||
|
|
||||||
|
def _snap(d_short: float, d_long: float, bid_s=0.030, ask_s=0.032,
|
||||||
|
bid_l=0.010, ask_l=0.012) -> pd.DataFrame:
|
||||||
|
return pd.DataFrame([
|
||||||
|
dict(instrument_name=INS_S, strike=K_S, delta=d_short,
|
||||||
|
bid=bid_s, ask=ask_s, iv=48.0, mid=(bid_s + ask_s) / 2),
|
||||||
|
dict(instrument_name=INS_L, strike=K_L, delta=d_long,
|
||||||
|
bid=bid_l, ask=ask_l, iv=57.0, mid=(bid_l + ask_l) / 2),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def _trade(d_short: float, d_long: float, bid_s=0.030, ask_s=0.032, bid_l=0.010,
|
||||||
|
ask_l=0.012, spot=2000.0, ST=None) -> dict | None:
|
||||||
|
S, V = _serie_spot(spot, ST), _serie_dvol()
|
||||||
|
Q = C.Quote(_seg(bid_s, ask_s, bid_l, ask_l), S, V)
|
||||||
|
return C.trade("ETH", TS0, _snap(d_short, d_long, bid_s, ask_s, bid_l, ask_l), Q, S, V)
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# lo spot e il DVOL arrivano da cblib GIA' causali: qui non si ri-spostano (doppio shift = un'ora
|
||||||
|
# di RITARDO invece di un'ora di anticipo, stesso errore col segno opposto)
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
def test_lo_script_usa_le_serie_di_cblib_senza_ri_spostarle():
|
||||||
|
"""🚨 Il 09/09 mattina lo script riparava da solo il look-ahead con `spot_causale`; la sera la
|
||||||
|
riparazione e' entrata in `cblib.spot_series` / `dvol_series` (debito §5.18). Applicare lo
|
||||||
|
spostamento DUE volte darebbe la chiusura di due ore prima. DEVE FALLIRE SE: qualcuno
|
||||||
|
reintroduce uno shift locale, o `cblib` smette di essere causale."""
|
||||||
|
import inspect
|
||||||
|
import cblib
|
||||||
|
src = inspect.getsource(C.misura)
|
||||||
|
assert "CB.spot_series(asset)" in src and "CB.dvol_series(asset)" in src
|
||||||
|
assert "spot_causale" not in src and "Timedelta(hours=1)" not in src
|
||||||
|
# l'UNICO `causale(` ammesso in misura() e' quello del contesto, e toglie un giorno al DVOL per la
|
||||||
|
# tabella di calendario: un `causale(CB.spot_series(...), "1h")` sarebbe il doppio shift
|
||||||
|
assert src.count("causale(") == 1 and 'CB.causale(V, "-1D")' in src
|
||||||
|
assert not hasattr(C, "spot_causale"), "lo shift locale e' stato rimosso: vive in cblib.causale"
|
||||||
|
# cblib e' causale (controllo positivo sul difetto che ripara)
|
||||||
|
idx = pd.date_range("2026-06-01", periods=3, freq="h", tz="UTC")
|
||||||
|
S = pd.Series([1.0, 2.0, 3.0], index=idx)
|
||||||
|
assert cblib.causale(S, "1h").asof(idx[1]) == 1.0 and S.asof(idx[1]) == 2.0
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# bs_put_vec == cblib.bs_put, elemento per elemento
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
def test_bs_put_vec_e_IDENTICA_a_cblib_bs_put_elemento_per_elemento():
|
||||||
|
"""DEVE FALLIRE SE: la vettorizzata devia dal prezzatore del sleeve — il denominatore di
|
||||||
|
ogni `f` smetterebbe di essere "lo stesso trade nel modello"."""
|
||||||
|
S = np.array([2400.0, 2000.0, 1900.0, 1800.0, 1500.0, 900.0])
|
||||||
|
T = np.array([0.10, 0.0383, 0.0192, 0.0027, 0.0027, 0.20])
|
||||||
|
sig = np.array([0.35, 0.50, 0.65, 1.10, 0.50, 0.05])
|
||||||
|
for K in (K_S, K_L, 2200.0):
|
||||||
|
v = C.bs_put_vec(S, K, T, sig)
|
||||||
|
atteso = [CB.bs_put(float(s), K, float(t), float(g)) for s, t, g in zip(S, T, sig)]
|
||||||
|
assert v == pytest.approx(atteso, rel=0, abs=1e-10), K
|
||||||
|
|
||||||
|
|
||||||
|
def test_bs_put_vec_a_T_o_sigma_nulli_da_l_INTRINSECO():
|
||||||
|
"""I casi degeneri entrano nel conto: all'ora di scadenza T=0 e il valore e' il payoff, non
|
||||||
|
un prezzo. DEVE FALLIRE SE: qualcuno toglie la maschera `ok` e lascia passare log/0 o NaN
|
||||||
|
dentro `mod_spread`."""
|
||||||
|
S = np.array([2400.0, 1900.0, 1500.0, 1000.0])
|
||||||
|
for T, sig in ((np.zeros(4), np.full(4, 0.5)),
|
||||||
|
(np.full(4, -1.0), np.full(4, 0.5)),
|
||||||
|
(np.full(4, 0.05), np.zeros(4))):
|
||||||
|
v = C.bs_put_vec(S, K_S, T, sig)
|
||||||
|
assert v == pytest.approx(np.maximum(K_S - S, 0.0), abs=1e-12)
|
||||||
|
assert np.isfinite(v).all()
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# Quote + percorso su quote sintetiche
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
def test_percorso_costa_lo_spread_nel_VERSO_GIUSTO():
|
||||||
|
"""chiudi_short = (ASK corta − BID lunga)·spot: si ricompra cio' che si e' venduto e si
|
||||||
|
rivende cio' che si e' comprato, cioe' si attraversa lo spread due volte.
|
||||||
|
liquida_long = (BID corta − ASK lunga)·spot e' lo specchio.
|
||||||
|
DEVE FALLIRE SE: qualcuno "corregge" i lati — a mid il percorso costerebbe meno e il fatto
|
||||||
|
misurato si ammorbidirebbe da solo."""
|
||||||
|
Q = _quote(salta_lunga=(1,))
|
||||||
|
|
||||||
|
assert list(Q.bid.index) == list(ORE)
|
||||||
|
assert Q.spot.iloc[0] == 2000.0
|
||||||
|
assert Q.dvol.iloc[0] == pytest.approx(0.50), "il DVOL entra in frazione, non in punti"
|
||||||
|
|
||||||
|
p = C.percorso(Q, ORE[0] - pd.Timedelta(hours=1), INS_S, INS_L, K_S, K_L)
|
||||||
|
assert len(p) == 3, "l'ora con la sola gamba corta dev'essere ESCLUSA"
|
||||||
|
assert list(p["ts"]) == [ORE[0], ORE[2], ORE[3]]
|
||||||
|
assert p["ts"].is_monotonic_increasing
|
||||||
|
|
||||||
|
assert p["chiudi_grezzo"].tolist() == pytest.approx([(0.032 - 0.010) * 2000.0] * 3)
|
||||||
|
assert p["chiudi_short"].tolist() == pytest.approx([(0.032 - 0.010) * 2000.0] * 3), (
|
||||||
|
"dentro la larghezza il taglio non deve mordere: sarebbe un costo inventato")
|
||||||
|
assert p["liquida_long"].tolist() == pytest.approx([(0.030 - 0.012) * 2000.0] * 3)
|
||||||
|
assert p["liquida_put"].tolist() == pytest.approx([0.010 * 2000.0] * 3)
|
||||||
|
assert p["iv_s"].tolist() == pytest.approx([48.0, 50.0, 51.0])
|
||||||
|
assert p["iv_l"].tolist() == pytest.approx([57.0, 59.0, 60.0])
|
||||||
|
assert p["spot"].tolist() == pytest.approx([2000.0] * 3)
|
||||||
|
|
||||||
|
# il valore MODELLATO e' quello del sleeve, agli stessi strike e alla stessa ora
|
||||||
|
for i, t in enumerate([ORE[0], ORE[2], ORE[3]]):
|
||||||
|
T = max((C.EXP - t).total_seconds(), 0.0) / 86400.0 / 365.25
|
||||||
|
atteso = CB.bs_put(2000.0, K_S, T, 0.5) - CB.bs_put(2000.0, K_L, T, 0.5)
|
||||||
|
assert p["mod_spread"].iloc[i] == pytest.approx(atteso, abs=1e-10)
|
||||||
|
assert p["mod_put"].iloc[i] == pytest.approx(CB.bs_put(2000.0, K_L, T, 0.5), abs=1e-10)
|
||||||
|
|
||||||
|
|
||||||
|
def test_percorso_TAGLIA_il_costo_di_chiusura_alla_LARGHEZZA():
|
||||||
|
"""🚨 Uno spread a rischio definito non puo' costare piu' della larghezza per chiudersi: si
|
||||||
|
lascia regolare. A T→0 le quote arrivano al 158% di spread relativo, e senza il taglio il
|
||||||
|
"peggior MTM" era un artefatto di book (2,6× il modello, corretto in revisione a 1,08).
|
||||||
|
`chiudi_grezzo` resta accanto perche' il taglio va CONTATO, non nascosto (P12)."""
|
||||||
|
Q = _quote(bid_s=0.005, ask_s=0.200, bid_l=0.010, ask_l=0.012)
|
||||||
|
p = C.percorso(Q, ORE[0] - pd.Timedelta(hours=1), INS_S, INS_L, K_S, K_L)
|
||||||
|
|
||||||
|
assert p["chiudi_grezzo"].tolist() == pytest.approx([(0.200 - 0.010) * 2000.0] * 4) # 380
|
||||||
|
assert p["chiudi_short"].tolist() == pytest.approx([LARGHEZZA] * 4) # 300
|
||||||
|
assert (p["chiudi_short"] <= LARGHEZZA + 1e-12).all()
|
||||||
|
assert (p["chiudi_grezzo"] > p["chiudi_short"]).all(), "il grezzo deve restare visibile"
|
||||||
|
# la gamba lunga liquidata non puo' valere meno di zero
|
||||||
|
assert p["liquida_long"].tolist() == pytest.approx([0.0] * 4)
|
||||||
|
|
||||||
|
|
||||||
|
def test_percorso_taglia_liquida_long_anche_in_ALTO():
|
||||||
|
"""L'altro estremo della stessa regola: un debit spread non vale piu' della sua larghezza."""
|
||||||
|
Q = _quote(bid_s=0.200, ask_s=0.210, bid_l=0.005, ask_l=0.010)
|
||||||
|
p = C.percorso(Q, ORE[0] - pd.Timedelta(hours=1), INS_S, INS_L, K_S, K_L)
|
||||||
|
assert p["liquida_long"].tolist() == pytest.approx([LARGHEZZA] * 4)
|
||||||
|
|
||||||
|
|
||||||
|
def test_percorso_parte_DOPO_l_ingresso_e_non_lo_include():
|
||||||
|
"""Il mark-to-market comincia all'ora successiva: includere l'ora d'ingresso metterebbe
|
||||||
|
dentro un P&L noto a priori (il mezzo spread pagato entrando)."""
|
||||||
|
Q = _quote()
|
||||||
|
p = C.percorso(Q, ORE[0], INS_S, INS_L, K_S, K_L)
|
||||||
|
assert list(p["ts"]) == [ORE[1], ORE[2], ORE[3]]
|
||||||
|
assert C.percorso(Q, ORE[-1], INS_S, INS_L, K_S, K_L).empty
|
||||||
|
|
||||||
|
|
||||||
|
def test_percorso_e_vuoto_se_uno_strumento_non_esiste_nella_tabella():
|
||||||
|
"""P5: "non vedo" ha uno stato suo (DataFrame vuoto → `trade` ritorna None), non un valore
|
||||||
|
inventato."""
|
||||||
|
Q = _quote()
|
||||||
|
assert C.percorso(Q, ORE[0], INS_S, "ETH-19JUN26-9999-P", K_S, 9999.0).empty
|
||||||
|
assert C.percorso(Q, ORE[0], "ETH-19JUN26-9999-P", INS_L, 9999.0, K_L).empty
|
||||||
|
|
||||||
|
|
||||||
|
def test_Quote_scarta_le_quote_NON_quotate():
|
||||||
|
"""bid o ask a zero (o assenti) non sono prezzi: entrano come "non quotato", non come 0."""
|
||||||
|
seg = _seg()
|
||||||
|
seg.loc[seg["instrument_name"] == INS_L, "bid"] = 0.0 # gamba lunga senza bid
|
||||||
|
Q = C.Quote(seg, _serie_spot(), _serie_dvol())
|
||||||
|
assert INS_L not in Q.bid.columns
|
||||||
|
assert C.percorso(Q, ORE[0], INS_S, INS_L, K_S, K_L).empty
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# la guardia di geometria in_geom (TOL_D)
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
def test_in_geom_e_FALSO_quando_le_delta_disponibili_sono_lontane():
|
||||||
|
"""`pick_legs` prende SEMPRE la delta piu' vicina: su una griglia rada restituisce una
|
||||||
|
struttura che non e' VRP01 e non lo dice. `in_geom` e' il pezzo che lo dice.
|
||||||
|
DEVE FALLIRE SE: qualcuno allarga TOL_D o toglie il flag — i 78 snapshot BTC scartati
|
||||||
|
il 09/09 sono esattamente quelli che quel flag ha fermato."""
|
||||||
|
t = _trade(-0.50, -0.01)
|
||||||
|
assert t is not None
|
||||||
|
assert t["in_geom"] is False
|
||||||
|
assert t["d_short"] == -0.50 and t["d_long"] == -0.01
|
||||||
|
|
||||||
|
|
||||||
|
def test_in_geom_e_VERO_sulla_geometria_di_VRP01():
|
||||||
|
"""Controllo POSITIVO (M15): un rilevatore tarato per non segnalare va provato su un caso
|
||||||
|
che DEVE passare, altrimenti "nessun trade fuori geometria" non significa niente."""
|
||||||
|
t = _trade(C.SHORT_D, C.LONG_D)
|
||||||
|
assert t["in_geom"] is True
|
||||||
|
assert t["d_short"] == pytest.approx(C.SHORT_D) and t["d_long"] == pytest.approx(C.LONG_D)
|
||||||
|
|
||||||
|
|
||||||
|
def test_in_geom_il_confine_e_esattamente_TOL_D():
|
||||||
|
"""I due confini si DERIVANO da `TOL_D`/`SHORT_D`/`LONG_D`, non si riscrivono (P1).
|
||||||
|
|
||||||
|
⚠️ Il confine ESATTO non e' asseribile in virgola mobile: `-0.28 - 0.08` vale
|
||||||
|
-0.36000000000000004, quindi la delta "sul confine" cade FUORI. Si prova un micron dentro
|
||||||
|
e un micron fuori — e' il confine che conta, non il suo ultimo bit."""
|
||||||
|
dentro_s, fuori_s = C.SHORT_D - C.TOL_D[0] + 1e-6, C.SHORT_D - C.TOL_D[0] - 1e-6
|
||||||
|
dentro_l, fuori_l = C.LONG_D - C.TOL_D[1] + 1e-6, C.LONG_D - C.TOL_D[1] - 1e-6
|
||||||
|
assert _trade(dentro_s, dentro_l)["in_geom"] is True, "un micron dentro deve passare"
|
||||||
|
assert _trade(fuori_s, dentro_l)["in_geom"] is False, "gamba corta appena fuori"
|
||||||
|
assert _trade(dentro_s, fuori_l)["in_geom"] is False, "gamba lunga appena fuori"
|
||||||
|
|
||||||
|
|
||||||
|
def test_P1_le_delta_bersaglio_vengono_dal_SLEEVE():
|
||||||
|
"""DEVE FALLIRE SE: qualcuno ridichiara −0,28/−0,10 nello script — la misura punterebbe su
|
||||||
|
una geometria diversa da quella del sleeve che dovrebbe descrivere."""
|
||||||
|
from src.portfolio.sleeves import VRP_CFG
|
||||||
|
assert C.SHORT_D is VRP_CFG["short_delta"]
|
||||||
|
assert C.LONG_D is VRP_CFG["long_delta"]
|
||||||
|
assert C.FEE_FRAC is VRP_CFG["fee_frac"]
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# trade() — i conti, e cio' che si RIFIUTA di calcolare
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
def test_f_exp_e_f_mtm_sono_NAN_a_SEGNI_DISCORDI():
|
||||||
|
"""Un rapporto fra un reale NEGATIVO e un modello POSITIVO non e' un fattore di stress:
|
||||||
|
e' due fatti diversi messi in frazione. Qui il credito reale (0,2 $) sta sotto le fee
|
||||||
|
d'ingresso (1,2 $) mentre il credito modellato e' pieno → pnl_real < 0 < pnl_mod.
|
||||||
|
DEVE FALLIRE SE: qualcuno toglie le condizioni di segno e pubblica un `f` negativo."""
|
||||||
|
t = _trade(C.SHORT_D, C.LONG_D, bid_s=0.0101, ask_s=0.0102, bid_l=0.009, ask_l=0.010)
|
||||||
|
assert t["pnl_real"] < 0 < t["pnl_mod"], (t["pnl_real"], t["pnl_mod"])
|
||||||
|
assert np.isnan(t["f_exp"])
|
||||||
|
assert t["mtm_real_min"] < 0 < t["mtm_mod_min"], (t["mtm_real_min"], t["mtm_mod_min"])
|
||||||
|
assert np.isnan(t["f_mtm"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_f_exp_e_UN_NUMERO_quando_perdono_entrambi():
|
||||||
|
"""Controllo POSITIVO del test qui sopra: con lo spot a 1500 a scadenza perdono sia il
|
||||||
|
reale sia il modello, e allora `f_exp` esiste ed e' esattamente il rapporto delle perdite."""
|
||||||
|
t = _trade(C.SHORT_D, C.LONG_D, bid_s=0.0101, ask_s=0.0102, bid_l=0.009, ask_l=0.010,
|
||||||
|
ST=1500.0)
|
||||||
|
assert t["pnl_real"] < 0 and t["pnl_mod"] < 0
|
||||||
|
assert t["f_exp"] == pytest.approx(t["pnl_real"] / t["pnl_mod"], abs=1e-12)
|
||||||
|
assert np.isfinite(t["f_exp"]) and t["f_exp"] > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_itm_pieno_dichiara_quando_f_exp_e_TAUTOLOGICO():
|
||||||
|
"""Se ENTRAMBE le gambe finiscono ITM il payoff e' la larghezza per il reale e per il
|
||||||
|
modello: `f_exp` si riduce a una funzione di `f_net` e non e' piu' un'informazione nuova.
|
||||||
|
Il flag esiste per poterlo DIRE (l'83% dei trade ETH del 09/09), non per correggerlo."""
|
||||||
|
assert _trade(C.SHORT_D, C.LONG_D, ST=1500.0)["itm_pieno"] is True # 1500 <= K_L
|
||||||
|
assert _trade(C.SHORT_D, C.LONG_D, ST=2000.0)["itm_pieno"] is False
|
||||||
|
assert _trade(C.SHORT_D, C.LONG_D, ST=K_L)["itm_pieno"] is True # confine incluso
|
||||||
|
assert _trade(C.SHORT_D, C.LONG_D, ST=K_L + 1.0)["itm_pieno"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_trade_CONTA_le_ore_tagliate_invece_di_nasconderle():
|
||||||
|
"""P12/D5: una riparazione silenziosa e' un'invenzione. Il taglio alla larghezza cambia il
|
||||||
|
peggior MTM, quindi le ore in cui morde vanno contate e stampate accanto al numero."""
|
||||||
|
normale = _trade(C.SHORT_D, C.LONG_D)
|
||||||
|
assert normale["n_ore"] == 3 and normale["n_tagli"] == 0
|
||||||
|
|
||||||
|
tagliato = _trade(C.SHORT_D, C.LONG_D, ask_s=0.200) # (0,200 − 0,010)·2000 = 380 > 300
|
||||||
|
assert tagliato["n_ore"] == 3 and tagliato["n_tagli"] == 3
|
||||||
|
assert tagliato["mtm_real_min"] == pytest.approx(tagliato["cred_real"] - LARGHEZZA)
|
||||||
|
|
||||||
|
|
||||||
|
def test_f_net_e_il_credito_reale_su_quello_modellato():
|
||||||
|
"""Il credito reale si incassa al BID della corta e si paga all'ASK della lunga: e' la
|
||||||
|
convenzione conservativa di `cblib.f_factors`, e vale 0,76 su ETH nel crollo (0,74 con lo spot
|
||||||
|
e il DVOL non causali della mattina del 09/09: i due numeri vanno cambiati insieme)."""
|
||||||
|
t = _trade(C.SHORT_D, C.LONG_D)
|
||||||
|
assert t["cred_real"] == pytest.approx((0.030 - 0.012) * 2000.0)
|
||||||
|
T = (C.EXP - TS0).total_seconds() / 86400.0 / 365.25
|
||||||
|
assert t["cred_mod"] == pytest.approx(
|
||||||
|
CB.bs_put(2000.0, K_S, T, 0.5) - CB.bs_put(2000.0, K_L, T, 0.5), abs=1e-10)
|
||||||
|
assert t["f_net"] == pytest.approx(t["cred_real"] / t["cred_mod"], abs=1e-12)
|
||||||
|
assert t["risk"] == pytest.approx(LARGHEZZA - t["cred_real"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_trade_e_None_se_il_credito_non_e_positivo():
|
||||||
|
"""Un credit spread a credito ≤ 0 non e' la struttura che si sta studiando."""
|
||||||
|
assert _trade(C.SHORT_D, C.LONG_D, bid_s=0.010, ask_s=0.011, bid_l=0.020, ask_l=0.030) is None
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# verdetto() — i rami
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
ESITO = ("IL CROLLO E' CATTURATO MA A VOL BASSA E FUORI DAL GATE DI VRP01: il f di stress che "
|
||||||
|
"la regola del 19/06 aspettava NON e' misurato nella sostanza — cio' che e' misurato "
|
||||||
|
"e' che il rapporto di credito f_net non cambia col regime")
|
||||||
|
|
||||||
|
|
||||||
|
def _r(f_exp=1.04, f_mtm=1.08, n=569, n_pre=286, n_fuori=2, f_net=0.74, q_itm=0.83,
|
||||||
|
n_strutture=4, n_tagli=8779, n_ore=213477, gate=0.0,
|
||||||
|
dur_f_net=0.71, dur_q_perde=0.46) -> dict:
|
||||||
|
return dict(n=n, n_pre=n_pre, n_fuori=n_fuori, f_net=f_net, f_exp=f_exp, f_mtm=f_mtm,
|
||||||
|
q_itm_pieno=q_itm, n_strutture=n_strutture, n_tagli=n_tagli, n_ore=n_ore,
|
||||||
|
gate_aperto=gate, durante=dict(f_net=dur_f_net, q_perde=dur_q_perde))
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_l_esito_e_una_CONCLUSIONE_non_un_esito_numerico():
|
||||||
|
"""La conclusione del 09/09 non e' "f vale x": e' che il crollo catturato sta a vol BASSA e
|
||||||
|
fuori dal gate, quindi il `f` di stress che la regola del 19/06 aspetta non e' misurato
|
||||||
|
nella sostanza. DEVE FALLIRE SE: qualcuno rimette un verdetto che dipende dal solo numero —
|
||||||
|
e' la lettura che il numero da solo autorizzava e che la revisione ha tolto."""
|
||||||
|
for s in ({"BTC": _r(), "ETH": _r()},
|
||||||
|
{"BTC": _r(f_exp=3.0), "ETH": _r(f_exp=0.2)},
|
||||||
|
{"BTC": None, "ETH": None}):
|
||||||
|
v = C.verdetto(s)
|
||||||
|
assert v.startswith(ESITO)
|
||||||
|
assert v.endswith("— n = 1 crollo, 1 scadenza per asset, famiglia INVERSE")
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_NON_MISURABILE_quando_un_asset_non_ha_snapshot_in_geometria():
|
||||||
|
"""P5: lo stato "non vedo" ha un ramo suo e nomina la causa (griglia di strike rada)."""
|
||||||
|
v = C.verdetto({"BTC": dict(n=0, n_fuori=78), "ETH": None})
|
||||||
|
assert "BTC: NON misurabile — 78 snapshot tutti FUORI geometria (griglia di strike rada)" in v
|
||||||
|
assert "ETH: NON misurabile — 0 snapshot tutti FUORI geometria" in v
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_distingue_ZERO_SNAPSHOT_da_NESSUN_INGRESSO_PRE_CROLLO():
|
||||||
|
"""Due guasti diversi con la stessa azione (P5): "tutti fuori geometria" e "in geometria ma
|
||||||
|
nessun ingresso PRIMA del crollo" sono cause diverse e vanno dette diverse — e' esattamente
|
||||||
|
il caso di BTC il 09/09, dove resta solo la lente DURANTE."""
|
||||||
|
v = C.verdetto({"BTC": _r(f_exp=float("nan"), n=247, n_pre=0, n_fuori=78,
|
||||||
|
dur_f_net=0.79, dur_q_perde=0.3724696356275304),
|
||||||
|
"ETH": _r()})
|
||||||
|
assert "BTC: nessun ingresso pre-crollo in geometria (solo DURANTE: f_net 0.79, perde il 37%)" in v
|
||||||
|
assert "FUORI geometria" not in v.split("BTC:")[1].split(";")[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_dichiara_che_il_f_a_scadenza_e_TAUTOLOGICO():
|
||||||
|
"""M13/§2: un numero si cita con la sua risoluzione. 286 snapshot su 4 strutture non sono
|
||||||
|
286 osservazioni, e a gambe entrambe ITM il `f` a scadenza e' una funzione di `f_net`.
|
||||||
|
Senza queste due note la riga "f 1,04" verrebbe letta come una misura indipendente."""
|
||||||
|
v = C.verdetto({"BTC": _r(), "ETH": _r()})
|
||||||
|
assert "a scadenza f 1.04 ma TAUTOLOGICO nel 83% dei trade" in v
|
||||||
|
assert "4 strutture, non 286 osservazioni" in v
|
||||||
|
assert "peggior MTM tagliato alla larghezza f 1.08" in v
|
||||||
|
assert "quote oltre la larghezza nel 4.1% delle ore" in v
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_dice_se_il_gate_di_VRP01_era_APERTO():
|
||||||
|
"""E' il fatto che decide se la misura parla del sleeve o di un altro trade: col gate chiuso
|
||||||
|
VRP01 quel trade NON lo avrebbe fatto."""
|
||||||
|
assert "gate IV-rank CHIUSO nel 0% delle ore" in C.verdetto({"BTC": _r(), "ETH": _r()})
|
||||||
|
assert "gate IV-rank APERTO nel 60% delle ore" in C.verdetto(
|
||||||
|
{"BTC": _r(gate=0.6), "ETH": _r(gate=0.6)})
|
||||||
|
# confine: 0,5 esatto non e' "aperto" (il confronto e' `>`)
|
||||||
|
assert "gate IV-rank CHIUSO nel 50% delle ore" in C.verdetto(
|
||||||
|
{"BTC": _r(gate=0.5), "ETH": _r(gate=0.5)})
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_non_divide_per_zero_quando_non_ci_sono_ore():
|
||||||
|
"""Un campione vuoto non deve far esplodere la riga: la quota di ore tagliate va a 0,0%."""
|
||||||
|
v = C.verdetto({"BTC": _r(n_tagli=0, n_ore=0), "ETH": _r()})
|
||||||
|
assert "quote oltre la larghezza nel 0.0% delle ore" in v
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_riproduce_ALLA_LETTERA_la_riga_pubblicata_il_09_09():
|
||||||
|
"""M23: la macchina deve ristampare il numero vecchio. Sono i valori esatti usciti da
|
||||||
|
`sintesi()` il 09/09 — se il testo cambia, cambia anche cio' che il diario cita, e va
|
||||||
|
cambiato nei due posti insieme."""
|
||||||
|
s = {"BTC": _r(f_exp=float("nan"), f_mtm=float("nan"), n=247, n_pre=0, n_fuori=78,
|
||||||
|
f_net=0.7941019699391642, q_itm=float("nan"), n_strutture=0, n_tagli=0,
|
||||||
|
n_ore=0, gate=0.0, dur_f_net=0.7941019699391642,
|
||||||
|
dur_q_perde=0.3724696356275304),
|
||||||
|
"ETH": _r(f_exp=1.0383032287675409, f_mtm=1.0790935015712533, n=569, n_pre=286,
|
||||||
|
n_fuori=2, f_net=0.7405178198702741, q_itm=0.8286713286713286,
|
||||||
|
n_strutture=4, n_tagli=8779, n_ore=213477, gate=0.0)}
|
||||||
|
assert C.verdetto(s) == (
|
||||||
|
ESITO + " — "
|
||||||
|
"BTC: nessun ingresso pre-crollo in geometria (solo DURANTE: f_net 0.79, perde il 37%); "
|
||||||
|
"ETH: f_net 0.74 (22/08 in rally 0,714); a scadenza f 1.04 ma TAUTOLOGICO nel 83% dei "
|
||||||
|
"trade (entrambe le gambe ITM ⇒ dipende solo da f_net; 4 strutture, non 286 "
|
||||||
|
"osservazioni); peggior MTM tagliato alla larghezza f 1.08 (quote oltre la larghezza "
|
||||||
|
"nel 4.1% delle ore); gate IV-rank CHIUSO nel 0% delle ore "
|
||||||
|
"— n = 1 crollo, 1 scadenza per asset, famiglia INVERSE")
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# carica() e l'integrazione sull'archivio VERO
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
def test_carica_tiene_solo_put_una_scadenza_la_finestra_e_nessun_duplicato():
|
||||||
|
"""L'archivio ereditato copre molte scadenze e molti tipi: se il filtro si allentasse, il
|
||||||
|
`f` verrebbe misurato su una struttura diversa da quella dichiarata nel docstring."""
|
||||||
|
if not C.ARCHIVIO.exists():
|
||||||
|
pytest.skip(f"archivio ereditato assente: {C.ARCHIVIO} (data/raw e' gitignored)")
|
||||||
|
a = C.carica()
|
||||||
|
assert len(a) > 0
|
||||||
|
assert set(pd.unique(a["option_type"])) == {"P"}, "call rimaste dentro"
|
||||||
|
assert (a["exp"] == C.EXP).all(), "piu' di una scadenza"
|
||||||
|
assert a["ts"].min() >= C.INGRESSI[0], "quote prima della finestra dichiarata"
|
||||||
|
assert a["ts"].max() <= C.EXP, "quote dopo la scadenza"
|
||||||
|
assert not a.duplicated(subset=["ts", "instrument_name"]).any()
|
||||||
|
assert (a["dte"] >= 0).all()
|
||||||
|
assert a["dte"].max() <= (C.EXP - C.INGRESSI[0]).total_seconds() / 86400.0 + 1e-9
|
||||||
|
assert {"BTC", "ETH"} <= set(pd.unique(a["asset"]))
|
||||||
|
|
||||||
|
|
||||||
|
def test_integrazione_la_misura_del_09_09_si_riproduce():
|
||||||
|
"""~11 s. Riproduce cio' su cui poggia la (non) riapertura della decisione del 19/06.
|
||||||
|
|
||||||
|
I fatti che decidono, e che il test tiene fermi:
|
||||||
|
* il gate IV-rank di VRP01 e' CHIUSO nel 100% delle ore → il sleeve questo trade non lo
|
||||||
|
avrebbe fatto, quindi il `f` misurato non e' il suo `f` di stress;
|
||||||
|
* il `f` a scadenza (1,04) e' TAUTOLOGICO nella grande maggioranza dei trade;
|
||||||
|
* 286 snapshot stanno su POCHE strutture: la risoluzione non e' 286 (M13);
|
||||||
|
* BTC resta NON misurabile pre-crollo;
|
||||||
|
* col taglio alla larghezza il peggior MTM e' vicino a 1, non 2,6 (che era book, non
|
||||||
|
rischio) — qui si controlla la TAGLIA, non la terza cifra."""
|
||||||
|
if not C.ARCHIVIO.exists():
|
||||||
|
pytest.skip(f"archivio ereditato assente: {C.ARCHIVIO}")
|
||||||
|
s = C.sintesi(C.misura())
|
||||||
|
|
||||||
|
eth = s["ETH"]
|
||||||
|
assert eth["n_pre"] > 200, "il campione pre-crollo si e' svuotato"
|
||||||
|
assert eth["f_net"] == pytest.approx(0.76, abs=0.03)
|
||||||
|
assert eth["f_exp"] == pytest.approx(1.04, abs=0.03)
|
||||||
|
assert eth["q_itm_pieno"] == pytest.approx(0.83, abs=0.10), (
|
||||||
|
"la quota di trade a f_exp tautologico e' cambiata: cambia la lettura del 1,04")
|
||||||
|
assert eth["n_strutture"] <= 10, (
|
||||||
|
f"{eth['n_pre']} snapshot su {eth['n_strutture']} strutture: se le strutture "
|
||||||
|
"diventassero tante, la nota sulla risoluzione andrebbe riscritta")
|
||||||
|
assert eth["gate_aperto"] == pytest.approx(0.0, abs=1e-12), (
|
||||||
|
"il gate IV-rank di VRP01 risulta APERTO: cambia il senso della misura, che oggi dice "
|
||||||
|
"'lo sleeve questo trade non lo avrebbe fatto'")
|
||||||
|
assert eth["perdita_su_risk"] == pytest.approx(1.01, abs=0.05)
|
||||||
|
assert 0.9 < eth["f_mtm"] < 1.5, (
|
||||||
|
f"peggior MTM {eth['f_mtm']:.2f}: col taglio alla larghezza sta attorno a 1; sopra 1,5 "
|
||||||
|
"vuol dire che il taglio non morde piu' e il numero torna a essere book, non rischio")
|
||||||
|
assert 0 < eth["n_tagli"] < eth["n_ore"] * 0.20, (
|
||||||
|
"il taglio deve mordere su una minoranza di ore: se e' zero non serve, se e' la "
|
||||||
|
"maggioranza il campione e' un altro")
|
||||||
|
|
||||||
|
btc = s["BTC"]
|
||||||
|
assert btc["n_pre"] == 0 and np.isnan(btc["f_exp"]), (
|
||||||
|
"BTC era NON misurabile pre-crollo: se ora lo e', il verdetto cambia e va riscritto, "
|
||||||
|
"non aggiustato")
|
||||||
|
assert btc["durante"]["n"] > 0 and 0.0 < btc["durante"]["q_perde"] < 1.0
|
||||||
|
|
||||||
|
v = C.verdetto(s)
|
||||||
|
assert v.startswith(ESITO)
|
||||||
|
assert "BTC: nessun ingresso pre-crollo in geometria" in v
|
||||||
|
assert "gate IV-rank CHIUSO nel 0% delle ore" in v
|
||||||
|
assert v.endswith("famiglia INVERSE")
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
"""Test di `scripts/research/r0909_deribit_universo.py` (2026-09-09) — l'universo Deribit letto oggi.
|
||||||
|
|
||||||
|
Lo script e' il filtro che decide COSA vale la pena misurare (C6): se sbaglia a leggere il venue,
|
||||||
|
sbaglia l'elenco delle monete candidate, e la misura successiva parte dalla domanda sbagliata.
|
||||||
|
Qui si congelano i tre pezzi che possono sbagliare in silenzio:
|
||||||
|
|
||||||
|
* i FILTRI — un perpetual `state != "open"` non e' negoziabile e non deve comparire in una
|
||||||
|
tabella intitolata "aperti"; un future datato non e' un perpetual;
|
||||||
|
* lo SPREAD assente — senza bid o senza ask lo spread e' NaN, non zero (P5: "non vedo" non e'
|
||||||
|
"spread perfetto", e uno zero stampato in quella colonna direbbe il contrario del vero);
|
||||||
|
* l'universo di XS01 — si IMPORTA da `sleeves.XS_UNIVERSE` (P1). Una lista ricopiata qui
|
||||||
|
risponderebbe "13/19 quotate" anche dopo che qualcuno ha cambiato le gambe del sleeve.
|
||||||
|
|
||||||
|
⚠️ NESSUN test tocca la rete e nessuno scrive in `data/`: la cache viene deviata in `tmp_path`
|
||||||
|
e `get()` e' sostituita da una che fallisce se qualcuno la chiama.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||||
|
|
||||||
|
import r0909_deribit_universo as U # noqa: E402
|
||||||
|
|
||||||
|
LETTO_A = "2026-09-09T15:05:43+00:00"
|
||||||
|
|
||||||
|
|
||||||
|
def _ms(giorno: str) -> int:
|
||||||
|
return int(time.mktime(time.strptime(giorno, "%Y-%m-%d")) * 1000) + int(
|
||||||
|
time.timezone * 1000)
|
||||||
|
|
||||||
|
|
||||||
|
def _instr(coin: str, nome: str, stato: str, listato: str, min_amt: float) -> dict:
|
||||||
|
# creation_timestamp e' letto con time.gmtime -> si costruisce in UTC puro
|
||||||
|
import calendar
|
||||||
|
ts = calendar.timegm(time.strptime(listato + " 12:00", "%Y-%m-%d %H:%M")) * 1000
|
||||||
|
return dict(base_currency=coin, instrument_name=nome, state=stato,
|
||||||
|
creation_timestamp=ts, min_trade_amount=min_amt)
|
||||||
|
|
||||||
|
|
||||||
|
def _bs(nome: str, bid, ask, mark, vol, oi=0.0, funding=0.0) -> dict:
|
||||||
|
return dict(instrument_name=nome, bid_price=bid, ask_price=ask, mark_price=mark,
|
||||||
|
volume_usd=vol, open_interest=oi, funding_8h=funding)
|
||||||
|
|
||||||
|
|
||||||
|
def _lettura() -> dict:
|
||||||
|
"""Una lettura sintetica del venue: 4 perpetual aperti, 1 INATTIVO, 2 future datati (di cui
|
||||||
|
uno sul DVOL — l'unico strumento long-vol diretto del venue)."""
|
||||||
|
fut = [
|
||||||
|
_instr("XRP", "XRP_USDC-PERPETUAL", "open", "2022-03-16", 1.0),
|
||||||
|
_instr("ETH", "ETH_USDC-PERPETUAL", "open", "2022-03-14", 0.01),
|
||||||
|
_instr("ADA", "ADA_USDC-PERPETUAL", "open", "2022-03-21", 10.0),
|
||||||
|
_instr("PAXG", "PAXG_USDC-PERPETUAL", "open", "2024-12-02", 0.001),
|
||||||
|
_instr("APT", "APT_USDC-PERPETUAL", "inactive", "2022-11-02", 1.0),
|
||||||
|
_instr("BTC", "BTC_USDC-27JUN26", "open", "2022-03-09", 0.001),
|
||||||
|
_instr("BTCDVOL", "BTCDVOL_USDC-30SEP26", "open", "2026-06-01", 1.0),
|
||||||
|
]
|
||||||
|
bs = [
|
||||||
|
_bs("XRP_USDC-PERPETUAL", 2.0, 2.002, 2.0, 38.13e6, oi=3.0e6, funding=4.0e-5),
|
||||||
|
_bs("ETH_USDC-PERPETUAL", 3000.0, 3000.3, 3000.0, 32.72e6, oi=8.0e3, funding=1.05e-4),
|
||||||
|
_bs("ADA_USDC-PERPETUAL", None, 0.6, 0.6, 3.33e6, oi=1.0e6, funding=None),
|
||||||
|
_bs("PAXG_USDC-PERPETUAL", 0.0, 3600.0, 3600.0, 0.18e6, oi=2.0e3, funding=0.0),
|
||||||
|
_bs("APT_USDC-PERPETUAL", 1.0, 1.1, 1.0, 999e6, oi=1.0, funding=0.0),
|
||||||
|
_bs("BTC_USDC-27JUN26", 100000.0, 100001.0, 100000.0, 500e6, oi=1.0, funding=0.0),
|
||||||
|
_bs("BTCDVOL_USDC-30SEP26", 30.0, 60.0, 45.0, 0.0, oi=12.0, funding=None),
|
||||||
|
]
|
||||||
|
return dict(letto_a=LETTO_A, fut=fut, bs=bs,
|
||||||
|
opt_usdc=[dict(base_currency="SOL")] * 3 + [dict(base_currency="BTC")] * 2,
|
||||||
|
opt_inv={"BTC": 912, "ETH": 790})
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# (a) tabella / sintesi / verdetto su una lettura SINTETICA
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
def test_tabella_esclude_i_perpetual_NON_APERTI():
|
||||||
|
"""Un perpetual `state != "open"` non e' negoziabile: se entrasse, sarebbe pure il piu'
|
||||||
|
liquido della tabella (999 M$ di volume stantio) e comparirebbe in cima all'elenco dei
|
||||||
|
candidati. DEVE FALLIRE SE: qualcuno toglie il filtro sullo stato."""
|
||||||
|
d = _lettura()
|
||||||
|
rows = U.tabella(d)
|
||||||
|
assert "APT" not in {r["coin"] for r in rows}
|
||||||
|
s = U.sintesi(rows, d)
|
||||||
|
assert s["inattivi"] == ["APT_USDC-PERPETUAL"], (
|
||||||
|
"un perpetual inattivo non va nella tabella, ma va DETTO: e' la differenza fra "
|
||||||
|
"'non esiste' e 'esiste e non si puo' negoziare' (P5)")
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_perpetual_INATTIVO_conta_come_gamba_XS01_MANCANTE():
|
||||||
|
"""APT e' in XS01 ed e' listato sul venue, ma inattivo: la riga "N/19 quotate" deve
|
||||||
|
contarlo fra i mancanti, o il filtro di eseguibilita' direbbe il falso (C6)."""
|
||||||
|
d = _lettura()
|
||||||
|
s = U.sintesi(U.tabella(d), d)
|
||||||
|
assert "APT" in s["xs01_mancanti"] and "APT" not in s["xs01_listate"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_tabella_esclude_i_FUTURE_DATATI_ma_la_sintesi_li_CONTA():
|
||||||
|
"""`kind=future` su Deribit contiene anche i future con scadenza: la domanda della tabella
|
||||||
|
e' sui PERPETUAL, e un future datato non e' una gamba direzionale continua. Restano contati
|
||||||
|
(`n_datati`) perche' e' li' dentro che vive il future sul DVOL."""
|
||||||
|
d = _lettura()
|
||||||
|
rows = U.tabella(d)
|
||||||
|
assert {r["coin"] for r in rows} == {"XRP", "ETH", "ADA", "PAXG"}
|
||||||
|
assert len(rows) == 4
|
||||||
|
assert U.sintesi(rows, d)["n_datati"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_future_sul_DVOL_viene_isolato_coi_suoi_numeri_di_negoziabilita():
|
||||||
|
"""C5/C6: e' l'unico strumento long-vol DIRETTO del venue, quindi va guardato — e va
|
||||||
|
guardato con bid/ask e OI accanto, perche' e' li' che si vede che non e' negoziabile
|
||||||
|
(bid 30 / ask 60 = 67% di spread). Un elenco di nomi non basterebbe."""
|
||||||
|
d = _lettura()
|
||||||
|
s = U.sintesi(U.tabella(d), d)
|
||||||
|
assert [f["nome"] for f in s["dvol_fut"]] == ["BTCDVOL_USDC-30SEP26"], (
|
||||||
|
"il future datato SENZA 'DVOL' nel nome non deve entrare qui")
|
||||||
|
f = s["dvol_fut"][0]
|
||||||
|
assert (f["stato"], f["min_amt"], f["bid"], f["ask"], f["oi"]) == ("open", 1.0, 30.0, 60.0, 12.0)
|
||||||
|
assert f["vol_usd"] == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_tabella_e_ORDINATA_per_volume_decrescente():
|
||||||
|
rows = U.tabella(_lettura())
|
||||||
|
vol = [r["vol_usd"] for r in rows]
|
||||||
|
assert vol == sorted(vol, reverse=True)
|
||||||
|
assert [r["coin"] for r in rows] == ["XRP", "ETH", "ADA", "PAXG"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_lo_spread_e_NAN_senza_bid_o_senza_ask():
|
||||||
|
"""P5: senza un lato del book lo spread non e' 0 bps (che vorrebbe dire "perfetto"), e'
|
||||||
|
"non misurabile". DEVE FALLIRE SE: qualcuno mette un `or 0.0` anche su bid/ask."""
|
||||||
|
rows = {r["coin"]: r for r in U.tabella(_lettura())}
|
||||||
|
assert np.isnan(rows["ADA"]["spread_bps"]), "bid assente -> spread non misurabile"
|
||||||
|
assert np.isnan(rows["PAXG"]["spread_bps"]), "bid a zero -> spread non misurabile"
|
||||||
|
assert rows["XRP"]["spread_bps"] == pytest.approx((2.002 - 2.0) / 2.001 * 1e4)
|
||||||
|
assert rows["ETH"]["spread_bps"] == pytest.approx(0.3 / 3000.15 * 1e4)
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_lotto_minimo_e_in_DOLLARI_non_in_unita():
|
||||||
|
"""C3: prima di misurare il rendimento a un capitale dato si misura il lotto minimo del
|
||||||
|
venue — e il confronto col pavimento $5 si fa in dollari, non in unita' della moneta."""
|
||||||
|
rows = {r["coin"]: r for r in U.tabella(_lettura())}
|
||||||
|
assert rows["XRP"]["min_usd"] == pytest.approx(1.0 * 2.0)
|
||||||
|
assert rows["ADA"]["min_usd"] == pytest.approx(10.0 * 0.6)
|
||||||
|
assert rows["ETH"]["min_amt"] == 0.01
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_data_di_listing_e_quella_del_venue():
|
||||||
|
rows = {r["coin"]: r for r in U.tabella(_lettura())}
|
||||||
|
assert rows["XRP"]["listato"] == "2022-03-16"
|
||||||
|
assert rows["PAXG"]["listato"] == "2024-12-02"
|
||||||
|
|
||||||
|
|
||||||
|
def test_P1_le_gambe_di_XS01_si_IMPORTANO_dal_sleeve():
|
||||||
|
"""*Un controllo puntato su una configurazione diversa da quella che gira passa sempre.*
|
||||||
|
Se l'elenco fosse ricopiato nello script, "13/19 quotate" resterebbe vero anche dopo un
|
||||||
|
cambio di universo del sleeve — cioe' smetterebbe di controllare qualcosa."""
|
||||||
|
from src.portfolio import sleeves
|
||||||
|
assert U.XS_UNIVERSE is sleeves.XS_UNIVERSE
|
||||||
|
src = inspect.getsource(U)
|
||||||
|
assert "from src.portfolio.sleeves import XS_UNIVERSE" in src
|
||||||
|
assert "XS_UNIVERSE = [" not in src, "universo XS01 ridichiarato nello script"
|
||||||
|
|
||||||
|
|
||||||
|
def test_le_gambe_XS01_mancanti_si_CALCOLANO_dall_universo():
|
||||||
|
"""La riga "mancano [...]" e' l'elenco delle gambe di XS01 non quotate sul venue: si
|
||||||
|
ricava dall'universo importato, quindi si aggiorna da sola se il sleeve cambia."""
|
||||||
|
from src.portfolio.sleeves import XS_UNIVERSE
|
||||||
|
rows = U.tabella(_lettura())
|
||||||
|
s = U.sintesi(rows, _lettura())
|
||||||
|
quotate = {"XRP", "ETH", "ADA"} # PAXG non e' in XS01
|
||||||
|
assert s["xs01_listate"] == sorted(c for c in XS_UNIVERSE if c in quotate)
|
||||||
|
assert s["xs01_mancanti"] == [c for c in XS_UNIVERSE if c not in quotate]
|
||||||
|
assert set(s["xs01_listate"]) | set(s["xs01_mancanti"]) == set(XS_UNIVERSE)
|
||||||
|
assert {r["coin"] for r in rows if r["xs01"]} == quotate
|
||||||
|
|
||||||
|
|
||||||
|
def test_liquidi_usa_la_soglia_dichiarata_e_il_confine_e_incluso():
|
||||||
|
"""La soglia $10M/giorno e' dichiarata nel modulo: il test la DERIVA, non la ricopia."""
|
||||||
|
d = _lettura()
|
||||||
|
for b in d["bs"]:
|
||||||
|
if b["instrument_name"] == "ADA_USDC-PERPETUAL":
|
||||||
|
b["volume_usd"] = U.SOGLIA_LIQUIDO_USD # esattamente in soglia -> liquido
|
||||||
|
if b["instrument_name"] == "PAXG_USDC-PERPETUAL":
|
||||||
|
b["volume_usd"] = U.SOGLIA_LIQUIDO_USD - 1.0 # un dollaro sotto -> no
|
||||||
|
s = U.sintesi(U.tabella(d), d)
|
||||||
|
assert s["liquidi"] == ["XRP", "ETH", "ADA"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_sintesi_riporta_le_famiglie_di_opzioni_e_l_ora_della_lettura():
|
||||||
|
"""N10/§7: un numero letto dal venue vale con la sua ora. Senza `letto_a` la riga
|
||||||
|
diventerebbe una verita' senza data."""
|
||||||
|
d = _lettura()
|
||||||
|
s = U.sintesi(U.tabella(d), d)
|
||||||
|
assert s["letto_a"] == LETTO_A
|
||||||
|
assert s["opt_inv"] == {"BTC": 912, "ETH": 790}
|
||||||
|
assert s["opt_usdc"] == [("SOL", 3), ("BTC", 2)]
|
||||||
|
assert s["n_perp"] == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_su_una_lettura_sintetica():
|
||||||
|
d = _lettura()
|
||||||
|
s = U.sintesi(U.tabella(d), d)
|
||||||
|
v = U.verdetto(s)
|
||||||
|
assert v.startswith("4 perpetual USDC; LIQUIDI (≥$10M/g): XRP, ETH; ")
|
||||||
|
assert "XS01 3/19 quotate" in v
|
||||||
|
assert "opzioni USDC-lineari su 2 sottostanti" in v
|
||||||
|
assert f"lettura del {LETTO_A}" in v
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# (b) leggi() — la cache, e la garanzia che NESSUN test vada in rete
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def cache_in_tmp(tmp_path, monkeypatch):
|
||||||
|
"""Devia la cache fuori da `data/_cache/` (debito §5.12: la suite non scrive nei file vivi)."""
|
||||||
|
p = tmp_path / "r0909_deribit_universo.json"
|
||||||
|
monkeypatch.setattr(U, "CACHE", p)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def test_leggi_con_cache_presente_NON_tocca_la_rete(cache_in_tmp, monkeypatch):
|
||||||
|
"""DEVE FALLIRE SE: qualcuno rende la lettura sempre fresca — la suite comincerebbe a
|
||||||
|
dipendere dalla rete e dal venue, e un test rosso non direbbe piu' niente sul codice."""
|
||||||
|
def _vietato(path):
|
||||||
|
raise AssertionError(f"chiamata di rete durante un test: {path}")
|
||||||
|
monkeypatch.setattr(U, "get", _vietato)
|
||||||
|
|
||||||
|
atteso = _lettura()
|
||||||
|
cache_in_tmp.write_text(json.dumps(atteso))
|
||||||
|
d = U.leggi(usa_cache=True)
|
||||||
|
assert d["letto_a"] == LETTO_A
|
||||||
|
assert len(d["fut"]) == len(atteso["fut"])
|
||||||
|
# (i NaN dello spread non sono confrontabili con ==: si confronta il resto)
|
||||||
|
chiave = lambda rows: [(r["coin"], r["listato"], r["vol_usd"], r["min_usd"], r["xs01"])
|
||||||
|
for r in rows]
|
||||||
|
assert chiave(U.tabella(d)) == chiave(U.tabella(atteso))
|
||||||
|
|
||||||
|
|
||||||
|
def test_leggi_senza_cache_interroga_il_venue_UNA_volta_e_la_scrive(cache_in_tmp, monkeypatch):
|
||||||
|
"""La cache non e' un'ottimizzazione: e' la FONTE del numero stampato (una lettura e' un
|
||||||
|
istante). Deve finire su disco con la sua ora, o la riga del diario non ha piu' fonte."""
|
||||||
|
chiamate = []
|
||||||
|
|
||||||
|
def _finto(path):
|
||||||
|
chiamate.append(path)
|
||||||
|
if "get_book_summary" in path:
|
||||||
|
return _lettura()["bs"]
|
||||||
|
if "kind=option" in path and "currency=USDC" in path:
|
||||||
|
return _lettura()["opt_usdc"]
|
||||||
|
if "kind=option" in path:
|
||||||
|
return [0] * 912
|
||||||
|
return _lettura()["fut"]
|
||||||
|
monkeypatch.setattr(U, "get", _finto)
|
||||||
|
|
||||||
|
assert not cache_in_tmp.exists()
|
||||||
|
d = U.leggi(usa_cache=True)
|
||||||
|
assert cache_in_tmp.exists(), "la lettura non e' stata salvata: il numero resta senza fonte"
|
||||||
|
assert json.loads(cache_in_tmp.read_text())["letto_a"] == d["letto_a"]
|
||||||
|
assert d["letto_a"].endswith("+00:00"), "l'ora della lettura dev'essere in UTC dichiarato"
|
||||||
|
assert d["opt_inv"] == {"BTC": 912, "ETH": 912}
|
||||||
|
assert len(chiamate) == 5, "una chiamata per famiglia: fut, book summary, opzioni USDC, BTC, ETH"
|
||||||
|
|
||||||
|
# e una seconda lettura con cache NON ripete le chiamate
|
||||||
|
prima = len(chiamate)
|
||||||
|
U.leggi(usa_cache=True)
|
||||||
|
assert len(chiamate) == prima
|
||||||
|
|
||||||
|
|
||||||
|
def test_fresh_ignora_la_cache(cache_in_tmp, monkeypatch):
|
||||||
|
"""`--fresh` deve rileggere davvero: e' l'unico modo di aggiornare una tabella che per
|
||||||
|
costruzione invecchia (i perpetual del 18/08/2026 non c'erano un mese prima)."""
|
||||||
|
cache_in_tmp.write_text(json.dumps(_lettura()))
|
||||||
|
chiamate = []
|
||||||
|
monkeypatch.setattr(U, "get", lambda path: chiamate.append(path) or [])
|
||||||
|
U.leggi(usa_cache=False)
|
||||||
|
assert chiamate, "con usa_cache=False la cache non va usata"
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_flag_fresh_e_DOCUMENTATO_e_cablato():
|
||||||
|
"""P15: una regola operativa va provata contro il codice che dovrebbe eseguirla. Qui:
|
||||||
|
il flag esiste nel `main`, e' l'unico modo di bypassare la cache, ed e' scritto nell'uso
|
||||||
|
che lo script stampa (chi legge la tabella deve sapere che puo' essere vecchia)."""
|
||||||
|
src = inspect.getsource(U)
|
||||||
|
assert 'leggi(usa_cache="--fresh" not in sys.argv)' in src, "il flag non e' cablato"
|
||||||
|
assert "--fresh per rileggere" in src, (
|
||||||
|
"il flag non e' documentato nell'uso che lo script stampa: chi legge la tabella non "
|
||||||
|
"sa come aggiornarla, e la lettura in cache invecchia in silenzio")
|
||||||
|
|
||||||
|
|
||||||
|
def test_lo_script_NON_ha_credenziali_ne_ordini():
|
||||||
|
"""L'API interrogata e' quella PUBBLICA: nessun token, nessun endpoint privato, nessun
|
||||||
|
ordine. Se un giorno servisse il conto, e' una decisione (debito §5.11), non un refactor."""
|
||||||
|
src = inspect.getsource(U)
|
||||||
|
assert U.API == "https://www.deribit.com/api/v2/public/"
|
||||||
|
for vietato in ("private/", "buy", "sell", "CERBERO", "api_key", "client_id"):
|
||||||
|
assert vietato not in src, f"lo script pubblico contiene '{vietato}'"
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# la lettura VERA in cache (se c'e'): il file su cui poggia la riga del 09/09
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
def test_la_cache_vera_se_c_e_regge_la_tabella_pubblicata():
|
||||||
|
"""Non e' un test di rete: legge il JSON che lo script ha gia' salvato. Se manca, si salta."""
|
||||||
|
if not U.CACHE.exists():
|
||||||
|
pytest.skip(f"nessuna lettura in cache ({U.CACHE}): il test non va in rete")
|
||||||
|
d = json.loads(U.CACHE.read_text())
|
||||||
|
rows = U.tabella(d)
|
||||||
|
s = U.sintesi(rows, d)
|
||||||
|
assert s["n_perp"] == len(rows) > 0
|
||||||
|
assert [r["vol_usd"] for r in rows] == sorted([r["vol_usd"] for r in rows], reverse=True)
|
||||||
|
assert {"BTC", "ETH"} <= {r["coin"] for r in rows}, "il venue non quota piu' BTC/ETH?"
|
||||||
|
assert set(s["xs01_listate"]) | set(s["xs01_mancanti"]) == set(U.XS_UNIVERSE)
|
||||||
|
assert U.verdetto(s).endswith(f"lettura del {d['letto_a']}")
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
"""Test di `scripts/research/r0909_libro_nei_crolli.py` (2026-09-09) — il libro nei crolli.
|
||||||
|
|
||||||
|
Cosa protegge, in due strati:
|
||||||
|
|
||||||
|
* il MECCANISMO su dati SINTETICI, dove la risposta e' nota per costruzione — le classi
|
||||||
|
ESCLUSIVE col loro ordine, le finestre peggiori non sovrapposte, il beta con pendenza
|
||||||
|
imposta, i rami del verdetto. Qui un test rosso significa "il codice ha cambiato
|
||||||
|
comportamento", mai "il mercato ha cambiato idea".
|
||||||
|
* la RIPRODUZIONE del risultato pubblicato il 09/09 sui dati veri: 4 POSITIVO / 6 immune /
|
||||||
|
2 PERDE su 12 finestre, libro > 0 in 8 (dal 2022: 8/8), e beta +0,0769 sulla finestra di
|
||||||
|
§46 che riproduce il +0,076 della memoria. E' M23: prima di pubblicare un numero nuovo, la
|
||||||
|
macchina deve ristampare quello vecchio.
|
||||||
|
|
||||||
|
🚨 LA REGRESSIONE PIU' IMPORTANTE DI QUESTO FILE. `classifica()` prova **immune PRIMA di
|
||||||
|
POSITIVO**, e l'ordine non e' un dettaglio: con l'ordine opposto lo stesso identico dato dava
|
||||||
|
"8/12 episodi in utile" invece di "4 POSITIVO / 6 immune". *L'ordine delle regole ERA il
|
||||||
|
verdetto* (corretto in revisione il 09/09). Un test lo congela.
|
||||||
|
|
||||||
|
⚠️ P1: i pesi del libro NON si ridichiarano — un test congela l'identita' fra i pesi usati dallo
|
||||||
|
script e `src.live.book.W_TP01` / `W_SKH`.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||||
|
|
||||||
|
import r0909_libro_nei_crolli as M # noqa: E402
|
||||||
|
|
||||||
|
RAW = ROOT / "data" / "raw"
|
||||||
|
|
||||||
|
|
||||||
|
def _feed_certificato_presente() -> bool:
|
||||||
|
return all((RAW / f"{a}_{tf}.parquet").exists()
|
||||||
|
for a in ("btc", "eth") for tf in ("1h", "5m"))
|
||||||
|
|
||||||
|
|
||||||
|
def _giorni(n: int, inizio: str = "2021-01-01") -> pd.DatetimeIndex:
|
||||||
|
return pd.date_range(inizio, periods=n, freq="D", tz="UTC")
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# classifica() — classi ESCLUSIVE, e l'ORDINE in cui si provano
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
def test_classifica_prova_IMMUNE_PRIMA_di_POSITIVO():
|
||||||
|
"""🚨 La regressione del 09/09. Sugli stessi numeri l'ordine "POSITIVO prima" dava 8/12
|
||||||
|
episodi in utile; l'ordine dichiarato (immune prima) da' 4 POSITIVO + 6 immune.
|
||||||
|
DEVE FALLIRE SE: qualcuno rimette il test del segno davanti alla banda di immunita' —
|
||||||
|
non e' un refactor, e' un altro verdetto."""
|
||||||
|
# peggiore #8 del 09/09: +0,21% di libro contro −26,54% di indice. E' dentro un decimo.
|
||||||
|
assert M.classifica(0.0021, -0.2654) == "immune+"
|
||||||
|
# peggiore #5: +4,26% contro −31,61% -> fuori dalla banda, e allora e' POSITIVO
|
||||||
|
assert M.classifica(0.0426, -0.3161) == "POSITIVO"
|
||||||
|
|
||||||
|
|
||||||
|
def test_classifica_immune_porta_il_SEGNO_con_se():
|
||||||
|
""""immune" non e' una classe cieca: +0,21% e −0,52% su un crollo del 26% sono entrambi
|
||||||
|
immunita', ma non sono la stessa notizia. DEVE FALLIRE SE: le due si fondono in una."""
|
||||||
|
assert M.classifica(0.0021, -0.2654) == "immune+"
|
||||||
|
assert M.classifica(-0.0052, -0.2583) == "immune-"
|
||||||
|
assert set(M.classifica(x, -0.30) for x in (0.001, -0.001)) == {"immune+", "immune-"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_classifica_fuori_dalla_banda_resta_il_segno():
|
||||||
|
assert M.classifica(+0.05, -0.30) == "POSITIVO"
|
||||||
|
assert M.classifica(-0.05, -0.30) == "PERDE"
|
||||||
|
assert M.classifica(-0.0637, -0.3411) == "PERDE" # peggiore #1 del 09/09
|
||||||
|
|
||||||
|
|
||||||
|
def test_classifica_il_confine_della_banda_e_STRETTO():
|
||||||
|
"""Esattamente un decimo NON e' immune: il confronto e' `<`, non `<=`.
|
||||||
|
DEVE FALLIRE SE: qualcuno lo rilassa e sposta episodi dentro la banda."""
|
||||||
|
assert M.classifica(-0.010, -0.10) == "PERDE"
|
||||||
|
assert M.classifica(-0.010000001, -0.10) == "PERDE"
|
||||||
|
assert M.classifica(-0.009999999, -0.10) == "immune-"
|
||||||
|
assert M.classifica(+0.010, -0.10) == "POSITIVO"
|
||||||
|
assert M.classifica(+0.009999999, -0.10) == "immune+"
|
||||||
|
|
||||||
|
|
||||||
|
def test_classifica_a_indice_zero_non_esiste_immunita():
|
||||||
|
"""Caso limite DICHIARATO (non un bug): con indice 0 la banda ha ampiezza 0, quindi nulla
|
||||||
|
e' immune. Non morde negli episodi (per costruzione hanno indice molto negativo), ma chi
|
||||||
|
riusa `classifica()` altrove deve saperlo."""
|
||||||
|
assert M.classifica(0.0, 0.0) == "PERDE"
|
||||||
|
assert M.classifica(+0.001, 0.0) == "POSITIVO"
|
||||||
|
|
||||||
|
|
||||||
|
def test_classifica_un_libro_esattamente_fermo_e_immune_col_segno_MENO():
|
||||||
|
"""Zero non e' positivo: `libro > 0` e' falso, quindi la classe e' "immune-".
|
||||||
|
E' una convenzione, e va scritta perche' non e' l'unica possibile."""
|
||||||
|
assert M.classifica(0.0, -0.20) == "immune-"
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# finestre_peggiori() — non sovrapposte, ordinate, al piu' N, e la peggiore e' la peggiore
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
def _serie_con_crolli(crolli: list[tuple[int, float]], n: int = 300) -> pd.Series:
|
||||||
|
"""Serie giornaliera piatta con crolli piantati a posizioni note: ogni `(i0, r)` mette
|
||||||
|
20 giorni consecutivi a rendimento `r` a partire dalla posizione `i0`."""
|
||||||
|
r = pd.Series(0.0, index=_giorni(n))
|
||||||
|
for i0, val in crolli:
|
||||||
|
r.iloc[i0:i0 + M.FINESTRA] = val
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
CROLLI = [(30, -0.010), (100, -0.005), (180, -0.002)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_finestre_peggiori_non_si_sovrappongono_e_sono_ordinate():
|
||||||
|
"""DEVE FALLIRE SE: il greedy smette di marcare gli intervalli occupati — senza quel filtro
|
||||||
|
le 12 "peggiori finestre" sarebbero 12 traslazioni di UNO stesso episodio, e la riga
|
||||||
|
"4/12" conterebbe 12 volte lo stesso crollo."""
|
||||||
|
idx = _serie_con_crolli(CROLLI)
|
||||||
|
prese = M.finestre_peggiori(idx, n=3)
|
||||||
|
assert len(prese) == 3
|
||||||
|
assert prese == sorted(prese), "l'elenco esce ordinato per data d'inizio"
|
||||||
|
for (s0, e0, _), (s1, _, _) in zip(prese, prese[1:]):
|
||||||
|
assert s1 > e0, f"finestre sovrapposte: {e0} e {s1}"
|
||||||
|
for s, e, _ in prese:
|
||||||
|
assert e - s == pd.Timedelta(days=M.FINESTRA - 1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_finestre_peggiori_trova_DAVVERO_la_peggiore():
|
||||||
|
"""Il primo episodio dev'essere il minimo globale del rolling, non un minimo locale.
|
||||||
|
DEVE FALLIRE SE: qualcuno sostituisce il greedy dal peggiore con una scansione in avanti."""
|
||||||
|
idx = _serie_con_crolli(CROLLI)
|
||||||
|
prese = M.finestre_peggiori(idx, n=3)
|
||||||
|
roll = (1 + idx).rolling(M.FINESTRA).apply(np.prod, raw=True) - 1
|
||||||
|
peggiore = min(prese, key=lambda t: t[2])
|
||||||
|
assert peggiore[2] == pytest.approx(float(roll.min()), abs=1e-12)
|
||||||
|
assert peggiore[2] == pytest.approx((1 - 0.010) ** M.FINESTRA - 1, abs=1e-12)
|
||||||
|
assert peggiore[0] == idx.index[30] and peggiore[1] == idx.index[49]
|
||||||
|
valori = sorted(v for _, _, v in prese)
|
||||||
|
attesi = sorted((1 + r) ** M.FINESTRA - 1 for _, r in CROLLI)
|
||||||
|
assert valori == pytest.approx(attesi, abs=1e-12)
|
||||||
|
|
||||||
|
|
||||||
|
def test_finestre_peggiori_rispetta_il_tetto_n():
|
||||||
|
idx = _serie_con_crolli(CROLLI)
|
||||||
|
assert len(M.finestre_peggiori(idx, n=2)) == 2
|
||||||
|
assert len(M.finestre_peggiori(idx, n=1)) == 1
|
||||||
|
assert len(M.finestre_peggiori(idx, n=100)) <= 100
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# beta_pieno / beta_ribasso — pendenza NOTA per costruzione
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
def test_beta_pieno_ritrova_una_pendenza_nota():
|
||||||
|
rng = np.random.default_rng(909)
|
||||||
|
i = pd.Series(rng.normal(0.0, 0.03, 400), index=_giorni(400))
|
||||||
|
assert M.beta_pieno(0.30 * i, i) == pytest.approx(0.30, abs=1e-9)
|
||||||
|
assert M.beta_pieno(0.0 * i, i) == pytest.approx(0.0, abs=1e-12)
|
||||||
|
assert M.beta_pieno(-0.75 * i, i) == pytest.approx(-0.75, abs=1e-9)
|
||||||
|
|
||||||
|
|
||||||
|
def test_beta_ribasso_guarda_SOLO_i_giorni_giu():
|
||||||
|
"""Controllo POSITIVO: su una serie asimmetrica (0,50 in salita, 0,10 in discesa) il beta
|
||||||
|
pieno NON puo' rispondere alla domanda del crollo — il beta al ribasso si'.
|
||||||
|
DEVE FALLIRE SE: il filtro `idx < soglia` sparisce e le due misure collassano in una."""
|
||||||
|
rng = np.random.default_rng(46)
|
||||||
|
i = pd.Series(rng.normal(0.0, 0.03, 600), index=_giorni(600))
|
||||||
|
libro = pd.Series(np.where(i.values < 0, 0.10 * i.values, 0.50 * i.values), index=i.index)
|
||||||
|
assert M.beta_ribasso(libro, i) == pytest.approx(0.10, abs=1e-9)
|
||||||
|
assert M.beta_pieno(libro, i) > 0.20, "il beta pieno MEDIA i due regimi: e' un'altra domanda"
|
||||||
|
|
||||||
|
|
||||||
|
def test_beta_ribasso_usa_la_soglia_che_riceve():
|
||||||
|
"""La riga [1] stampa quattro beta (pieno, finestra §46, idx<0, idx≤−3%): sono lenti
|
||||||
|
diverse, non una."""
|
||||||
|
rng = np.random.default_rng(3)
|
||||||
|
i = pd.Series(rng.normal(0.0, 0.03, 800), index=_giorni(800))
|
||||||
|
forte = i.values < -0.03
|
||||||
|
libro = pd.Series(np.where(forte, 0.90 * i.values, 0.10 * i.values), index=i.index)
|
||||||
|
assert (i < -0.03).sum() >= 20, "campione insufficiente: il test non avrebbe potenza"
|
||||||
|
b3 = M.beta_ribasso(libro, i, -0.03)
|
||||||
|
b0 = M.beta_ribasso(libro, i, 0.0)
|
||||||
|
assert b3 == pytest.approx(0.90, abs=1e-9), "sotto −3% la pendenza e' quella imposta"
|
||||||
|
assert abs(b0 - b3) > 0.05, (
|
||||||
|
"la soglia non entra nel calcolo: le lenti darebbero lo stesso numero e la riga [1] "
|
||||||
|
"stamperebbe quattro volte la stessa cosa")
|
||||||
|
|
||||||
|
|
||||||
|
def test_beta_ribasso_e_NAN_sotto_venti_osservazioni():
|
||||||
|
"""P5: "non vedo" non e' "beta zero". DEVE FALLIRE SE: qualcuno toglie la guardia e
|
||||||
|
pubblica una pendenza stimata su tre giorni."""
|
||||||
|
i = pd.Series(np.linspace(-0.05, -0.01, 10), index=_giorni(10))
|
||||||
|
assert np.isnan(M.beta_ribasso(0.3 * i, i)), "10 giorni non fanno una pendenza"
|
||||||
|
i25 = pd.Series(np.linspace(-0.05, -0.01, 25), index=_giorni(25))
|
||||||
|
assert M.beta_ribasso(0.3 * i25, i25) == pytest.approx(0.30, abs=1e-9)
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_finestra_di_46_e_dichiarata_e_non_e_tutta_la_storia():
|
||||||
|
"""§46 e' un numero NATO su una finestra che comincia col DVOL (2021-03-24): confrontarlo
|
||||||
|
col beta di tutta la storia sarebbe P7 al contrario (etichettare un numero con la finestra
|
||||||
|
sbagliata). Lo script tiene le due misure separate — questo test lo congela."""
|
||||||
|
assert M.FINESTRA_46 == pd.Timestamp("2021-03-24", tz="UTC")
|
||||||
|
assert M.BETA_MEMORIA == 0.076 and M.TOL_BETA == 0.01
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# verdetto() — TRE affermazioni, tre domande diverse
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
def _m(guadagna=4, immune=6, n=12, beta_46=0.0769, giorni_libro=-0.0048,
|
||||||
|
giorni_pos=0.175, positivi=8, dal22=(8, 8), quota=1.08) -> dict:
|
||||||
|
return dict(beta_46=beta_46,
|
||||||
|
sintesi=dict(n_episodi=n, guadagna=guadagna, immune=immune,
|
||||||
|
perde=n - guadagna - immune, positivi=positivi, dal22=dal22,
|
||||||
|
giorni_pos=giorni_pos, giorni_libro=giorni_libro,
|
||||||
|
quota_short_nei_guadagni=quota))
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_separa_il_GIORNO_dalla_FINESTRA():
|
||||||
|
"""Le due domande hanno risposte OPPOSTE sugli stessi dati (nel giorno perde, per finestra
|
||||||
|
e' immune-o-positivo), ed e' il fatto centrale del 09/09: SKH01 e' marcato all'uscita, e
|
||||||
|
uno short aperto nel crollo viene accreditato dopo. Un solo aggettivo mentirebbe."""
|
||||||
|
v = M.verdetto(_m())
|
||||||
|
assert v.startswith("NEL GIORNO DEL CROLLO IL LIBRO PERDE")
|
||||||
|
assert "IMMUNE-O-POSITIVO per finestra" in v
|
||||||
|
assert M.verdetto(_m(giorni_libro=+0.0012)).startswith(
|
||||||
|
"NEL GIORNO DEL CROLLO IL LIBRO GUADAGNA")
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_i_tre_rami_della_FINESTRA():
|
||||||
|
"""Le soglie sono 2/3 sui POSITIVO e 2/3 su POSITIVO+immune.
|
||||||
|
DEVE FALLIRE SE: qualcuno le sposta guardando l'esito (selezione sull'hold-out)."""
|
||||||
|
assert "POSITIVO per finestra" in M.verdetto(_m(guadagna=8, immune=2))
|
||||||
|
assert "IMMUNE-O-POSITIVO per finestra" in M.verdetto(_m(guadagna=4, immune=6))
|
||||||
|
assert "PERDE per finestra" in M.verdetto(_m(guadagna=3, immune=2))
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_i_confini_delle_due_soglie():
|
||||||
|
v8 = M.verdetto(_m(guadagna=8, immune=0)) # 8 >= 8.0
|
||||||
|
assert "POSITIVO per finestra" in v8 and "IMMUNE-O-POSITIVO" not in v8
|
||||||
|
assert "IMMUNE-O-POSITIVO per finestra" in M.verdetto(_m(guadagna=7, immune=1))
|
||||||
|
assert "PERDE per finestra" in M.verdetto(_m(guadagna=7, immune=0))
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_dice_se_il_beta_RIPRODUCE_la_memoria_e_su_QUALE_finestra():
|
||||||
|
"""La tolleranza e' TOL_BETA e il beta confrontato e' quello della finestra di §46, non il
|
||||||
|
beta pieno: due numeri diversi (0,0769 contro 0,086), e solo il primo va confrontato."""
|
||||||
|
# ⚠️ il confine esatto non e' asseribile in virgola mobile: 0,076 + 0,01 vale 0,086 e la
|
||||||
|
# differenza torna 0,009999999999999995, cioe' DENTRO. Si prova un micron di qua e di la'.
|
||||||
|
dentro = M.BETA_MEMORIA + M.TOL_BETA - 1e-6
|
||||||
|
fuori = M.BETA_MEMORIA + M.TOL_BETA + 1e-6
|
||||||
|
assert "(riproduce il" in M.verdetto(_m(beta_46=dentro))
|
||||||
|
assert "(NON riproduce il" not in M.verdetto(_m(beta_46=dentro))
|
||||||
|
assert "(NON riproduce il" in M.verdetto(_m(beta_46=fuori))
|
||||||
|
assert "sulla finestra di §46" in M.verdetto(_m())
|
||||||
|
assert f"+{M.BETA_MEMORIA:.3f}" in M.verdetto(_m())
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_dichiara_che_SKH01_e_marcato_all_uscita():
|
||||||
|
"""P4: l'allerta (qui: la riga) risponde anche al "perche'". Senza quella nota, il lettore
|
||||||
|
vede SKH01 ≈ 0 nella tabella per giorno e il guadagno nella tabella per finestra, e non ha
|
||||||
|
modo di sapere che e' la stessa serie letta in due modi."""
|
||||||
|
assert "marcata all'uscita" in M.verdetto(_m())
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_riproduce_ALLA_LETTERA_la_riga_pubblicata_il_09_09():
|
||||||
|
"""M23: la macchina deve ristampare il numero vecchio. Sono i valori esatti usciti da
|
||||||
|
`misura()` il 09/09 — se il testo cambia, cambia anche cio' che il diario cita, e va
|
||||||
|
cambiato nei due posti insieme."""
|
||||||
|
m = _m(guadagna=4, immune=6, n=12, beta_46=0.07691539529362904,
|
||||||
|
giorni_libro=-0.004841811670794324, giorni_pos=0.175, positivi=8, dal22=(8, 8),
|
||||||
|
quota=1.0793712686517833)
|
||||||
|
assert M.verdetto(m) == (
|
||||||
|
"NEL GIORNO DEL CROLLO IL LIBRO PERDE (-0.48%/g, positivo nel 18% dei giorni ≤ −5%); "
|
||||||
|
"IMMUNE-O-POSITIVO per finestra: 4 POSITIVO / 6 immune / 2 PERDE su 12 "
|
||||||
|
"(libro > 0 in 8, dal 2022 in 8/8); beta +0.0769 sulla finestra di §46 "
|
||||||
|
"(riproduce il +0.076); la gamba short di SKH01 (marcata all'uscita) fa il 108% "
|
||||||
|
"dei guadagni negli episodi con libro > 0")
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# P1 — i pesi si IMPORTANO dal book live
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
def test_P1_i_pesi_sono_QUELLI_del_book_live_non_una_copia():
|
||||||
|
"""*Un controllo puntato su una configurazione diversa da quella che gira passa sempre.*
|
||||||
|
L'identita' (`is`) vale solo se i nomi sono legati allo STESSO oggetto dell'import: un
|
||||||
|
`W_TP01 = 0.75` riscritto nello script la romperebbe anche a valore uguale."""
|
||||||
|
import src.live.book as B
|
||||||
|
|
||||||
|
assert M.W_TP01 is B.W_TP01, "TP01: peso ridichiarato invece che importato da src/live/book"
|
||||||
|
assert M.W_SKH is B.W_SKH, "SKH01: peso ridichiarato invece che importato da src/live/book"
|
||||||
|
assert M.W_TP01 + M.W_SKH == pytest.approx(1.0)
|
||||||
|
|
||||||
|
src = inspect.getsource(M)
|
||||||
|
assert "from src.live.book import" in src
|
||||||
|
assert not re.search(r"^W_TP01\s*=", src, re.M), "peso TP01 ridichiarato a livello di modulo"
|
||||||
|
assert not re.search(r"^W_SKH\s*=", src, re.M), "peso SKH01 ridichiarato a livello di modulo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_riga_informativa_37_5_62_5_NON_e_il_peso_del_libro():
|
||||||
|
"""§26 compare come riga informativa. DEVE FALLIRE SE: qualcuno la promuove a peso di
|
||||||
|
default — il 75/25 e' decisione vincolante, la riapre solo `weights_tilt_null`."""
|
||||||
|
assert M.W_INFO == (0.375, 0.625)
|
||||||
|
assert (M.W_TP01, M.W_SKH) != M.W_INFO
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# INTEGRAZIONE sui dati veri
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
def test_la_scomposizione_di_SKH01_in_long_e_short_e_ESATTA():
|
||||||
|
"""~2 s. Due guardie, entrambe P1:
|
||||||
|
(a) la versione "tutto" dev'essere BIT-EXACT con `sleeves._skyhook_returns` — se non lo
|
||||||
|
fosse, l'attribuzione descriverebbe un altro sleeve;
|
||||||
|
(b) long + short == intera. In principio filtrare gli entry-dict potrebbe cambiare il
|
||||||
|
non-overlap (un long che non occupa piu' la barra lascia entrare uno short): la
|
||||||
|
misura dice che qui non succede, e il test lo tiene vero."""
|
||||||
|
if not _feed_certificato_presente():
|
||||||
|
pytest.skip("feed certificato BTC/ETH 5m/1h assente in data/raw (gitignored)")
|
||||||
|
from src.portfolio.sleeves import _skyhook_returns
|
||||||
|
|
||||||
|
d = M.skh_per_direzione()
|
||||||
|
assert set(d) == {"tutto", "long", "short"}
|
||||||
|
J = pd.concat(d, axis=1, join="inner")
|
||||||
|
assert float((J["long"] + J["short"] - J["tutto"]).abs().max()) < 1e-12, (
|
||||||
|
"long + short != intera: la scomposizione non e' un'attribuzione, e' un'altra misura")
|
||||||
|
|
||||||
|
ref = _skyhook_returns()
|
||||||
|
if ref.index.tz is None:
|
||||||
|
ref.index = ref.index.tz_localize("UTC")
|
||||||
|
comune = d["tutto"].index.intersection(ref.index)
|
||||||
|
assert len(comune) > 1000
|
||||||
|
assert float((d["tutto"].loc[comune] - ref.loc[comune]).abs().max()) == 0.0, (
|
||||||
|
"la versione 'tutto' non e' piu' bit-exact col sleeve di ricerca")
|
||||||
|
|
||||||
|
|
||||||
|
def test_integrazione_il_risultato_pubblicato_del_09_09_si_riproduce():
|
||||||
|
"""~2 s. Se questo test diventa rosso NON va "aggiornato": va capito se e' cambiato il
|
||||||
|
dato (rebuild del feed) o il codice, e la riga citata nel diario va rifatta.
|
||||||
|
|
||||||
|
I due fatti che decidono: (1) per FINESTRA il libro non perde quasi mai — 4 POSITIVO +
|
||||||
|
6 immune su 12, e libro > 0 in 8/8 episodi dal 2022; (2) NEL GIORNO del crollo perde
|
||||||
|
(−0,48%/g, positivo solo nel 18% dei giorni ≤ −5%). Sono due risposte opposte alla stessa
|
||||||
|
domanda dell'operatore, e stanno insieme perche' SKH01 e' marcato all'uscita."""
|
||||||
|
if not _feed_certificato_presente():
|
||||||
|
pytest.skip("feed certificato BTC/ETH 5m/1h assente in data/raw (gitignored)")
|
||||||
|
|
||||||
|
m = M.misura()
|
||||||
|
|
||||||
|
assert m["beta"] == pytest.approx(0.086, abs=0.03), "beta pieno fuori dal valore pubblicato"
|
||||||
|
assert abs(m["beta_46"] - M.BETA_MEMORIA) < M.TOL_BETA, (
|
||||||
|
f"il beta sulla finestra di §46 ({m['beta_46']:+.4f}) non riproduce piu' il "
|
||||||
|
f"+{M.BETA_MEMORIA:.3f}: e' la riproduzione che giustifica la lente")
|
||||||
|
|
||||||
|
s = m["sintesi"]
|
||||||
|
assert s["n_episodi"] == 12
|
||||||
|
assert (s["guadagna"], s["immune"], s["perde"]) == (4, 6, 2)
|
||||||
|
assert s["guadagna"] + s["immune"] + s["perde"] == s["n_episodi"]
|
||||||
|
assert s["positivi"] == 8, "il conteggio nudo libro > 0 e' l'altra lettura, e va stampata"
|
||||||
|
assert s["dal22"] == (8, 8), "dal 2022 ogni episodio ha il libro in utile"
|
||||||
|
assert s["giorni_libro"] < 0, "nel GIORNO del crollo il libro perde: e' la meta' scomoda"
|
||||||
|
assert s["giorni_pos"] == pytest.approx(0.175, abs=0.05)
|
||||||
|
|
||||||
|
classi = [r["classe"] for r in m["episodi"]]
|
||||||
|
assert classi.count("POSITIVO") == s["guadagna"]
|
||||||
|
assert sum(c.startswith("immune") for c in classi) == s["immune"]
|
||||||
|
assert classi.count("PERDE") == s["perde"]
|
||||||
|
|
||||||
|
dal_2022 = [r for r in m["episodi"] if int(r["da"][:4]) >= 2022]
|
||||||
|
assert len(dal_2022) == 8
|
||||||
|
assert all(r["libro"] > 0 for r in dal_2022), (
|
||||||
|
"un episodio dal 2022 in poi non ha piu' il libro in utile: "
|
||||||
|
+ str([(r["nome"], r["da"], r["libro"]) for r in dal_2022]))
|
||||||
|
|
||||||
|
v = M.verdetto(m)
|
||||||
|
assert v.startswith("NEL GIORNO DEL CROLLO IL LIBRO PERDE")
|
||||||
|
assert "IMMUNE-O-POSITIVO per finestra" in v
|
||||||
|
assert "(riproduce il +0.076)" in v
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
"""Test di `scripts/research/r0909_xrp_terza_gamba.py` (2026-09-09) — XRP come terza gamba.
|
||||||
|
|
||||||
|
Tre cose vanno protette, e nessuna e' il numero finale:
|
||||||
|
|
||||||
|
1. **Il paginatore.** `_fetch_1h` esiste nella forma che ha perche' Coinbase ha un BUCO su XRP
|
||||||
|
(delistato 2021-01, rilistato 2023-07 dopo la sentenza SEC): un batch vuoto NON deve
|
||||||
|
fermare il ciclo. Un paginatore che si ferma al primo vuoto avrebbe certificato XRP su
|
||||||
|
mezzo campione — e la certificazione decide QUALE lente si usa, cioe' decide il risultato.
|
||||||
|
2. **La regola L-PULITA.** "Dal primo anno in cui tutti i successivi restano sotto soglia":
|
||||||
|
un anno pulito ISOLATO non e' un regime. E' la regola che sceglie la finestra, ed e'
|
||||||
|
dichiarata prima di guardare l'esito — quindi va congelata prima che qualcuno la ritocchi.
|
||||||
|
3. **I tre rami del verdetto**, coi loro AND: AGGIUNGE richiede hold-out E de-levering E anni.
|
||||||
|
|
||||||
|
Piu' il guardrail del namespace: XRP vive in `alt_xrp_*`, e `load_data("XRP")` deve continuare
|
||||||
|
a fallire (il "solo dati certi" di CLAUDE.md non e' codice, e' l'assenza del file).
|
||||||
|
|
||||||
|
⚠️ NESSUN test va in rete: il riferimento Coinbase/Bitstamp o e' in cache o il test si salta.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||||
|
|
||||||
|
import r0909_xrp_terza_gamba as M # noqa: E402
|
||||||
|
|
||||||
|
RAW = ROOT / "data" / "raw"
|
||||||
|
ORA_MS = 3_600_000
|
||||||
|
T0 = pd.Timestamp("2026-01-01", tz="UTC")
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# (a) _fetch_1h — un batch vuoto NON ferma il ciclo
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
class ExFinto:
|
||||||
|
"""Exchange finto e DETERMINISTICO: restituisce un batch vuoto alle chiamate elencate in
|
||||||
|
`vuote` (indici 0-based), altrove `limit` barre orarie a partire da `since`."""
|
||||||
|
|
||||||
|
def __init__(self, vuote=(), righe_prima_di_since=False):
|
||||||
|
self.vuote = set(vuote)
|
||||||
|
self.righe_prima_di_since = righe_prima_di_since
|
||||||
|
self.since_visti: list[int] = []
|
||||||
|
|
||||||
|
def fetch_ohlcv(self, symbol, tf, since=None, limit=None):
|
||||||
|
n = len(self.since_visti)
|
||||||
|
self.since_visti.append(since)
|
||||||
|
assert tf == "1h" and symbol and limit
|
||||||
|
if n in self.vuote:
|
||||||
|
return []
|
||||||
|
base = since - 10 * ORA_MS if self.righe_prima_di_since else since
|
||||||
|
return [[base + i * ORA_MS, 1.0, 1.0, 1.0, 10.0 + n + i, 1.0] for i in range(limit)]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def orologio_fermo(monkeypatch):
|
||||||
|
"""`_fetch_1h` chiude il ciclo su `time.time()`: qui lo si ferma a T0+72h, cosi' il numero
|
||||||
|
di pagine e' un fatto e non il meteo della macchina. (Il patch vive solo dentro il test.)"""
|
||||||
|
fine = T0 + pd.Timedelta(hours=72)
|
||||||
|
monkeypatch.setattr(M.time, "time", lambda: fine.timestamp())
|
||||||
|
return fine
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_1h_AVANZA_dopo_un_batch_vuoto(orologio_fermo):
|
||||||
|
"""IL test di questo file. Coinbase su XRP restituisce vuoto per due anni e mezzo: se il
|
||||||
|
ciclo si fermasse (break) o non facesse avanzare `since` (loop infinito), la certificazione
|
||||||
|
girerebbe su un campione mutilato — e la lente L-PULITA la sceglie la certificazione.
|
||||||
|
DEVE FALLIRE SE: qualcuno sostituisce `since += limit*3_600_000; continue` con un `break`."""
|
||||||
|
ex = ExFinto(vuote={0})
|
||||||
|
s = M._fetch_1h(ex, "XRP/USD", T0, 24)
|
||||||
|
|
||||||
|
assert len(ex.since_visti) >= 2, "il ciclo si e' fermato al primo batch vuoto"
|
||||||
|
assert ex.since_visti[1] == ex.since_visti[0] + 24 * ORA_MS, (
|
||||||
|
"dopo un batch vuoto `since` deve avanzare di un'intera pagina")
|
||||||
|
assert len(s) == 48
|
||||||
|
assert s.index.min() == T0 + pd.Timedelta(hours=24), "la prima pagina era il buco"
|
||||||
|
assert s.index.is_monotonic_increasing and s.index.is_unique
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_1h_scavalca_un_buco_IN_MEZZO_e_riprende(orologio_fermo):
|
||||||
|
"""Il buco vero di XRP e' in mezzo (2021-01 → 2023-07), non in testa: il dato PRIMA e quello
|
||||||
|
DOPO devono stare tutti e due nella serie, col buco che resta un buco (D2: una riga assente
|
||||||
|
non si inventa)."""
|
||||||
|
ex = ExFinto(vuote={1})
|
||||||
|
s = M._fetch_1h(ex, "XRP/USD", T0, 24)
|
||||||
|
|
||||||
|
assert len(s) == 48
|
||||||
|
assert s.index.min() == T0
|
||||||
|
assert s.index.max() == T0 + pd.Timedelta(hours=71)
|
||||||
|
buco = s[(s.index >= T0 + pd.Timedelta(hours=24)) & (s.index < T0 + pd.Timedelta(hours=48))]
|
||||||
|
assert len(buco) == 0, "il buco e' stato riempito con qualcosa: non c'era niente da riempire"
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_1h_scarta_le_righe_PRIMA_di_since_e_avanza_lo_stesso(orologio_fermo):
|
||||||
|
"""Alcuni venue ignorano `since` e ritornano sempre la stessa pagina: il filtro
|
||||||
|
`x[0] >= since` la svuota, e a quel punto vale la stessa regola del batch vuoto."""
|
||||||
|
ex = ExFinto(righe_prima_di_since=True)
|
||||||
|
s = M._fetch_1h(ex, "XRP/USD", T0, 24)
|
||||||
|
# le prime 10 barre di ogni pagina cadono prima di `since` e vengono scartate
|
||||||
|
assert len(ex.since_visti) >= 2
|
||||||
|
assert ex.since_visti[1] > ex.since_visti[0], "il ciclo si e' impantanato"
|
||||||
|
assert (s.index >= T0).all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_1h_non_si_impianta_su_una_pagina_da_una_riga(monkeypatch):
|
||||||
|
"""La guardia `since = nxt if nxt > since else since + pagina` esiste per non girare a
|
||||||
|
vuoto: qui ogni chiamata rende UNA barra a `since`, e il ciclo deve comunque terminare."""
|
||||||
|
fine = T0 + pd.Timedelta(hours=5)
|
||||||
|
monkeypatch.setattr(M.time, "time", lambda: fine.timestamp())
|
||||||
|
|
||||||
|
class UnaRiga:
|
||||||
|
def __init__(self):
|
||||||
|
self.n = 0
|
||||||
|
|
||||||
|
def fetch_ohlcv(self, symbol, tf, since=None, limit=None):
|
||||||
|
self.n += 1
|
||||||
|
assert self.n < 50, "ciclo infinito"
|
||||||
|
return [[since, 1.0, 1.0, 1.0, 10.0, 1.0]]
|
||||||
|
|
||||||
|
s = M._fetch_1h(UnaRiga(), "XRP/USD", T0, 24)
|
||||||
|
assert len(s) == 5
|
||||||
|
assert list(s.index) == list(pd.date_range(T0, periods=5, freq="h"))
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# (b) la regola L-PULITA dentro certifica()
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
ANNI = (2022, 2023, 2024, 2025, 2026)
|
||||||
|
|
||||||
|
|
||||||
|
def _finto_feed(sporchi: dict[int, int], n: int = 700):
|
||||||
|
"""Feed sintetico: per ogni anno `n` barre orarie, di cui `sporchi[y]` a +2% dal
|
||||||
|
riferimento (cioe' sopra la soglia >1% che definisce una barra sporca)."""
|
||||||
|
dd, rr = [], []
|
||||||
|
for y in ANNI:
|
||||||
|
idx = pd.date_range(f"{y}-01-01", periods=n, freq="h", tz="UTC")
|
||||||
|
rif = np.full(n, 100.0)
|
||||||
|
mio = rif.copy()
|
||||||
|
mio[:sporchi.get(y, 0)] = 102.0
|
||||||
|
dd.append(pd.DataFrame(dict(datetime=idx, close=mio, high=mio + 1.0, low=mio - 1.0)))
|
||||||
|
rr.append(pd.DataFrame(dict(ts=idx, close=rif, fonte="CB")))
|
||||||
|
D = pd.concat(dd, ignore_index=True)
|
||||||
|
R = pd.concat(rr, ignore_index=True).set_index("ts")
|
||||||
|
return D, R
|
||||||
|
|
||||||
|
|
||||||
|
def _certifica(monkeypatch, sporchi: dict[int, int], n: int = 700) -> dict:
|
||||||
|
D, R = _finto_feed(sporchi, n)
|
||||||
|
monkeypatch.setattr(M, "dati", lambda asset, tf: D)
|
||||||
|
monkeypatch.setattr(M, "riferimento_1h", lambda start: R)
|
||||||
|
return M.certifica()
|
||||||
|
|
||||||
|
|
||||||
|
def test_L_PULITA_parte_dal_primo_anno_con_TUTTI_i_successivi_puliti(monkeypatch):
|
||||||
|
"""Lo scenario vero: 2022 e 2023 sporchi (0,57% nel 2023), 2024-2026 puliti → 2024-01-01."""
|
||||||
|
c = _certifica(monkeypatch, {2022: 8, 2023: 4})
|
||||||
|
assert c["per_anno"][2023]["q_1pct"] == pytest.approx(4 / 700, abs=1e-6)
|
||||||
|
assert c["per_anno"][2022]["q_1pct"] > M.SOGLIA_PULITA
|
||||||
|
assert c["puliti"] == [2024, 2025, 2026]
|
||||||
|
assert c["pulita_da"] == pd.Timestamp("2024-01-01", tz="UTC")
|
||||||
|
|
||||||
|
|
||||||
|
def test_L_PULITA_ignora_un_anno_pulito_ISOLATO(monkeypatch):
|
||||||
|
"""La regressione che la regola esiste per impedire: 2022 pulito, 2023 sporco, 2024+ puliti.
|
||||||
|
Un anno pulito isolato NON e' un regime — la lente parte dalla coda pulita finale (2024),
|
||||||
|
non dal 2022. DEVE FALLIRE SE: qualcuno semplifica in `min(puliti)`."""
|
||||||
|
c = _certifica(monkeypatch, {2023: 8})
|
||||||
|
assert c["puliti"] == [2022, 2024, 2025, 2026], "il 2022 e' pulito, e va detto"
|
||||||
|
assert c["pulita_da"] == pd.Timestamp("2024-01-01", tz="UTC"), (
|
||||||
|
"la lente non puo' partire dal 2022: il 2023 in mezzo e' sporco")
|
||||||
|
|
||||||
|
|
||||||
|
def test_L_PULITA_e_tutta_la_storia_se_non_c_e_niente_di_sporco(monkeypatch):
|
||||||
|
c = _certifica(monkeypatch, {})
|
||||||
|
assert c["puliti"] == list(ANNI)
|
||||||
|
assert c["pulita_da"] == pd.Timestamp("2022-01-01", tz="UTC")
|
||||||
|
|
||||||
|
|
||||||
|
def test_L_PULITA_e_NONE_se_nessun_anno_e_pulito(monkeypatch):
|
||||||
|
"""P5: "non c'e' una finestra pulita" ha uno stato suo. `misura()` allora non aggiunge la
|
||||||
|
lente e `verdetto()` ripiega sul 2024+ dichiarato (la lente di SOL)."""
|
||||||
|
c = _certifica(monkeypatch, {y: 8 for y in ANNI})
|
||||||
|
assert c["puliti"] == []
|
||||||
|
assert c["pulita_da"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_soglia_di_pulizia_e_INCLUSIVA_e_vale_SOGLIA_PULITA(monkeypatch):
|
||||||
|
"""Il confronto e' `<=`: esattamente in soglia l'anno e' pulito. Con n=1000 barre la
|
||||||
|
soglia 0,5% cade su un intero (5 barre), quindi il confine e' asseribile."""
|
||||||
|
c = _certifica(monkeypatch, {2022: 5, 2023: 5, 2024: 5, 2025: 5, 2026: 5}, n=1000)
|
||||||
|
assert c["per_anno"][2022]["q_1pct"] == pytest.approx(M.SOGLIA_PULITA)
|
||||||
|
assert c["puliti"] == list(ANNI)
|
||||||
|
c2 = _certifica(monkeypatch, {2022: 6, 2023: 5, 2024: 5, 2025: 5, 2026: 5}, n=1000)
|
||||||
|
assert 2022 not in c2["puliti"]
|
||||||
|
assert c2["pulita_da"] == pd.Timestamp("2023-01-01", tz="UTC")
|
||||||
|
|
||||||
|
|
||||||
|
def test_certifica_riporta_per_ogni_anno_le_misure_che_la_tabella_stampa(monkeypatch):
|
||||||
|
c = _certifica(monkeypatch, {2022: 8})
|
||||||
|
for y, r in c["per_anno"].items():
|
||||||
|
assert set(r) == {"n", "q_1pct", "med_bps", "fonte", "flat_1h", "flat_5m"}
|
||||||
|
assert r["n"] == 700
|
||||||
|
assert r["flat_1h"] == 0.0, "high != low per costruzione"
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# (c) verdetto() — i tre rami, tutti con AND
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
NOME = "L-PULITA (2024+)"
|
||||||
|
|
||||||
|
|
||||||
|
def _m(hold, sh3, sh2k, anni_dsh, nome: str = NOME, k: float = 0.827) -> dict:
|
||||||
|
h = np.asarray(hold, float)
|
||||||
|
z = np.zeros(len(h))
|
||||||
|
return dict(lenti={nome: dict(sh=z, hold=h, dd=z, cagr=z)},
|
||||||
|
delev={nome: dict(k=k, sh3=sh3, sh2k=sh2k, cagr3=0.146, cagr2k=0.134)},
|
||||||
|
anno={2024 + i: dict(dsh=float(v)) for i, v in enumerate(anni_dsh)})
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_AGGIUNGE_solo_se_passano_tutte_e_tre_le_condizioni():
|
||||||
|
v = M.verdetto(_m([0.10] * 24, sh3=1.60, sh2k=1.50, anni_dsh=[0.5, 0.4, -0.1]))
|
||||||
|
assert v.startswith("XRP AGGIUNGE")
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_DILUISCE_quando_cadono_hold_out_E_de_levering():
|
||||||
|
v = M.verdetto(_m([-0.169] * 24, sh3=1.54, sh2k=1.53, anni_dsh=[0.54, -0.44, -0.25]))
|
||||||
|
assert v.startswith("XRP DILUISCE")
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_PARI_quando_le_condizioni_si_contraddicono():
|
||||||
|
"""N5/M8: quando una condizione passa e l'altra no, il verdetto non sceglie la piu' comoda —
|
||||||
|
dichiara che non e' selezionabile."""
|
||||||
|
solo_hold = M.verdetto(_m([0.10] * 24, sh3=1.50, sh2k=1.60, anni_dsh=[0.5, 0.4, -0.1]))
|
||||||
|
assert solo_hold.startswith("XRP: PARI / NON SELEZIONABILE")
|
||||||
|
solo_delev = M.verdetto(_m([-0.10] * 24, sh3=1.60, sh2k=1.50, anni_dsh=[0.5, 0.4, -0.1]))
|
||||||
|
assert solo_delev.startswith("XRP: PARI / NON SELEZIONABILE")
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_chiede_il_75_percento_delle_ancore_non_la_sola_mediana():
|
||||||
|
"""M7/§2: la banda d'ancora NON e' un intervallo di confidenza, ma la robustezza alla scelta
|
||||||
|
dell'ancora e' una condizione a se'. Mediana > 0 con solo il 70% delle ancore positive non
|
||||||
|
basta. DEVE FALLIRE SE: qualcuno lascia solo la mediana."""
|
||||||
|
hold = [0.10] * 16 + [-0.10] * 8 # mediana +0,10, ma >0 solo nel 67% delle ancore
|
||||||
|
v = M.verdetto(_m(hold, sh3=1.60, sh2k=1.50, anni_dsh=[0.5, 0.4, -0.1]))
|
||||||
|
assert not v.startswith("XRP AGGIUNGE")
|
||||||
|
hold_ok = [0.10] * 18 + [-0.10] * 6 # esattamente 75%
|
||||||
|
assert M.verdetto(_m(hold_ok, sh3=1.60, sh2k=1.50,
|
||||||
|
anni_dsh=[0.5, 0.4, -0.1])).startswith("XRP AGGIUNGE")
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_il_de_levering_chiede_un_margine_di_0_02():
|
||||||
|
"""M5: il null del de-levering non si passa "per un pelo" — un vantaggio dentro 0,02 di
|
||||||
|
Sharpe e' de-levering travestito, ed e' esattamente il caso di XRP (1,54 contro 1,53)."""
|
||||||
|
quasi = M.verdetto(_m([0.10] * 24, sh3=1.52, sh2k=1.50, anni_dsh=[0.5, 0.4, -0.1]))
|
||||||
|
assert not quasi.startswith("XRP AGGIUNGE")
|
||||||
|
appena = M.verdetto(_m([0.10] * 24, sh3=1.521, sh2k=1.50, anni_dsh=[0.5, 0.4, -0.1]))
|
||||||
|
assert appena.startswith("XRP AGGIUNGE")
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_ripiega_sulla_lente_di_SOL_se_non_c_e_una_coda_pulita():
|
||||||
|
"""Quando `pulita_da` e' None, `misura()` non crea la lente L-PULITA: il verdetto deve
|
||||||
|
leggere quella dichiarata di riserva, non la prima che capita."""
|
||||||
|
m = _m([-0.10] * 24, sh3=1.50, sh2k=1.60, anni_dsh=[-0.1, -0.2, -0.3],
|
||||||
|
nome="2024+ (lente di SOL)")
|
||||||
|
m["lenti"]["L-FULL"] = dict(sh=np.zeros(24), hold=np.full(24, 9.0),
|
||||||
|
dd=np.zeros(24), cagr=np.zeros(24))
|
||||||
|
m["delev"]["L-FULL"] = dict(k=1.0, sh3=9.0, sh2k=0.0, cagr3=0.0, cagr2k=0.0)
|
||||||
|
v = M.verdetto(m)
|
||||||
|
assert "lente 2024+ (lente di SOL)" in v
|
||||||
|
assert v.startswith("XRP DILUISCE"), "ha letto la L-FULL invece della lente dichiarata"
|
||||||
|
|
||||||
|
|
||||||
|
def test_verdetto_riproduce_ALLA_LETTERA_la_riga_pubblicata_il_09_09():
|
||||||
|
"""M23: i valori sono quelli usciti dalla misura a 24 ancore del 09/09."""
|
||||||
|
m = _m([-0.169] * 24, sh3=1.54, sh2k=1.53, anni_dsh=[0.54, -0.44, -0.25])
|
||||||
|
assert M.verdetto(m) == (
|
||||||
|
"XRP DILUISCE — lente L-PULITA (2024+): dSharpe hold-out mediano -0.169 "
|
||||||
|
"(>0 nel 0% di 24 ancore); de-levering: Sh 3 gambe 1.54 vs 2 gambe×0.827 1.53; "
|
||||||
|
"anni 2024+ con dSh>0: 1/3")
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# (d) il GUARDRAIL del namespace
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
def test_il_divieto_su_data_raw_xrp_e_gia_DERIVATO_dal_codice():
|
||||||
|
"""`tests/test_sol_leg.py::test_nessun_asset_fuori_universo_nel_namespace_NUDO` deriva gli
|
||||||
|
asset vietati da `rebuild_history.DERIBIT_INSTR`: finche' XRP e' li' dentro, quel test
|
||||||
|
copre gia' `data/raw/xrp_*.parquet` e qui NON si duplica. Questo test congela la premessa
|
||||||
|
— se qualcuno togliesse XRP dal dict, la guardia sparirebbe in silenzio."""
|
||||||
|
from scripts.analysis.rebuild_history import DERIBIT_INSTR
|
||||||
|
assert "XRP" in DERIBIT_INSTR, (
|
||||||
|
"XRP fuori da DERIBIT_INSTR: il divieto su data/raw/xrp_*.parquet non e' piu' derivato "
|
||||||
|
"da nessuna parte — riscrivere la guardia in test_sol_leg.py prima di toglierlo")
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_data_XRP_continua_a_FALLIRE():
|
||||||
|
"""L'altra meta': il dato XRP dello studio vive in `alt_xrp_*` e il feed attivo non lo vede.
|
||||||
|
Se un giorno `load_data("XRP")` funzionasse, il cron (`--asset BTC ETH`) non lo
|
||||||
|
rinfrescherebbe comunque -> dato stantio che sembra attivo."""
|
||||||
|
from src.data.downloader import load_data
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
load_data("XRP", "1h")
|
||||||
|
|
||||||
|
|
||||||
|
def test_lo_studio_legge_il_namespace_di_RICERCA():
|
||||||
|
"""`dati()` importata da r0822_sol_leg instrada XRP su `alt_xrp_*`: se non c'e' il file,
|
||||||
|
l'errore deve dire come rigenerarlo, non fingere che l'asset non esista."""
|
||||||
|
assert M.ASSET == "XRP" and M.TRE == ("BTC", "ETH", "XRP")
|
||||||
|
if (RAW / "alt_xrp_1h.parquet").exists():
|
||||||
|
d = M.dati("XRP", "1h")
|
||||||
|
assert {"close", "datetime"} <= set(d.columns) and len(d) > 0
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# (e) INTEGRAZIONE (opzionale: salta senza i dati o senza la cache del riferimento)
|
||||||
|
# ===========================================================================================
|
||||||
|
|
||||||
|
def test_integrazione_xrp_diluisce_anche_a_due_ancore(monkeypatch):
|
||||||
|
"""~2 s. Non riproduce i numeri a 24 ancore (2 ancore sono un'altra misura): riproduce la
|
||||||
|
DIREZIONE, che e' la cosa che decide — dSharpe hold-out mediano negativo e verdetto
|
||||||
|
"XRP DILUISCE", cioe' l'ipotesi a priori confermata."""
|
||||||
|
if not all((RAW / f"alt_xrp_{tf}.parquet").exists() for tf in ("1h", "5m")):
|
||||||
|
pytest.skip("namespace di ricerca alt_xrp_* assente (rigenerabile, gitignored)")
|
||||||
|
if not all((RAW / f"{a}_{tf}.parquet").exists() for a in ("btc", "eth") for tf in ("1h", "5m")):
|
||||||
|
pytest.skip("feed certificato BTC/ETH assente")
|
||||||
|
if not M.CACHE.exists():
|
||||||
|
pytest.skip(f"cache del riferimento assente ({M.CACHE}): il test NON va in rete")
|
||||||
|
|
||||||
|
# cintura: anche se la cache sparisse a meta' test, nessuno tocca la rete
|
||||||
|
def _vietato(*a, **k):
|
||||||
|
raise AssertionError("il test ha provato ad andare in rete (_fetch_1h)")
|
||||||
|
monkeypatch.setattr(M, "_fetch_1h", _vietato)
|
||||||
|
|
||||||
|
m = M.misura(2)
|
||||||
|
assert m["n_anchors"] == 2
|
||||||
|
assert m["cert"]["pulita_da"] == pd.Timestamp("2024-01-01", tz="UTC")
|
||||||
|
|
||||||
|
nome = ([n for n in m["lenti"] if n.startswith("L-PULITA")] or ["2024+ (lente di SOL)"])[0]
|
||||||
|
assert np.median(m["lenti"][nome]["hold"]) < 0, "il dSharpe hold-out ha cambiato segno"
|
||||||
|
assert M.verdetto(m).startswith("XRP DILUISCE")
|
||||||
|
assert m["corr"]["L-FULL"] < 0.74, "la correlazione col book supera la soglia del 19/06"
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
"""Revisione settimanale (src/live/revisione.py): sola lettura, firmata, e il silenzio non e' un esito.
|
||||||
|
|
||||||
|
Nessun test chiama il modello o Telegram: `interroga` e `invia` sono iniettati. Nessun test scrive
|
||||||
|
nel repo: tutto in una radice temporanea con file sintetici.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
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 revisione as R # noqa: E402
|
||||||
|
from src.live.venue_probe import in_release_window # noqa: E402
|
||||||
|
|
||||||
|
ADESSO = datetime(2026, 9, 14, 6, 15, tzinfo=timezone.utc) # un lunedi'
|
||||||
|
|
||||||
|
|
||||||
|
def _radice(tmp_path: Path) -> Path:
|
||||||
|
(tmp_path / "CLAUDE.md").write_text("# CLAUDE\n\n## 4. Gate\n| STATARB | 2026-09-27 | soglie | ritiro |\n")
|
||||||
|
(tmp_path / "config").mkdir(); (tmp_path / "config" / "live.json").write_text('{"disaster_sl_pct": 0.30}')
|
||||||
|
j = tmp_path / "docs" / "journal"; j.mkdir(parents=True)
|
||||||
|
for i in range(1, 10):
|
||||||
|
g = (ADESSO - timedelta(days=i)).date().isoformat()
|
||||||
|
(j / f"{g}.md").write_text(f"# Giornale {g}\nEquity $4,477.46 · leva 0.26x\n")
|
||||||
|
d = tmp_path / "docs" / "diary"; d.mkdir()
|
||||||
|
(d / "2026-09-09b-recente.md").write_text("recente: f 0,712\n")
|
||||||
|
(d / "2026-08-20-vecchio.md").write_text("vecchio\n")
|
||||||
|
(d / "senza-data.md").write_text("senza data\n")
|
||||||
|
live = tmp_path / "data" / "live"; live.mkdir(parents=True)
|
||||||
|
(live / "scale_watch_state.json").write_text('{"scala": 1.0}')
|
||||||
|
(live / "usde_watch.jsonl").write_text("\n".join(f'{{"i": {i}}}' for i in range(10)) + "\n")
|
||||||
|
return tmp_path
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- materiale
|
||||||
|
|
||||||
|
def test_il_materiale_prende_la_finestra_dichiarata_e_dice_cosa_non_legge(tmp_path):
|
||||||
|
voci = R.materiale(_radice(tmp_path), ADESSO)
|
||||||
|
nomi = [n for n, _, _ in voci]
|
||||||
|
giornali = [n for n in nomi if n.startswith("giornale ")]
|
||||||
|
assert len(giornali) == R.FINESTRA_GIORNALE_G, "7 pagine, non 9: la finestra e' dichiarata"
|
||||||
|
assert "giornale 2026-09-13" in nomi and "giornale 2026-09-07" in nomi and "giornale 2026-09-06" not in nomi
|
||||||
|
assert "diario 2026-09-09b-recente.md" in nomi
|
||||||
|
assert "diario 2026-08-20-vecchio.md" not in nomi and "diario senza-data.md" not in nomi
|
||||||
|
testo = dict((n, t) for n, t, _ in voci)
|
||||||
|
assert "0,712" in testo["diario 2026-09-09b-recente.md"]
|
||||||
|
assert testo["stato usde_watch.jsonl (ultime 3)"].splitlines() == ['{"i": 7}', '{"i": 8}', '{"i": 9}']
|
||||||
|
assert testo["stato scale_history.jsonl (ultime 3)"] == "(assente)", "un file assente si DICE (P5)"
|
||||||
|
assert testo["git log dal 2026-08-31 + stato del working tree"].startswith("non leggibile"), \
|
||||||
|
"in una cartella che non e' un repo il log non si inventa"
|
||||||
|
assert any(n.startswith("monitor_health") for n in nomi)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_sorveglianti_vengono_prima_delle_voci_grandi(tmp_path):
|
||||||
|
"""Il tetto totale taglia in coda: gli stati (piccoli) devono stare PRIMA di giornale e diari,
|
||||||
|
altrimenti sono i primi a sparire — e' successo alla prima stesura (usde_convert a 2 caratteri)."""
|
||||||
|
nomi = [n for n, _, _ in R.materiale(_radice(tmp_path), ADESSO)]
|
||||||
|
i_stato = max(i for i, n in enumerate(nomi) if n.startswith("stato "))
|
||||||
|
i_giornale = min(i for i, n in enumerate(nomi) if n.startswith("giornale "))
|
||||||
|
assert i_stato < i_giornale
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_troncamento_e_dichiarato(tmp_path, monkeypatch):
|
||||||
|
r = _radice(tmp_path)
|
||||||
|
(r / "CLAUDE.md").write_text("x" * 1000)
|
||||||
|
monkeypatch.setattr(R, "MAX_CHARS_VOCE", 100)
|
||||||
|
n, t, tr = R.materiale(r, ADESSO)[0]
|
||||||
|
assert tr and "[… troncato a 100 caratteri su 1,000]" in t
|
||||||
|
assert "[TRONCATO]" in R.costruisci_prompt([(n, t, tr)], ADESSO)
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_prompt_porta_le_regole_e_la_data(tmp_path):
|
||||||
|
p = R.costruisci_prompt(R.materiale(_radice(tmp_path), ADESSO), ADESSO)
|
||||||
|
assert "DATA DELLA REVISIONE: 2026-09-14 06:15Z" in p
|
||||||
|
for t in R.TITOLI:
|
||||||
|
assert t in p
|
||||||
|
for parola in ("SOLA LETTURA", "CITA LA FONTE", "DECISIONI VINCOLANTI", "docs/memory", "I GATE HANNO UNA DATA",
|
||||||
|
"NIENTE PREVISIONI"):
|
||||||
|
assert parola in p, parola
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- guardia
|
||||||
|
|
||||||
|
def _revisione(sintesi="Tutto letto. Equity $4,477.46.", extra=""):
|
||||||
|
return ("## Sintesi\n" + sintesi + "\n## Cosa non torna\nniente\n## Cosa e' maturo da decidere\n"
|
||||||
|
"STATARB il 2026-09-27\n## Proposte (nessuna azione eseguita)\nnessuna\n"
|
||||||
|
"## Cosa ho letto e cosa mi mancava\nCLAUDE.md" + extra)
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_guardia_rifiuta_il_vuoto_e_i_titoli_mancanti():
|
||||||
|
assert R.valida("", "fonte")[0] == "rifiutata"
|
||||||
|
stato, motivi = R.valida("## Sintesi\nsolo questa", "fonte")
|
||||||
|
assert stato == "rifiutata" and "titoli mancanti" in motivi[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_guardia_sui_numeri_ha_il_controllo_positivo():
|
||||||
|
fonte = "Equity $4,477.46 · leva 0.26x · 2026-09-27"
|
||||||
|
assert R.valida(_revisione(), fonte) == ("ok", [])
|
||||||
|
stato, motivi = R.valida(_revisione(sintesi="Equity 9.999,99 e leva 3,14x"), fonte)
|
||||||
|
assert stato == "sospetta" and "non presenti nel materiale" in motivi[0]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- rapporto e Telegram
|
||||||
|
|
||||||
|
def test_il_rapporto_e_firmato_e_dichiara_la_sola_lettura():
|
||||||
|
voci = [("CLAUDE.md", "x" * 10, False), ("giornale", "y", True)]
|
||||||
|
r = R.rapporto(_revisione(), None, "ok", [], "claude-fable-5-1", ADESSO, voci)
|
||||||
|
assert r.startswith("# Revisione settimanale — 2026-09-14")
|
||||||
|
for frase in ("`claude-fable-5-1`", "SOLA LETTURA", "lettore fallibile (P13)", "## Firma e materiale",
|
||||||
|
"| CLAUDE.md | 10 | |", "| giornale | 1 | si |", "l'unico file"):
|
||||||
|
assert frase in r, frase
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_modello_muto_produce_un_rapporto_che_lo_dice():
|
||||||
|
r = R.rapporto(None, "timeout dopo 900s", "rifiutata", ["timeout dopo 900s"], "m", ADESSO, [])
|
||||||
|
assert "## Revisione NON eseguita" in r and "timeout dopo 900s" in r and "P5" in r
|
||||||
|
tg = R.per_telegram(None, "timeout dopo 900s", "rifiutata", ADESSO, Path("docs/revisioni/x.md"))
|
||||||
|
assert tg.startswith("<b>🚨") and "timeout" in tg
|
||||||
|
|
||||||
|
|
||||||
|
def test_telegram_porta_la_sintesi_e_dichiara_il_taglio():
|
||||||
|
tg = R.per_telegram(_revisione(sintesi="A <b>corta</b>."), None, "ok", ADESSO, Path("p.md"))
|
||||||
|
assert "A <b>corta</b>." in tg and "niente" not in tg, "solo la Sintesi, con l'HTML neutralizzato"
|
||||||
|
lunga = R.per_telegram(_revisione(sintesi="z" * 5000), None, "ok", ADESSO, Path("p.md"))
|
||||||
|
assert "[… sintesi tagliata" in lunga and len(lunga) < 4096
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- orchestrazione: cosa scrive, cosa no
|
||||||
|
|
||||||
|
def _albero(root: Path) -> set[str]:
|
||||||
|
return {str(p.relative_to(root)) for p in root.rglob("*") if p.is_file()}
|
||||||
|
|
||||||
|
|
||||||
|
def test_secco_non_chiama_e_non_scrive(tmp_path, capsys):
|
||||||
|
r = _radice(tmp_path); prima = _albero(r)
|
||||||
|
|
||||||
|
def mai(*a, **k):
|
||||||
|
raise AssertionError("il modello NON va chiamato in --secco")
|
||||||
|
esito = R.scrivi(r, ADESSO, secco=True, interroga=mai, invia=mai)
|
||||||
|
assert esito["secco"] and _albero(r) == prima
|
||||||
|
assert "SECCO" in capsys.readouterr().out
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_giro_riuscito_scrive_UN_solo_file_e_manda_la_sintesi(tmp_path):
|
||||||
|
r = _radice(tmp_path); prima = _albero(r); mandati = []
|
||||||
|
esito = R.scrivi(r, ADESSO, modello="claude-fable-5-1", quiet=True,
|
||||||
|
interroga=lambda p, m, t: (_revisione(), None), invia=lambda h: mandati.append(h) or True)
|
||||||
|
nuovi = _albero(r) - prima
|
||||||
|
assert nuovi == {"docs/revisioni/2026-09-14.md"}, "l'unico file scritto e' il rapporto"
|
||||||
|
assert esito["stato"] == "ok" and esito["telegram"] is True
|
||||||
|
assert "## Sintesi" in (r / "docs" / "revisioni" / "2026-09-14.md").read_text()
|
||||||
|
assert len(mandati) == 1 and "Revisione settimanale" in mandati[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_errore_del_modello_scrive_il_rapporto_e_allerta(tmp_path):
|
||||||
|
r = _radice(tmp_path); mandati = []
|
||||||
|
esito = R.scrivi(r, ADESSO, quiet=True, interroga=lambda p, m, t: ("", "CLI `claude` non trovata"),
|
||||||
|
invia=lambda h: mandati.append(h) or True)
|
||||||
|
assert esito["stato"] == "rifiutata" and "non trovata" in esito["errore"]
|
||||||
|
assert "NON eseguita" in (r / "docs" / "revisioni" / "2026-09-14.md").read_text()
|
||||||
|
assert mandati and mandati[0].startswith("<b>🚨")
|
||||||
|
|
||||||
|
|
||||||
|
def test_una_risposta_che_non_e_una_revisione_e_un_errore_dichiarato(tmp_path):
|
||||||
|
r = _radice(tmp_path)
|
||||||
|
esito = R.scrivi(r, ADESSO, quiet=True, telegram=False, interroga=lambda p, m, t: ("ciao", None))
|
||||||
|
assert esito["stato"] == "rifiutata" and "titoli mancanti" in esito["errore"]
|
||||||
|
assert "NON eseguita" in (r / "docs" / "revisioni" / "2026-09-14.md").read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_telegram_non_manda(tmp_path):
|
||||||
|
r = _radice(tmp_path)
|
||||||
|
|
||||||
|
def mai(h):
|
||||||
|
raise AssertionError("Telegram NON va chiamato con telegram=False")
|
||||||
|
esito = R.scrivi(r, ADESSO, quiet=True, telegram=False, interroga=lambda p, m, t: (_revisione(), None), invia=mai)
|
||||||
|
assert "telegram" not in esito
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_prompt_va_su_stdin_non_su_argv(monkeypatch):
|
||||||
|
"""Al primo giro reale (09/09) 195k caratteri su argv hanno dato `Argument list too long` PRIMA
|
||||||
|
di chiamare il modello. DEVE FALLIRE SE: qualcuno rimette il prompt fra gli argomenti."""
|
||||||
|
visto = {}
|
||||||
|
|
||||||
|
class _P:
|
||||||
|
returncode, stdout, stderr = 0, _revisione(), ""
|
||||||
|
|
||||||
|
def finto_run(args, **kw):
|
||||||
|
visto["args"], visto["input"] = args, kw.get("input")
|
||||||
|
return _P()
|
||||||
|
monkeypatch.setattr(R.subprocess, "run", finto_run)
|
||||||
|
prompt = "PROMPT " * 40_000
|
||||||
|
testo, errore = R.interroga(prompt, "claude-fable-5-1", 5)
|
||||||
|
assert errore is None and "## Sintesi" in testo
|
||||||
|
assert visto["input"] == prompt and all(len(a) < 200 for a in visto["args"])
|
||||||
|
assert "--model" in visto["args"] and "claude-fable-5-1" in visto["args"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_una_eccezione_nella_chiamata_diventa_un_errore_non_un_crash(tmp_path, monkeypatch):
|
||||||
|
"""Controllo positivo sul difetto del primo giro: `OSError` dalla CLI → rapporto NON eseguita + 🚨."""
|
||||||
|
def esplode(args, **kw):
|
||||||
|
raise OSError(7, "Argument list too long")
|
||||||
|
monkeypatch.setattr(R.subprocess, "run", esplode)
|
||||||
|
assert R.interroga("x", "m", 5) == ("", "OSError: [Errno 7] Argument list too long")
|
||||||
|
r = _radice(tmp_path); mandati = []
|
||||||
|
|
||||||
|
def esplode_in_scrivi(p, m, t):
|
||||||
|
raise RuntimeError("boom")
|
||||||
|
esito = R.scrivi(r, ADESSO, quiet=True, interroga=esplode_in_scrivi, invia=lambda h: mandati.append(h) or True)
|
||||||
|
assert esito["stato"] == "rifiutata" and "RuntimeError: boom" in esito["errore"]
|
||||||
|
assert "NON eseguita" in (r / "docs" / "revisioni" / "2026-09-14.md").read_text()
|
||||||
|
assert mandati and mandati[0].startswith("<b>🚨")
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_revisore_non_e_il_modello_che_scrive_il_codice():
|
||||||
|
from src.live import analista as A
|
||||||
|
assert R.MODELLO == "claude-fable-5-1" and R.MODELLO != A.MODELLO_DEFAULT
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- cadenza: tre dichiarazioni d'accordo
|
||||||
|
|
||||||
|
def _riga_crontab() -> str | None:
|
||||||
|
try:
|
||||||
|
out = subprocess.run(["crontab", "-l"], capture_output=True, text=True, timeout=10)
|
||||||
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||||
|
return None
|
||||||
|
if out.returncode != 0:
|
||||||
|
return None
|
||||||
|
righe = [r for r in out.stdout.splitlines() if "cron_review.sh" in r and not r.lstrip().startswith("#")]
|
||||||
|
return righe[0] if righe else ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_cadenza_dichiarata_nel_cron_sh_e_quella_installata_e_fuori_dagli_slot():
|
||||||
|
sh = (ROOT / "scripts" / "cron_review.sh").read_text()
|
||||||
|
m = re.search(r"lunedi' (\d\d):(\d\d) UTC", sh)
|
||||||
|
assert m, "cron_review.sh deve dichiarare «lunedi' HH:MM UTC»"
|
||||||
|
ora, minuto = int(m.group(1)), int(m.group(2))
|
||||||
|
assert minuto != 0, "mai al minuto tondo (rate-limit per-IP)"
|
||||||
|
prossimo = ADESSO.replace(hour=ora, minute=minuto) # ADESSO e' un lunedi'
|
||||||
|
assert prossimo.weekday() == 0 and not in_release_window(prossimo)
|
||||||
|
assert "revisione.py --quiet" in sh
|
||||||
|
riga = _riga_crontab()
|
||||||
|
if riga is None:
|
||||||
|
pytest.skip("crontab non leggibile: la dichiarazione installata non e' verificabile qui")
|
||||||
|
assert riga, "crontab leggibile ma cron_review.sh NON installato"
|
||||||
|
campi = riga.split()
|
||||||
|
assert campi[:5] == [str(minuto), str(ora), "*", "*", "1"], f"installata {campi[:5]}, dichiarata {minuto} {ora} * * 1"
|
||||||
+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
|
from __future__ import annotations
|
||||||
|
|
||||||
import importlib.util
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
sys.path.insert(0, str(ROOT))
|
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 journal as J # noqa: E402
|
||||||
from src.live import tradesdb as T # noqa: E402
|
from src.live import tradesdb as T # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
def _carica_script():
|
def _carica_script():
|
||||||
spec = importlib.util.spec_from_file_location("trades_db_script", ROOT / "scripts/live/trades_db.py")
|
return carica_script("scripts/live/trades_db.py", "trades_db_script")
|
||||||
mod = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(mod)
|
|
||||||
return mod
|
|
||||||
|
|
||||||
|
|
||||||
def _px_piatto(livello=100.0):
|
def _px_piatto(livello=100.0):
|
||||||
@@ -33,11 +29,11 @@ def _px_piatto(livello=100.0):
|
|||||||
|
|
||||||
|
|
||||||
def _prepara(con, equity):
|
def _prepara(con, equity):
|
||||||
con.execute("INSERT INTO fills (fill_id, ts_utc, ts_source, asset, side, qty, price, fee) "
|
# il fill si scrive con `upsert_fills`, non con un INSERT a mano: una riga scritta a mano
|
||||||
"VALUES ('f1','2026-08-24T10:47:15+00:00','test','BTC','buy',0.001,60000.0,0.01)")
|
# aveva `verified` NULL, una forma che la produzione non produce mai
|
||||||
for ts, eq in equity:
|
T.upsert_fills(con, [T.Fill(ts_utc="2026-08-24T10:47:15+00:00", ts_source="test",
|
||||||
con.execute("INSERT INTO equity (ts_utc, equity, src) VALUES (?,?,?)", (ts, eq, "test"))
|
asset="BTC", side="buy", qty=0.001, price=60000.0, fee=0.01)])
|
||||||
con.commit()
|
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):
|
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()
|
con.close()
|
||||||
_carica_script().report()
|
_carica_script().report()
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
assert "TWR : +3.95%" in out
|
assert "TWR" in out and "+3.95%" in out
|
||||||
assert "+234.95%" not in out and "+235" not in out # il grezzo non compare piu'
|
assert "+234.95%" not in out # il grezzo non compare piu'
|
||||||
assert "movimenti capitale : +1,400.00 certi" in out
|
assert "+1,400.00 certi" in out
|
||||||
assert "trading da arming : +9.70" in out
|
assert "+9.70" in out
|
||||||
assert "movimenti di capitale INCLUSI" in out # il delta $ resta, etichettato
|
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()
|
con.close()
|
||||||
_carica_script().report()
|
_carica_script().report()
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
assert "TWR : +5.00%" in out and "movimenti capitale" not in out
|
assert "+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
|
||||||
|
|||||||
@@ -205,3 +205,64 @@ def test_la_quota_si_decide_da_lunedi_e_solo_se_IDONEO():
|
|||||||
assert q("2026-09-30", "IDONEO") is True, "smette di chiedere: la decisione si dimentica"
|
assert q("2026-09-30", "IDONEO") is True, "smette di chiedere: la decisione si dimentica"
|
||||||
assert q("2026-08-31", "IN_ATTESA") is False, "chiede la quota su un conto non idoneo"
|
assert q("2026-08-31", "IN_ATTESA") is False, "chiede la quota su un conto non idoneo"
|
||||||
assert q("2026-08-31", "NON_IDONEO") is False
|
assert q("2026-08-31", "NON_IDONEO") is False
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------ lettura dei trade: i due limiti del canale (issue #2) ------------------------------
|
||||||
|
# Il 07/09 il lettore chiedeva limit=50, la sonda del 06/09 aveva fatto 54 ordini: 400 USDE di
|
||||||
|
# acquisti sono finiti nel «reward». Il lettore ora chiede il massimo del venue (1000) e, se la
|
||||||
|
# lista puo' essere incompleta — troncata al limite, o ultima lettura oltre la finestra di 24h
|
||||||
|
# che il gateway espone — dichiara «non leggibile» invece di sommare (P12).
|
||||||
|
|
||||||
|
class _ClientFinto:
|
||||||
|
def __init__(self, rows):
|
||||||
|
self.rows, self.limit_chiesto = rows, None
|
||||||
|
|
||||||
|
def trade_history(self, instrument, limit=20):
|
||||||
|
self.limit_chiesto = limit
|
||||||
|
return self.rows[:limit]
|
||||||
|
|
||||||
|
|
||||||
|
def _ordini(n, t0_ms, amt=100.0, passo_ms=15_000):
|
||||||
|
return [{"timestamp": t0_ms + i * passo_ms, "direction": "buy", "amount": amt} for i in range(n)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_controllo_positivo_54_ordini_si_sommano_tutti():
|
||||||
|
"""Il caso del 06-07/09: 54 ordini in 24h. Con il vecchio limit=50 ne mancavano 4."""
|
||||||
|
prev, now = 1_788_698_101_000, 1_788_784_501_000 # 06/09 12:35Z -> 07/09 12:35Z
|
||||||
|
c = _ClientFinto(_ordini(54, prev + 30_000_000))
|
||||||
|
tot, n, motivo = W._trades_spot_da(c, prev, now)
|
||||||
|
assert motivo is None and n == 54 and abs(tot - 5400.0) < 1e-9
|
||||||
|
assert c.limit_chiesto == W.TRADE_LIMIT >= 54
|
||||||
|
|
||||||
|
|
||||||
|
def test_lista_al_limite_del_venue_non_e_leggibile():
|
||||||
|
prev, now = 1_788_698_101_000, 1_788_784_501_000
|
||||||
|
c = _ClientFinto(_ordini(W.TRADE_LIMIT + 5, prev + 1000))
|
||||||
|
tot, n, motivo = W._trades_spot_da(c, prev, now)
|
||||||
|
assert tot is None and n is None and "troncata" in motivo
|
||||||
|
# e il reward NON si attribuisce (P12)
|
||||||
|
an = W.analizza({"eq_usde": 1000.0}, 1400.0, tot)
|
||||||
|
assert an["reward_stimato"] is None and not an["reward_rilevato"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_lettura_oltre_la_finestra_del_gateway_non_e_leggibile():
|
||||||
|
"""Giro saltato: 48h dall'ultima lettura. I trade fra 48h e 24h fa sono invisibili."""
|
||||||
|
prev = 1_788_698_101_000
|
||||||
|
now = prev + 48 * 3_600_000
|
||||||
|
c = _ClientFinto(_ordini(3, now - 3_600_000))
|
||||||
|
tot, n, motivo = W._trades_spot_da(c, prev, now)
|
||||||
|
assert tot is None and "finestra" in motivo
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_cadenza_del_cron_sta_dentro_la_tolleranza():
|
||||||
|
"""24h + qualche secondo (il cron delle 12:35 di due giorni consecutivi) e' coperta."""
|
||||||
|
prev = 1_788_698_101_000
|
||||||
|
assert W.trade_copertura(10, prev, prev + 24 * 3_600_000 + 2_000) is None
|
||||||
|
assert W.trade_copertura(10, prev, prev + 24 * 3_600_000 + W.TOLL_FINESTRA_MS + 1) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_trade_prima_della_lettura_precedente_non_contano():
|
||||||
|
prev, now = 1_788_698_101_000, 1_788_784_501_000
|
||||||
|
rows = _ordini(2, prev - 60_000) + [{"timestamp": prev + 1, "direction": "sell", "amount": 40.0}]
|
||||||
|
tot, n, motivo = W._trades_spot_da(_ClientFinto(rows), prev, now)
|
||||||
|
assert motivo is None and n == 1 and abs(tot + 40.0) < 1e-9
|
||||||
|
|||||||
Reference in New Issue
Block a user