diff --git a/docs/specs/mainnet-microtest-plan.md b/docs/specs/mainnet-microtest-plan.md new file mode 100644 index 0000000..ccfaebe --- /dev/null +++ b/docs/specs/mainnet-microtest-plan.md @@ -0,0 +1,151 @@ +# Piano operativo — Micro-test MAINNET (Deribit) + +**Obiettivo.** Rispondere all'unica domanda che testnet e backtest non possono: +*quando si passa ai fill VERI, quanto edge sopravvive?* Si misura con poco denaro +reale (€500–1500) per 2–4 settimane, leggendo il report **ledger-vs-backtest**. +Costo del verdetto: poche centinaia di € invece di scoprirlo a €5k. + +**Perché serve (e perché testnet non basta).** Testnet ha feed farlocchi — il +2026-06-13 `ETH-PERPETUAL` inverse era **congelato a 1661.95 da 12h+** mentre il +prezzo reale si muoveva (gap −1.3%); BTC era vivo. Su mainnet l'arbitraggio tiene +inverse ≈ lineare ≈ realtà (entro bps): **il micro-test risolve il problema feed +per costruzione**, oltre a dare fill/slippage/fee reali. + +--- + +## Prerequisiti (umani, fuori dal codice) + +1. **Conto Deribit MAINNET** (KYC) finanziato con **~€1000** in USDC (vedi sizing). +2. **MAINNET_TOKEN Cerbero** abilitato al trading mainnet sul conto (lato CerberoSuite). +3. Decidere il capitale: **€1000 raccomandato** (vedi sezione Sizing). + +> NB sicurezza: il MAINNET_TOKEN dà accesso a soldi veri. Va in `.env` (gitignored), +> mai committato. Un bot-tag dedicato (`pythagoras-mainnet`) per isolarlo nell'audit. + +--- + +## Come si punta a mainnet (già pronto, 1 sola modifica .env) + +Il client legge il token da env (default = testnet). Per il micro-test, nel `.env`: + +``` +CERBERO_TOKEN= +CERBERO_BOT_TAG=pythagoras-mainnet +``` + +**Nessuna modifica di codice.** `CerberoClient.is_mainnet()` lo rileva; il runner, +l'ExecutionClient e gli smoke usano lo stesso client. Per tornare a testnet: togliere +le due righe. (Verificato: default invariato testnet, override flippa il rilevamento.) + +> Consiglio: **non riusare il container/data testnet**. Avviare il micro-test su una +> dir dati pulita (`data/` separata o nuovo volume) così i ledger reali mainnet non +> si mescolano allo storico testnet, e il report parte clean-start. + +--- + +## Sizing & taglia — perché €1000 e SOLO le fade in fase 1 + +Notional = `alloc_sleeve × position_size × leverage`; amount = notional/prezzo, +quantizzato allo step Deribit. Rumore di arrotondamento = step/amount. + +**Fades-only (6 sleeve equal, pos 0.5 × leva 3) — rumore arrotondamento:** + +| Capitale | notional/fade | round% BTC | round% ETH | +|----------|---------------|-----------|-----------| +| €500 | $125 | 5.2% | 1.3% | +| **€1000** | **$250** | **2.6%** | **0.7%** | +| €1500 | $375 | 1.7% | 0.4% | + +→ **€1000 è il punto giusto**: BTC ~2.6% di rumore (leggibile), abbastanza piccolo +da non far male. €500 funziona ma BTC sale al 5%; €1500 è il più pulito. + +**Perché NIENTE pairs/SH01 in fase 1:** +- **Pairs** (pos 0.13, 2 gambe): a €1000 il notional/gamba è ~$21 → arrotondamento + **~30%** (la gamba alt anche peggio): a questa taglia misureresti l'arrotondamento, + non l'edge. Servono ~€5–8k per pairs puliti. Inoltre l'executor a 2 gambe ha + leg-risk: più superfici d'errore. → fuori dalla fase 1. +- **SH01**: bootstrap full-history dal parquet inverse; su mainnet va ri-derivato il + parquet da prezzi mainnet. Diversificatore, non core. → fuori dalla fase 1. + +Le **6 fade single-leg (MR01/02/07 × BTC/ETH)** sono il **core edge**, l'esecuzione +più semplice (open market + TP limit resting + disaster-SL), e quelle col track +record validato col worker reale. Sono il test giusto. + +--- + +## Fasi + +### Fase 0 — Smoke mainnet (€0 di rischio reale, 1 ordine minimo) +Con il token mainnet attivo, eseguire `live_exec_smoke.py` (ordine→verifica→fee) e +`live_shadow_smoke.py` su **size minima** per confermare: auth mainnet OK, strumenti +USDC risolti, fill verificato per order_id, fee reali lette. Se uno fallisce, STOP. + +### Fase 1 — Solo fade, €1000, 2–4 settimane +`portfolios.yml` per il micro-test: +```yaml +overrides: + total_capital: 1000 + leverage: 3 # config che vorremmo deployare (misura il vero) + position_size: 0.5 + paper_sleeves: [TR01, ROT02, TSM01, XS01, PR01..., SH01...] # tutto tranne le 6 fade -> paper + execution: + enabled: true + sleeves: [MR01, MR02, MR07] # solo fade (DIP01 opz.: 1h BTC, core honest) + pairs_enabled: false # niente 2-gambe in fase 1 + disaster_sl_pct: 0.30 # rete di coda invariata + real_truth: true +``` +> Alternativa prudente settimana-1: `leverage: 2` per dimezzare il DD mentre si +> verifica l'esecuzione, poi 3. + +Avviare il runner mainnet (container separato o stessa immagine con `.env` mainnet), ++ reconciler orario + report ledger-vs-backtest giornaliero `--since `. + +### Fase 2 — Verdetto (dal report ledger-vs-backtest) +Aspettare **≥10–20 trade reali** (su mainnet le fade ETH/BTC tradano regolarmente, no +feed congelato). Criterio (lo stesso del report, soglie già nel verdetto 🟢/🟡/🔴): +- 🟢 **slippage medio ≤15 bps E leakage per-trade < €0.30** in relativo → l'edge regge, + si scala. +- 🟡 leakage moderato → estendere il campione, capire dove perde. +- 🔴 slippage >40 bps o leakage che mangia l'edge → **NON scalare**; l'edge non + sopravvive all'esecuzione (hai perso ~€20–50, non €5k). + +### Fase 3 — Espansione (solo se 🟢) +1. Alzare il capitale (€1000→€2k→€5k) — i pairs diventano puliti sopra ~€5k. +2. Riaccendere pairs (a conto flat) e SH01 (con parquet mainnet). +3. Continuare a leggere il report ad ogni step. + +--- + +## Rischi & safeguard (già implementati) + +- **Disaster-SL on-book −30%** ad ogni apertura → protegge da outage del runner. +- **Reconciler orario** (`reconcile_account --telegram`) → drift conto-vs-libri + + guard `STALE_REAL_POSITION` (worker non gestito). +- **Netting close** (v1.1.25) → niente gambe orfane/close cappati. +- **Real-truth ledger** → equity = soldi veri sul conto. +- **Kill-switch**: `execution.enabled: false` (smette di aprire reali, gestisce le + uscite) oppure stop del container + flatten manuale (procedura già rodata). +- Perdita massima plausibile in fase 1: qualche % di €1000 = **€20–80**. È il prezzo + del verdetto. + +--- + +## Cosa NON fare + +- Non partire con pairs/SH01 (rumore/complessità). +- Non scalare prima del verdetto 🟢 stabile per più giorni. +- Non mescolare i dati testnet e mainnet nello stesso ledger. +- Non committare mai il MAINNET_TOKEN. + +--- + +## Checklist di avvio + +- [ ] Conto Deribit mainnet KYC + finanziato €1000 USDC +- [ ] MAINNET_TOKEN Cerbero abilitato al trading sul conto +- [ ] `.env` con `CERBERO_TOKEN` + `CERBERO_BOT_TAG=pythagoras-mainnet` (dir dati pulita) +- [ ] Fase 0 smoke mainnet verde (ordine→verifica→fee) +- [ ] `portfolios.yml` micro-test (fade-only, €1000, exec sleeves MR01/02/07) +- [ ] runner mainnet + reconciler + report ledger-vs-backtest schedulati +- [ ] dopo ≥10–20 trade: leggere il verdetto → scala / aspetta / stop diff --git a/src/live/cerbero_client.py b/src/live/cerbero_client.py index f8b2463..68a66ec 100644 --- a/src/live/cerbero_client.py +++ b/src/live/cerbero_client.py @@ -1,17 +1,30 @@ -"""Client HTTP per Cerbero MCP — Deribit testnet.""" +"""Client HTTP per Cerbero MCP — Deribit. + +Ambiente determinato dal TOKEN (vedi API_REFERENCE): TESTNET_TOKEN -> upstream +testnet, MAINNET_TOKEN -> upstream live. Default = testnet. Per il micro-test +mainnet basta esportare CERBERO_TOKEN (e opzionalmente CERBERO_BOT_TAG) nel `.env` +— NESSUNA modifica di codice. Vedi docs/specs/mainnet-microtest-plan.md.""" from __future__ import annotations -from dataclasses import dataclass +import os +from dataclasses import dataclass, field from typing import Any import requests -BASE_URL = "https://cerbero-mcp.tielogic.xyz" -TOKEN = "_hm0FkyC67P9OXJTy7R9SE2lfhGz_Wa6i89KqH_uXrk" -BOT_TAG = "pythagoras-paper" +BASE_URL = os.environ.get("CERBERO_BASE_URL", "https://cerbero-mcp.tielogic.xyz") +# Token TESTNET di default; override via env CERBERO_TOKEN per puntare a mainnet. +TESTNET_TOKEN = "_hm0FkyC67P9OXJTy7R9SE2lfhGz_Wa6i89KqH_uXrk" +TOKEN = os.environ.get("CERBERO_TOKEN", TESTNET_TOKEN) +BOT_TAG = os.environ.get("CERBERO_BOT_TAG", "pythagoras-paper") TIMEOUT = 15 +def is_mainnet() -> bool: + """True se il token attivo NON è quello testnet di default (= upstream live).""" + return TOKEN != TESTNET_TOKEN + + @dataclass class CerberoClient: base_url: str = BASE_URL