Phase 0: project skeleton

- pyproject.toml with uv, deps for runtime + gui + backtest + dev
- ruff/mypy strict config, pre-commit hooks for ruff/mypy/pytest
- src/cerbero_bite/ layout with empty modules ready for Phase 1+
- structlog JSONL logger with daily rotation
- click CLI with placeholder subcommands (status, start, kill-switch,
  gui, replay, config hash, audit verify)
- 6 smoke tests passing, mypy --strict clean, ruff clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 23:10:30 +02:00
commit 881bc8a1bf
40 changed files with 6018 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
dist/
*.egg-info/
.eggs/
.pytest_cache/
.coverage
htmlcov/
.tox/
.nox/
.cache
.hypothesis/
.ruff_cache/
.mypy_cache/
# venv
.venv/
venv/
env/
# uv
# (uv.lock is checked in; ignore only the venv it creates)
# Editor
.vscode/
.idea/
*.swp
*.swo
.DS_Store
# Cerbero Bite — runtime data
data/
*.local.yaml
.lockfile
.gui-lockfile
# Secrets
.env
.env.*
!.env.example
+43
View File
@@ -0,0 +1,43 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.4
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
args: ["--maxkb=500"]
- id: check-merge-conflict
- id: detect-private-key
- repo: local
hooks:
- id: mypy
name: mypy --strict
entry: uv run mypy --strict src/cerbero_bite
language: system
types: [python]
pass_filenames: false
- id: pytest-fast
name: pytest unit
entry: uv run pytest tests/unit -x -q
language: system
types: [python]
pass_filenames: false
stages: [pre-commit]
- id: pytest-golden
name: pytest golden
entry: uv run pytest tests/golden tests/integration -x -q
language: system
types: [python]
pass_filenames: false
stages: [pre-push]
+72
View File
@@ -0,0 +1,72 @@
# Cerbero Bite
Sistema deterministico rule-based per l'esecuzione sistematica della strategia
**Cerberus Bite**: credit spread su opzioni Ethereum (Deribit) con gestione
attiva, sizing Quarter Kelly e disciplina di uscita rigida.
## Sintesi della strategia
- **Sottostante:** ETH/USD su Deribit (opzioni europee).
- **Struttura:** Bull Put Spread (modalità principale) o Bear Call Spread,
vendita di credito a delta corto **0.100.15** (≈ 18% OTM).
- **DTE:** 1421 giorni, sweet spot a 18 DTE.
- **Sizing:** Quarter Kelly = **13% del capitale corrente**, con cap
hard 200 EUR per trade e 1.000 EUR di engagement aperto totale.
- **Gestione attiva:** profit take 50% credito, stop loss 1.5× credito,
vol stop +10 punti DVOL, time stop 7 DTE, exit su short strike testato
(|delta| ≥ 0.30). Su ETH **non si difende rollando**: si esce.
- **Frequenza:** apertura ogni 7 giorni, una posizione alla volta.
Il sistema è **deterministico**: nessun LLM partecipa al decision loop.
Le regole sono codificate, le soglie sono parametri di configurazione,
i tool MCP sono usati esclusivamente come fonti di dati e canali di
esecuzione (proposta verso Cerbero core, notifiche verso Adriano).
## Catena di responsabilità
```
Cerbero Bite (rule engine) → Adriano (decisione finale) → Cerbero core (esecuzione)
```
- Cerbero Bite **propone** trade e segnala uscite. Non esegue mai
direttamente sul broker.
- Adriano riceve un report strutturato e dà conferma esplicita.
- Cerbero core riceve l'istruzione tramite `cerbero-memory.push_user_instruction`
con `source="cerbero-bite"` e ne pianifica l'apertura/chiusura sul
proprio motore di esecuzione.
## Documentazione
I documenti di progetto si trovano sotto `docs/`. Sono numerati e da
leggere in ordine per chi implementa.
| File | Contenuto |
|---|---|
| `docs/00-overview.md` | Visione del sistema, perimetro, non-obiettivi |
| `docs/01-strategy-rules.md` | Regole della strategia (entry/manage/exit) |
| `docs/02-architecture.md` | Architettura software, componenti, interfacce |
| `docs/03-algorithms.md` | Specifiche dettagliate dei sette algoritmi core |
| `docs/04-mcp-integration.md` | Mappa dei tool MCP usati e contratti |
| `docs/05-data-model.md` | Schema persistenza posizioni, log, KB |
| `docs/06-operational-flow.md` | Flussi operativi: avvio, settimanale, monitoring |
| `docs/07-risk-controls.md` | Kill switch, cap, dead-man, audit |
| `docs/08-testing-validation.md` | TDD, paper trading, golden tests |
| `docs/09-development-roadmap.md` | Fasi di sviluppo e milestone |
| `docs/10-config-spec.md` | Schema di `strategy.yaml` con soglie |
| `docs/11-gui-streamlit.md` | Dashboard Streamlit locale per osservazione e azioni manuali |
## Stato attuale
Il progetto è in fase **specifica**. Nessun codice è stato ancora
prodotto; tutta la documentazione di design è quella presente nella
cartella `docs/`. La prima fase di implementazione è dettagliata in
`docs/09-development-roadmap.md`.
## Avvertenza
Questo sistema gestisce capitale reale ed è soggetto alle Hard
Prohibitions di Cerbero (vedi `Cerbero_Office/CLAUDE.md`). Le opzioni
su criptovaluta sono strumenti complessi con rischio di perdita totale.
La validazione statistica via Monte Carlo (`Cerbero_Office/NewStrategy/
analisi_dai_storici/`) è uno stimatore, non una garanzia. Le regole di
stop loss e i cap di sizing sono **non negoziabili**.
+108
View File
@@ -0,0 +1,108 @@
# 00 — Overview
## Obiettivo
Eseguire in modo sistematico, deterministico e disciplinato la strategia
**Cerberus Bite** — credit spread su opzioni Ethereum su Deribit con
gestione attiva — minimizzando l'errore umano e rispettando i cap di
rischio definiti.
## Perché senza LLM
La strategia è **completamente codificabile in regole**. Non c'è alcuna
componente di interpretazione semantica del mercato che richieda un
modello linguistico. Tutte le decisioni del decision loop sono
matematiche o booleane:
- "il mark price dello spread è ≤ 50% del credito iniziale?" — confronto numerico
- "DVOL corrente è ≥ DVOL all'apertura + 10?" — confronto numerico
- "delta della short put ha superato 0.30 in valore assoluto?" — confronto numerico
- "sono trascorse 11 giorni da apertura?" — confronto numerico
L'introduzione di un LLM in questo loop introdurrebbe:
- non determinismo (stesso input → output diversi)
- latenza non controllata
- costi ricorrenti senza valore aggiunto
- difficoltà di test e di audit
- rischio di hallucination su dati di mercato
Il LLM (Milito) resta nel ruolo di **consulente offline**: aggiorna le
regole quando l'evidenza statistica lo richiede, riceve dati post-trade
per imparare, ma **non sta nel loop di esecuzione**.
## Confine del sistema
### Cosa fa Cerbero Bite
1. Legge dati di mercato dagli MCP (Deribit, Hyperliquid, sentiment, macro).
2. Valuta condizioni di entrata su finestra temporale fissa (settimanale).
3. Calcola la struttura ottimale dello spread secondo le regole.
4. Verifica liquidità, cap di rischio, calendar macro.
5. Calcola sizing in contratti.
6. Costruisce la proposta di trade e la inoltra ad Adriano (Telegram).
7. Su conferma di Adriano, invia istruzione a Cerbero core.
8. Persiste lo stato della posizione (entry DVOL, credit, strikes, ecc.).
9. Monitora ogni 12 ore le posizioni aperte e applica le regole di uscita.
10. Su trigger di uscita, propone la chiusura ad Adriano e poi a Cerbero core.
11. Logga ogni evento per audit e per il successivo aggiornamento Kelly.
### Cosa NON fa
- Non esegue ordini direttamente sul broker.
- Non aggiorna autonomamente le proprie regole o i parametri.
- Non interpreta news o sentiment in modo qualitativo.
- Non sceglie un sottostante diverso da ETH (per ora).
- Non applica strategie di rolling (regola hard della strategia).
- Non considera setup intraday o eventi a frequenza inferiore alle 12 ore.
## Stakeholder
| Attore | Ruolo |
|---|---|
| Adriano | Owner del capitale, decisore finale, riceve report |
| Cerbero Bite | Rule engine deterministico (questo progetto) |
| Cerbero core | Motore di esecuzione ordini sul broker |
| Milito | Consulente offline (LLM), aggiorna regole su evidenza |
| Cerbero_Brain | Wiki tecnica condivisa con Milito (ricerca lezioni) |
## Vincoli ereditati
- **Cap Cerbero v4** (vedi `Cerbero_Office/CLAUDE.md`):
- Max 200 EUR per singolo trade
- Max 1.000 EUR di impegno totale aperto
- Max 4 posizioni concorrenti
- Max 6 trade per giorno
- Stop trading se perdita giornaliera > 3% equity
- No vendita vega 24h prima di FOMC, CPI, NFP
- **Hard Prohibitions** (non aggirabili nemmeno con override Adriano):
- Mai opzioni nude
- Mai leva > 3x sui perp (irrilevante per Bite, ma resta valido)
- Mai short premium senza SL on-exchange dove tecnicamente possibile
## Metriche di successo a 12 mesi
Soglie minime sotto le quali il sistema va riesaminato:
| Metrica | Target | Soglia di allarme |
|---|---|---|
| Win rate | ≥ 75% | < 65% |
| CAGR (capitale operativo) | ≥ 30% | < 10% |
| Drawdown massimo | ≤ 25% | > 35% |
| Sharpe ratio (annualizzato) | ≥ 0.8 | < 0.4 |
| Numero di violazioni di regole | 0 | ≥ 1 (incident review) |
| Errori di stato (posizioni desincronizzate) | 0 | ≥ 1 (kill switch + fix) |
I risultati sono confrontati a fine anno con la simulazione Monte Carlo
del documento `Cerbero_Office/NewStrategy/analisi_dai_storici/
analisi-montecarlo-kelly.md`.
## Glossario rapido
- **DVOL**: indice di volatilità implicita ETH calcolato da Deribit, analogo al VIX per S&P 500.
- **Bull Put Spread**: vendita di una put OTM e acquisto di una put più OTM come protezione, su stesso sottostante e stessa scadenza. Incassi credito netto.
- **DTE**: Days To Expiration, giorni alla scadenza dell'opzione.
- **Quarter Kelly**: 25% della frazione di Kelly piena, sizing professionale standard. In questa strategia coincide con il 13% del capitale corrente.
- **Vol stop**: regola di uscita che chiude la posizione se la volatilità implicita esplode oltre una soglia rispetto all'entry.
- **Combo order**: ordine atomico su Deribit che apre/chiude entrambe le gambe dello spread simultaneamente, eliminando il leg risk.
+225
View File
@@ -0,0 +1,225 @@
# 01 — Regole della strategia (Cerberus Bite)
Le regole qui formalizzate sono la versione **canonica e immutabile** che
il rule engine deve applicare. Modifiche richiedono una review esplicita
con Adriano (e una nuova versione di questo documento).
Sorgente teorica: `Cerbero_Office/NewStrategy/strategia-credit-spread-eth.md`
+ analisi Monte Carlo + cap operativi Cerbero v4.
## 1. Universo e strumenti
| Parametro | Valore |
|---|---|
| Sottostante | ETH/USD |
| Exchange | Deribit |
| Tipo opzioni | Europee, cash-settled, USDC-margined |
| Strutture ammesse | Bull Put Spread, Bear Call Spread, (Iron Condor solo in regime laterale, vedere §6) |
| Strutture vietate | Naked options, calendar spread, ratio spread, condor sbilanciati |
## 2. Trigger di apertura (entry)
Il rule engine valuta l'apertura di un nuovo trade **una sola volta al
giorno**, alle **14:00 UTC** del lunedì (orario UE pomeridiano stabile,
fuori dai picchi di funding statunitensi). Se il lunedì è festività
italiana, l'engine ignora la regola e attende il lunedì successivo.
Una nuova posizione viene aperta **solo se tutte** le seguenti condizioni
sono vere:
1. **Nessuna posizione Cerberus Bite già aperta.**
2. **Capitale corrente ≥ 720 USD.** Sotto questa soglia il sizing in
contratti non può raggiungere 1 unità Deribit.
3. **DVOL corrente ≥ 35.** Sotto 35 il premio è troppo magro per
mantenere il rapporto fees/credit accettabile.
4. **DVOL corrente ≤ 90.** Sopra 90 si è in regime di stress, no entry.
5. **Nessun evento macro** (FOMC, CPI USA, NFP, ECB, Powell speech)
nei prossimi DTE giorni dall'apertura.
6. **Funding rate ETH-PERP** in valore assoluto annualizzato ≤ 80%
(filtra regimi di liquidazioni a cascata imminenti).
7. **Holdings ETH** in `cerbero-portfolio` non superiori al 30% del
patrimonio totale (correlazione direzionale già alta).
8. **Liquidità degli strike candidati** entro le soglie del §3.
Se anche **una sola** condizione fallisce → **no entry**, log con motivo,
ritento la settimana successiva.
## 3. Selezione struttura
### 3.1 Bias direzionale
Il bias è dedotto deterministicamente da due indicatori, mai in modo
discrezionale:
| Indicatore | Calcolo | Soglia bull | Soglia bear |
|---|---|---|---|
| Trend ETH 30g | (ETH_now / ETH_30g_ago - 1) × 100 | ≥ +5% | ≤ -5% |
| Funding cross-exchange | mediana funding annualizzato 4 maggiori exchange | ≥ +20% | ≤ -20% |
Risultato:
- Entrambi bull → **Bull Put Spread**.
- Entrambi bear → **Bear Call Spread**.
- Discordanti → **no entry** (mercato indeciso, edge incerto).
- Entrambi in range neutro `(-5%, +5%)` E DVOL ≥ 55 E ADX(14) < 20 →
**Iron Condor stretto** ammesso (vedi §6).
- Tutti gli altri casi neutri → **no entry**.
### 3.2 Strike short
Si seleziona lo strike short più vicino a delta target, in valore
assoluto.
| Parametro | Valore |
|---|---|
| Delta short target | 0.12 |
| Tolleranza delta | [0.10, 0.15] |
| Distanza minima OTM | 15% (anche se delta è dentro tolleranza) |
| Distanza massima OTM | 25% |
Se nessuno strike disponibile rientra in entrambe le tolleranze → no entry.
### 3.3 Strike long (protezione)
Si seleziona lo strike a distanza fissa dallo short:
| Parametro | Valore |
|---|---|
| Larghezza spread (in % di spot) | 4% |
| Larghezza minima accettabile | 3% |
| Larghezza massima accettabile | 5% |
Se non esiste lo strike esatto a 4%, si prende il più vicino entro
[3%, 5%]; altrimenti no entry.
### 3.4 Rapporto credito/larghezza
Il credito netto incassato (mid-price del combo) deve essere ≥ **30%**
della larghezza dello spread. Sotto questa soglia il rapporto
rischio/rendimento è inferiore al minimo accettabile. → no entry.
## 4. Liquidità (filtro hard pre-entry)
Per ciascuno strumento delle due gambe, **tutte** le condizioni:
| Misura | Soglia |
|---|---|
| Open interest | ≥ 100 contratti |
| Volume 24h sullo strumento | ≥ 20 contratti |
| Spread bid-ask | ≤ 15% del mid (≤ 10% per ATM, irrilevante qui) |
| Profondità book ai primi 3 livelli | ≥ 5 contratti aggregati |
Calcoliamo inoltre uno **slippage stimato** moltiplicando la larghezza
del bid-ask di entrambe le gambe per la size proposta. Se lo slippage
totale > 8% del credito atteso → no entry.
## 5. Sizing
### 5.1 Calcolo numero contratti
```
risk_target_usd = capitale_corrente * 0.13 # Quarter Kelly
risk_target_usd = min(risk_target_usd, 200_eur_in_usd) # cap Cerbero
n_contracts = floor(risk_target_usd / max_loss_per_contract)
n_contracts = min(n_contracts, 4) # cap aggregato implicito
```
`max_loss_per_contract` = larghezza spread × multiplier ETH (1 ETH).
### 5.2 Vincolo aggregato
Se `n_contracts × max_loss_per_contract + engagement_aperto_totale > 1.000_eur`
riduci `n_contracts` finché il vincolo è rispettato. Se risulta < 1 → no entry.
### 5.3 Volatilità di volatilità
Sizing dinamico per regime DVOL:
| DVOL corrente | Aggiustamento `n_contracts` |
|---|---|
| < 45 | × 1.0 (base) |
| 4560 | × 0.85 |
| 6080 | × 0.65 |
| > 80 | (no entry, vedi §2) |
Arrotondamento sempre per difetto a intero, minimo 1.
## 6. Iron Condor (ammesso solo in regime laterale)
Se le condizioni del §3.1 attivano IC, struttura:
- Bull Put Spread alle stesse regole §3.2/§3.3
- Bear Call Spread speculare (delta 0.12, 4% width)
- Sizing: `n_contracts` calcolato come sopra ma **dimezzato** per gamba
(rischio totale = somma max loss dei due lati, ma su ETH i due lati
non possono essere entrambi violati, quindi rischio effettivo = max
loss del lato peggiore + credito del lato vincente)
- Profit take: 25% del credito totale (più aggressivo)
- Tutto il resto delle regole §7-§9 si applica per lato.
## 7. Gestione attiva (decision loop ogni 12 ore)
Per ogni posizione aperta, il rule engine valuta in ordine:
1. **Profit take** — se mark price del combo ≤ 50% del credito incassato,
`decision = CLOSE_PROFIT`.
2. **Stop loss stretto** — se mark price del combo ≥ 250% del credito
(perdita = 1.5× credito), `decision = CLOSE_STOP`.
3. **Vol stop** — se DVOL corrente ≥ DVOL all'entry + 10,
`decision = CLOSE_VOL`.
4. **Time stop** — se `(scadenza - oggi) ≤ 7 giorni`,
`decision = CLOSE_TIME` (a meno che non siamo già al 50% di profit:
in quel caso teniamo per regola 1 in arrivo).
5. **Strike testato** — se `|delta_short_corrente| ≥ 0.30`,
`decision = CLOSE_DELTA`. Su ETH non si difende, si esce.
6. **Movimento esplosivo** — se `|return_4h_ETH| ≥ 5%` direzione avversa,
`decision = CLOSE_AVERSE` (azione conservativa, può essere finalizzata
alla prossima fenestra).
7. Altrimenti `decision = HOLD`.
L'ordine è importante: il primo trigger soddisfatto vince.
## 8. Esecuzione di apertura
1. Engine costruisce **combo order Deribit** (un solo ordine atomico
con due gambe). Mai due ordini separati.
2. Limit price = mid-price calcolato dall'engine.
3. Ordine inviato come limit GTC con time-in-force che scade alle
16:00 UTC dello stesso giorno.
4. Se non riempito entro 30 min, engine alza limit di 1 tick verso
ask combinato fino a un massimo di 3 step (poi cancella e log
"no fill").
5. Riempimento → engine persiste lo stato (vedi `05-data-model.md`)
includendo: spot ETH, DVOL, strike, scadenza, credit incassato,
timestamp.
## 9. Esecuzione di chiusura
1. Stessa logica di combo, in direzione inversa.
2. Limit di partenza al mid-price.
3. Se non riempito entro 30 min: alza limit fino a 3 step verso il
bid combinato. Su trigger `CLOSE_STOP` o `CLOSE_VOL` o `CLOSE_DELTA`
l'engine può accettare slippage maggiore (fino a 5 step) perché
l'urgenza prevale sul prezzo.
4. Riempimento → posizione marcata `CLOSED`, P&L calcolato e loggato,
record archiviato per Kelly recalibration mensile.
## 10. Cosa NON fa il rule engine — esplicito
- Non rolla mai una posizione (regola §7.5: si esce e basta).
- Non aggiunge gambe a una posizione esistente (no conversione in IC
durante un trade aperto).
- Non aggiusta strike o size dopo l'apertura.
- Non apre nuovi trade per "compensare" perdite recenti.
- Non opera fuori dalla finestra del lunedì 14:00 UTC, eccetto chiusure.
- Non deroga ai cap nemmeno per "opportunità eccezionali".
- Non si aggiorna automaticamente: nuovo set di regole = nuovo deploy
con review esplicita.
## 11. Riepilogo soglie (parametri di config)
Tutti i numeri sopra sono **parametrizzati** in `strategy.yaml` (vedi
`10-config-spec.md`). Modificarli è una decisione di Adriano con
giustificazione scritta nel commit message. La regola di default
fissata da questo documento è la **golden config** che il sistema parte
con al primo deploy.
+264
View File
@@ -0,0 +1,264 @@
# 02 — Architettura
## Vista a blocchi
```
┌─────────────────────────────────┐
│ ADRIANO (utente) │
└──────┬──────────────────────────┘
│ Telegram (report + conferma)
┌─────────────────────────────────────────────────────────────────┐
│ CERBERO BITE (rule engine) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ scheduler │ │ state store │ │ audit logger │ │
│ │ (APScheduler)│ │ (SQLite) │ │ (append-only) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ decision orchestrator (core/) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ entry │ │ sizing │ │ exit │ │ greeks │ │ │
│ │ │validator │ │ engine │ │ decision │ │ aggregator│ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │liquidity │ │ combo │ │ kelly │ │ │
│ │ │ gate │ │ builder │ │ recalib │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ MCP client wrappers (clients/) │ │
│ │ Deribit │ Hyperliquid │ Macro │ Sentiment │ Portfolio │ │
│ │ Memory │ Telegram │ Brain-Bridge │ │
│ └─────────────────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────────────┘
│ MCP JSON-RPC
┌───────────────────────────────┐ ┌───────────────────┐
│ MCP servers │ │ Cerbero core │
│ (esistenti in CerberoSuite) │───▶│ (esecuzione) │
└───────────────────────────────┘ └───────────────────┘
┌────────────────┐
│ Deribit │
│ API │
└────────────────┘
```
## Stack tecnologico
| Strato | Scelta | Motivazione |
|---|---|---|
| Linguaggio | Python 3.12+ | Coerente con `mcp_cerbero_brain` e l'ecosistema Cerbero |
| Async runtime | `asyncio` | Necessario per i client MCP |
| Scheduler | `APScheduler` | Cron-like + interval-based, in-process |
| Persistenza stato | SQLite (file singolo) | Zero deploy overhead, transazionale, sufficiente per ≤ 4 posizioni |
| Persistenza log | File `.jsonl` rotativi (gzip > 30g) | Audit-friendly, append-only, parseable |
| Validazione config | `pydantic` v2 | Schema a runtime su `strategy.yaml` |
| Test | `pytest` + `pytest-asyncio` + `hypothesis` (property-based) | TDD imposto |
| Type checking | `mypy --strict` | Disciplina, niente sorprese a runtime |
| Format/lint | `ruff` | Standard del progetto |
| Dependency manager | `uv` | Coerente con `mcp_cerbero_brain` |
| MCP client | SDK ufficiale `mcp` | Connessione ai server già configurati in `.mcp.json` |
| Notifiche | MCP `cerbero-telegram` | Riusa canale esistente |
| GUI | `streamlit` ≥ 1.40 + `plotly` | Dashboard locale, processo separato (vedi `11-gui-streamlit.md`) |
## Layout cartelle
```
Cerbero_Bite/
├── README.md
├── pyproject.toml
├── uv.lock
├── strategy.yaml # config golden
├── strategy.local.yaml.example # override locale (gitignored)
├── docs/ # questa documentazione
├── src/cerbero_bite/
│ ├── __init__.py
│ ├── __main__.py # entry point CLI
│ ├── core/ # algoritmi puri (no I/O)
│ │ ├── entry_validator.py
│ │ ├── sizing_engine.py
│ │ ├── exit_decision.py
│ │ ├── greeks_aggregator.py
│ │ ├── liquidity_gate.py
│ │ ├── combo_builder.py
│ │ └── kelly_recalibration.py
│ ├── clients/ # wrapper MCP
│ │ ├── deribit.py
│ │ ├── hyperliquid.py
│ │ ├── macro.py
│ │ ├── sentiment.py
│ │ ├── portfolio.py
│ │ ├── memory.py
│ │ ├── telegram.py
│ │ └── brain_bridge.py
│ ├── runtime/ # I/O, scheduling, orchestrazione
│ │ ├── scheduler.py
│ │ ├── orchestrator.py
│ │ ├── monitoring.py
│ │ └── alert_manager.py
│ ├── state/ # persistenza
│ │ ├── repository.py
│ │ ├── models.py # SQLAlchemy o dataclasses
│ │ └── migrations/
│ ├── config/ # caricamento e validazione yaml
│ │ ├── schema.py
│ │ └── loader.py
│ ├── reporting/ # report umani
│ │ ├── pre_trade.py
│ │ ├── post_trade.py
│ │ └── daily_digest.py
│ ├── gui/ # Streamlit dashboard (vedi 11-gui-streamlit.md)
│ │ ├── main.py
│ │ ├── pages/
│ │ ├── components/
│ │ └── data_layer.py
│ └── safety/ # kill switch, dead man, audit
│ ├── kill_switch.py
│ ├── dead_man.py
│ └── audit_log.py
├── tests/
│ ├── unit/ # test puri sui moduli core/
│ ├── integration/ # test con MCP fake
│ ├── golden/ # scenari deterministici di riferimento
│ └── fixtures/
├── scripts/
│ ├── reset_state.py
│ ├── replay_day.py # replay forensico di una giornata
│ └── recalibrate_kelly.py
└── data/
├── state.sqlite # gitignored
└── log/ # gitignored
```
## Principio di separazione
- **`core/`** contiene **funzioni pure**: input → output, niente I/O,
niente MCP, niente time. Testabili con dati statici. Sono il cuore
della strategia e devono restare riproducibili al byte.
- **`clients/`** contiene wrapper sui server MCP. Ogni client espone
una API tipizzata che usa internamente JSON-RPC. Mai logica di
business qui.
- **`runtime/`** orchestrazione: composizione di `clients/` + `core/` +
`state/`. È l'unico strato che può fare I/O e ha effetti collaterali.
- **`state/`** persistenza. Mai logica di business. Solo CRUD.
- **`config/`** caricamento di `strategy.yaml`, validazione, esposizione
immutabile dei parametri.
- **`reporting/`** generazione di stringhe per Telegram. Niente logica
di trading, solo formatting.
- **`safety/`** controlli trasversali (vedere `07-risk-controls.md`).
## Decision orchestrator — sequenza tipo per "Lunedì 14:00 UTC"
```python
async def evaluate_entry():
# 1. Stato sistema
if not safety.system_healthy(): return
if state.has_open_position(): return
# 2. Dati di mercato
spot = await deribit.get_index_price("ETH")
dvol = await deribit.get_dvol()
funding = await sentiment.get_funding_cross_exchange("ETH")
macro = await macro.get_calendar(days=18)
holdings = await portfolio.get_holdings()
# 3. Algoritmi puri
bias = entry_validator.compute_bias(spot, trend_30d, funding)
if bias == "no_entry": return log_and_skip()
capital = await portfolio.get_capital()
chain = await deribit.get_options_chain("ETH", dte_target=18)
short_strike, long_strike = combo_builder.select_strikes(
chain, bias, spot, config
)
if short_strike is None: return log_and_skip("no_strike")
if not liquidity_gate.passes(short_strike, long_strike, deribit_book):
return log_and_skip("illiquid")
n = sizing_engine.compute_contracts(
capital, max_loss_per_contract, dvol, config
)
if n < 1: return log_and_skip("undersize")
proposal = combo_builder.build(short_strike, long_strike, n, mid_price)
# 4. Conferma
await telegram.send(reporting.pre_trade(proposal))
confirmation = await wait_user_confirmation(timeout="60min")
if not confirmation.accepted: return log_and_skip("rejected")
# 5. Esecuzione via Cerbero core
instruction = combo_builder.to_cerbero_instruction(proposal)
await memory.push_user_instruction(instruction, source="cerbero-bite")
# 6. Stato
state.create_position(proposal, dvol_at_entry=dvol)
audit_logger.log("ENTRY_PROPOSED", proposal)
```
## Sequenza tipo per "monitoring 12h"
```python
async def evaluate_open_positions():
if not safety.system_healthy(): return
for position in state.list_open_positions():
spot = await deribit.get_index_price("ETH")
dvol = await deribit.get_dvol()
mark = await deribit.get_combo_mark(position.legs)
delta_short = await deribit.get_instrument(position.short_leg).delta
decision = exit_decision.evaluate(
position=position,
spot=spot,
dvol_now=dvol,
mark_now=mark,
delta_short_now=delta_short,
now=datetime.now(timezone.utc),
config=config,
)
if decision.action == "HOLD":
audit_logger.log("HOLD", position.id, decision.reason)
continue
await telegram.send(reporting.exit_proposal(position, decision))
confirmation = await wait_user_confirmation(timeout="30min")
if not confirmation.accepted:
audit_logger.log("EXIT_DEFERRED", position.id)
continue
await memory.push_user_instruction(
combo_builder.close_instruction(position),
source="cerbero-bite"
)
state.mark_closing(position.id)
```
## Failure modes e retry
| Modalità | Risposta |
|---|---|
| MCP non risponde (timeout) | Retry esponenziale 3 tentativi (1s, 5s, 30s); poi alert + skip ciclo |
| MCP risponde con dato palesemente rotto (es. orderbook tutti a 0) | Skip ciclo, alert |
| Confidence Adriano scaduta | Skip apertura; le chiusure restano in coda con alta priorità |
| Stato SQLite corrotto | Kill switch attivato, alert manuale richiesto |
| Cerbero core non riceve l'istruzione | Engine si aspetta ack via `cerbero-memory.get_pending`; se entro 5 min ack assente, alert e blocco apertura |
Vedi `07-risk-controls.md` per il dettaglio completo dei kill switch.
## Concorrenza e idempotenza
- Una sola istanza dell'engine alla volta (file lock su `data/.lockfile`).
- Tutti i ticker di scheduler sono **idempotenti**: se il sistema crasha
durante un'apertura, al riavvio il monitoring ricostruisce lo stato
da SQLite + Cerbero core (`cerbero-memory.get_state`) e prosegue.
- Ogni proposta ha un `proposal_id` UUID. Cerbero core ignora
duplicati con lo stesso id.
+460
View File
@@ -0,0 +1,460 @@
# 03 — Algoritmi core
Specifica dettagliata dei sette algoritmi puri che vivono in `src/cerbero_bite/core/`.
Sono **funzioni deterministiche**: stesso input → stesso output, niente I/O,
niente LLM. Ogni modulo è testabile in isolamento con dati statici.
Convenzione di tipo: tutti i moduli usano `pydantic` v2 per i record di
input/output, `Decimal` per i prezzi (mai `float` — vedi nota in §0.2).
## 0. Convenzioni
### 0.1 Tipi base
```python
from decimal import Decimal
from datetime import datetime
from typing import Literal
from pydantic import BaseModel
PutOrCall = Literal["P", "C"]
SpreadType = Literal["bull_put", "bear_call", "iron_condor"]
class OptionLeg(BaseModel):
instrument: str # es. "ETH-13MAY26-1900-P"
side: Literal["BUY", "SELL"]
strike: Decimal
expiry: datetime # UTC, scadenza Deribit (08:00 UTC)
type: PutOrCall
size: int # numero contratti
mid_price_eth: Decimal # mid in ETH (Deribit prices in ETH per inverse)
delta: Decimal
gamma: Decimal
theta: Decimal
vega: Decimal
```
### 0.2 Precisione numerica
I prezzi opzioni sono in ETH e si lavorano con `Decimal` ad alta
precisione. Mai `float` per quantità monetarie. Le greche sono ricevute
come float dal MCP Deribit; vengono convertite in `Decimal` con 6 cifre
all'ingresso del modulo.
### 0.3 Deterministic clock
Ogni algoritmo che dipende dal tempo riceve `now: datetime` come
parametro **esplicito**. Mai `datetime.now()` dentro `core/`. Questo
rende i test riproducibili.
---
## 1. `entry_validator`
**Responsabilità:** verificare che tutte le condizioni di entrata
elencate in `01-strategy-rules.md §2` siano soddisfatte. Restituisce
`pass | fail(reason)`.
```python
class EntryContext(BaseModel):
capital_usd: Decimal
dvol_now: Decimal
funding_perp_annualized: Decimal # ETH-PERP, frazione annualizzata
eth_holdings_pct_of_portfolio: Decimal
next_macro_event_in_days: int | None # None se nessun evento entro DTE
has_open_position: bool
class EntryDecision(BaseModel):
accepted: bool
reasons: list[str] # tutti i motivi di fallimento
def validate_entry(ctx: EntryContext, cfg: StrategyConfig) -> EntryDecision: ...
```
**Logica** (in ordine, accumula tutti i motivi di fallimento):
1. `has_open_position` → fail.
2. `capital_usd < 720` → fail.
3. `dvol_now < 35 or dvol_now > 90` → fail.
4. `next_macro_event_in_days is not None and next_macro_event_in_days <= dte_target` → fail.
5. `abs(funding_perp_annualized) > 0.80` → fail.
6. `eth_holdings_pct_of_portfolio > 0.30` → fail.
Restituisce `accepted = (len(reasons) == 0)`.
**Bias direzionale** (componente separato):
```python
class TrendContext(BaseModel):
eth_now: Decimal
eth_30d_ago: Decimal
funding_cross_annualized: Decimal # mediana 4 exchange
dvol_now: Decimal
adx_14: Decimal
def compute_bias(ctx: TrendContext, cfg: StrategyConfig) -> SpreadType | None: ...
```
Restituisce `"bull_put"` / `"bear_call"` / `"iron_condor"` / `None`
secondo le tabelle del §3.1 della strategia.
---
## 2. `liquidity_gate`
**Responsabilità:** validare la liquidità degli strumenti delle due
gambe e stimare lo slippage atteso.
```python
class InstrumentSnapshot(BaseModel):
instrument: str
bid: Decimal
ask: Decimal
mid: Decimal
open_interest: int
volume_24h: int
book_depth_top3: int # somma size primi 3 livelli (lato vicino)
class LiquidityCheck(BaseModel):
accepted: bool
reasons: list[str]
estimated_slippage_pct_of_credit: Decimal
def check(legs: list[InstrumentSnapshot],
credit: Decimal,
n_contracts: int,
cfg: StrategyConfig) -> LiquidityCheck: ...
```
**Soglie** (parametri da `cfg`, default in `01-strategy-rules.md §4`):
- Per ogni leg: `OI >= 100`, `volume_24h >= 20`,
`(ask - bid) / mid <= 0.15`, `book_depth_top3 >= 5`.
- `slippage_total_eth = (ask_short - mid_short) * n + (mid_long - bid_long) * n`
- `estimated_slippage_pct_of_credit = slippage_total_eth / credit`
- Reject se > 0.08 (8%).
---
## 3. `sizing_engine`
**Responsabilità:** calcolare il numero di contratti rispettando Quarter
Kelly, cap Cerbero, vincolo aggregato e aggiustamento volatilità.
```python
class SizingContext(BaseModel):
capital_usd: Decimal
max_loss_per_contract_usd: Decimal # = spread_width × 1 ETH
dvol_now: Decimal
open_engagement_usd: Decimal # somma max_loss aperti su CB
eur_to_usd: Decimal # tasso live, per cap 200 EUR
other_open_positions: int # da CB, escluso il nuovo
class SizingResult(BaseModel):
n_contracts: int
risk_dollars: Decimal
reason_if_zero: str | None
def compute_contracts(ctx: SizingContext, cfg: StrategyConfig) -> SizingResult: ...
```
**Logica:**
```
risk_target = ctx.capital_usd * cfg.kelly_fraction # 0.13
cap_per_trade_usd = 200_eur * ctx.eur_to_usd
risk_target = min(risk_target, cap_per_trade_usd)
# aggiustamento DVOL
if ctx.dvol_now < 45: adj = 1.00
elif ctx.dvol_now < 60: adj = 0.85
elif ctx.dvol_now < 80: adj = 0.65
else: return zero("dvol > 80, no entry")
risk_target *= adj
n = floor(risk_target / ctx.max_loss_per_contract_usd)
n = min(n, cfg.max_contracts_per_trade) # default 4
# vincolo engagement aggregato
cap_aggregate_usd = 1000_eur * ctx.eur_to_usd
while n > 0 and (n * ctx.max_loss_per_contract + ctx.open_engagement) > cap_aggregate_usd:
n -= 1
# vincolo numero posizioni concorrenti
if ctx.other_open_positions >= cfg.max_concurrent_positions: return zero("too many positions")
if n < 1: return zero("undersize")
return SizingResult(n_contracts=n, risk_dollars=n * max_loss_per_contract)
```
**Test obbligatori (TDD):**
- Capitale 720, max_loss/contratto 93, dvol 40 → 1 contratto
- Capitale 1500, dvol 50 → adj 0.85; 13% × 1500 = 195; 195 × 0.85 = 165; 165/93 ≈ 1
- Capitale 1500, dvol 40 → 13% × 1500 = 195, cap 200 EUR; 195/93 ≈ 2
- Capitale 5000, dvol 40 → 13% × 5000 = 650, cap 200 EUR ≈ 215 USD; 215/93 ≈ 2 (cap morde)
- Capitale 100000, dvol 40 → cap 215 USD; 2 contratti (cap saturo)
---
## 4. `combo_builder`
**Responsabilità:** dato un bias e una option chain, selezionare i due
strike e costruire il combo order ready-to-submit.
```python
class StrikeCandidate(BaseModel):
strike: Decimal
delta: Decimal
instrument: str
class ComboProposal(BaseModel):
proposal_id: UUID
spread_type: SpreadType
legs: list[OptionLeg]
credit_target_eth: Decimal
credit_target_usd: Decimal
max_loss_eth: Decimal
max_loss_usd: Decimal
breakeven: Decimal
spot_at_proposal: Decimal
dvol_at_proposal: Decimal
expiry: datetime
def select_strikes(chain: list[InstrumentSnapshot],
bias: SpreadType,
spot: Decimal,
dte_target: int,
cfg: StrategyConfig) -> tuple[StrikeCandidate, StrikeCandidate] | None: ...
def build(short: InstrumentSnapshot,
long: InstrumentSnapshot,
n_contracts: int,
spot: Decimal,
dvol: Decimal,
cfg: StrategyConfig) -> ComboProposal: ...
def to_cerbero_instruction(proposal: ComboProposal) -> CerberoInstruction: ...
def close_instruction(position: OpenPosition, reason: str) -> CerberoInstruction: ...
```
**Selezione short strike:**
1. Filtra chain per `expiry` con DTE compreso in `[14, 21]` (preferito 18).
2. Filtra per tipo (`P` per bull_put, `C` per bear_call).
3. Calcola distanza percentuale dallo spot e ritieni solo strike con
distanza in `[15%, 25%]`.
4. Tra questi, scegli quello con `|delta|` più vicino a 0.12 (range
accettato `[0.10, 0.15]`).
5. Se nessuno → `None`.
**Selezione long strike:**
1. Larghezza target: `spot * 0.04`. Range accettato `[0.03 × spot, 0.05 × spot]`.
2. Per bull_put: `long_strike = short_strike - width`. Trova lo strike
esistente più vicino.
3. Verifica che la distanza effettiva sia nel range; altrimenti `None`.
4. Per bear_call: simmetrico, `long_strike = short_strike + width`.
**Verifica credit ratio:**
- `credit = mid_short - mid_long` (ETH).
- Se `credit / width < 0.30``None` (spec §3.4).
---
## 5. `greeks_aggregator`
**Responsabilità:** dato un set di legs, calcolare le greche aggregate
nette dello spread.
```python
class AggregateGreeks(BaseModel):
delta_net: Decimal
gamma_net: Decimal
theta_net: Decimal # giornaliero, in USD
vega_net: Decimal # per 1 punto IV
def aggregate(legs: list[OptionLeg], spot: Decimal, eth_price_usd: Decimal) -> AggregateGreeks: ...
```
**Logica:**
```
delta_net = sum( sign(side) * size * leg.delta for leg in legs )
# sign(side): +1 per BUY, -1 per SELL
# (analogo per gamma, theta, vega)
```
Theta convertito in USD: `theta_eth × eth_price_usd × multiplier`.
**Uso:** alimentazione del report pre-trade e check di sanità in
monitoring (vedi `01-strategy-rules.md §7.3` — il vol_stop usa DVOL,
non vega aggregata, ma vega aggregata serve per validazione).
---
## 6. `exit_decision`
**Responsabilità:** dato lo stato di una posizione aperta + dati
correnti, decidere se chiudere e perché.
```python
class PositionSnapshot(BaseModel):
proposal_id: UUID
spread_type: SpreadType
legs: list[OptionLeg]
credit_received_eth: Decimal
credit_received_usd: Decimal
spot_at_entry: Decimal
dvol_at_entry: Decimal
expiry: datetime
opened_at: datetime
eth_price_usd_now: Decimal
spot_now: Decimal
dvol_now: Decimal
mark_combo_now_eth: Decimal # mark price del combo (debito necessario per chiudere)
delta_short_now: Decimal
return_4h_now: Decimal # ritorno ETH a 4h
now: datetime
ExitAction = Literal["HOLD", "CLOSE_PROFIT", "CLOSE_STOP", "CLOSE_VOL",
"CLOSE_TIME", "CLOSE_DELTA", "CLOSE_AVERSE"]
class ExitDecisionResult(BaseModel):
action: ExitAction
reason: str
pnl_estimate_eth: Decimal
pnl_estimate_usd: Decimal
def evaluate(snapshot: PositionSnapshot, cfg: StrategyConfig) -> ExitDecisionResult: ...
```
**Logica** (ordine vincolante, primo match vince):
```python
days_to_expiry = (snapshot.expiry - snapshot.now).total_seconds() / 86400
debit_needed = snapshot.mark_combo_now_eth # ETH
profit_take_threshold = snapshot.credit_received_eth * 0.50
# 1. Profit take
if debit_needed <= profit_take_threshold:
return CLOSE_PROFIT
# 2. Stop loss stretto
if debit_needed >= snapshot.credit_received_eth * 2.5: # spec: stop a 1.5× credito
return CLOSE_STOP
# 3. Vol stop
if snapshot.dvol_now >= snapshot.dvol_at_entry + 10:
return CLOSE_VOL
# 4. Time stop (con eccezione "quasi a profit")
if days_to_expiry <= 7:
if debit_needed > profit_take_threshold * 0.7: # NON siamo prossimi al 50% di profit
return CLOSE_TIME
# 5. Strike testato
if abs(snapshot.delta_short_now) >= 0.30:
return CLOSE_DELTA
# 6. Movimento esplosivo direzione avversa
adverse_move = (snapshot.return_4h_now < -0.05 if spread_type == "bull_put"
else snapshot.return_4h_now > 0.05)
if adverse_move:
return CLOSE_AVERSE
return HOLD
```
**Calcolo P&L stimato:**
```
realized_pnl_eth = (credit_received - debit_needed) * n_contracts
realized_pnl_usd = realized_pnl_eth * eth_price_usd_now
```
**Test obbligatori:**
- Posizione con mark = 50% credito → CLOSE_PROFIT
- Posizione con mark = 250% credito → CLOSE_STOP
- DVOL +12 dall'entry → CLOSE_VOL
- 6 giorni a scadenza, mark = 80% credito → CLOSE_TIME
- 6 giorni a scadenza, mark = 30% credito → HOLD (eccezione)
- delta short = -0.32 → CLOSE_DELTA
- Tutto OK ma return -7% in 4h → CLOSE_AVERSE
- Tutto neutro → HOLD
---
## 7. `kelly_recalibration`
**Responsabilità:** ricalcolare il `kelly_fraction` ogni 30 giorni
basandosi sui trade reali chiusi.
```python
class TradeRecord(BaseModel):
proposal_id: UUID
pnl_usd: Decimal
risk_usd: Decimal
closed_at: datetime
outcome: ExitAction
class KellyResult(BaseModel):
win_rate: Decimal
avg_win_pct_risk: Decimal
avg_loss_pct_risk: Decimal
full_kelly_pct: Decimal
quarter_kelly_pct: Decimal
sample_size: int
recommended_fraction: Decimal
confidence: Literal["low", "medium", "high"] # da sample size
def recalibrate(trades: list[TradeRecord], cfg: StrategyConfig) -> KellyResult: ...
```
**Logica:**
- Ignora trade con `closed_at` più vecchi di 365 giorni.
- Win rate = trade con pnl > 0 / totali.
- Avg win = media dei pnl positivi normalizzata sul risk.
- Avg loss = media dei pnl negativi normalizzata sul risk (in valore assoluto).
- `b = avg_win / avg_loss`
- `full_kelly = (win_rate × b - (1 win_rate)) / b` (clip a 0 se negativo)
- `quarter_kelly = full_kelly × 0.25`
- `recommended_fraction`:
- Se `sample_size < 30` → mantieni `cfg.kelly_fraction` (no update).
- Se 30 ≤ sample < 100 → media pesata 50/50 con cfg corrente.
- Se ≥ 100 → adotta `quarter_kelly`.
- `confidence`: low (<30), medium (30-99), high (≥100).
Output non viene mai applicato automaticamente: il sistema produce un
report mensile, Adriano decide se aggiornare `strategy.yaml`.
---
## Cross-cutting: ordine di chiamata
Il decision orchestrator chiama gli algoritmi in questa sequenza
canonica per l'apertura:
```
1. entry_validator.validate_entry (filtri base)
2. entry_validator.compute_bias (direzione)
3. combo_builder.select_strikes (struttura)
4. liquidity_gate.check (eseguibilità)
5. sizing_engine.compute_contracts (size)
6. combo_builder.build (proposta finale)
7. greeks_aggregator.aggregate (validazione e report)
```
Per il monitoring (12h):
```
1. exit_decision.evaluate (decisione)
2. greeks_aggregator.aggregate (per il report di chiusura)
```
Ogni passo ha precondizioni esplicite documentate nella docstring del
modulo. Il loro ordine è bloccato perché ciascuno consuma output del
precedente.
+243
View File
@@ -0,0 +1,243 @@
# 04 — MCP Integration
Tutti i server MCP sono già configurati a livello di `CerberoSuite` (vedi
`Cerbero_Office/.mcp.json`). Cerbero Bite vi si connette come **client
MCP** usando l'SDK ufficiale `mcp` per Python.
## Configurazione di connessione
Cerbero Bite legge `~/.config/cerbero-suite/mcp.json` (o, in dev, da
`.mcp.json` locale puntato via env var `CERBERO_BITE_MCP_CONFIG`). I
server vengono risolti **per nome** dichiarato nel file di config.
```python
# clients/_base.py — abstract
class McpClient:
name: str # "cerbero-deribit", ecc.
timeout_s: float = 8.0
retry_max: int = 3
retry_base_delay: float = 1.0 # esponenziale
async def call(self, tool: str, **params) -> dict: ...
```
Ogni wrapper concreto eredita `McpClient` ed espone metodi tipizzati.
La logica di retry e timeout è centralizzata.
## Server MCP usati
### `cerbero-deribit`
Tool consumati:
| Tool | Uso | Frequenza |
|---|---|---|
| `get_index_price(asset="ETH")` | Spot ETH per calcolo strike | Ogni ciclo entry + monitor |
| `get_dvol()` | Volatilità implicita aggregata ETH | Ogni ciclo entry + monitor |
| `get_options_chain(asset, expiry_window)` | Lista strumenti per dato DTE | Solo entry |
| `get_instrument(instrument_name)` | Mid, bid, ask, greche su singolo strumento | Entry + monitor |
| `get_orderbook(instrument_name, depth=5)` | Profondità per liquidity_gate e slippage | Solo entry |
| `get_combo_mark(legs)` | Mark price del combo (debito di chiusura) | Solo monitor |
| `get_account_summary(currency="USDC")` | Equity Deribit, margin libero | Periodico |
Wrapper:
```python
# clients/deribit.py
class DeribitClient(McpClient):
name = "cerbero-deribit"
async def index_price(self, asset: str) -> Decimal: ...
async def dvol(self) -> Decimal: ...
async def options_chain(self, asset: str, dte_min: int, dte_max: int) -> list[InstrumentSnapshot]: ...
async def instrument(self, name: str) -> InstrumentSnapshot: ...
async def orderbook(self, name: str, depth: int = 5) -> OrderbookSnapshot: ...
async def combo_mark(self, legs: list[OptionLeg]) -> Decimal: ...
async def account_summary(self) -> AccountSummary: ...
```
**Note:**
- Tutti i prezzi sono ricevuti come float dal MCP, convertiti in
`Decimal` con `quantize` a 6 cifre nel wrapper.
- Greche convertite con la stessa quantizzazione.
- Se `mark_iv = 7%` o `300%` o `bid = 0` su orderbook ATM su tutti gli
strumenti → wrapper solleva `DeribitDataAnomalyError` (probabile
testnet o feed rotto). Il decision orchestrator cattura, alert,
skippa il ciclo.
### `cerbero-hyperliquid`
| Tool | Uso |
|---|---|
| `get_perp_funding_rate(asset="ETH")` | Filtro entry §2.6 |
| `get_perp_summary(asset="ETH")` | Volume 24h, conferma liquidità correlata |
| `get_account_summary()` | Solo per coerenza, non usato in decision loop |
```python
class HyperliquidClient(McpClient):
async def funding_rate_annualized(self, asset: str) -> Decimal: ...
async def perp_summary(self, asset: str) -> PerpSummary: ...
```
### `cerbero-sentiment`
| Tool | Uso |
|---|---|
| `get_funding_cross_exchange(asset="ETH")` | Bias direzionale §3.1 (mediana 4 maggiori) |
Le news qualitative **non sono usate** nel decision loop (no LLM).
Vengono eventualmente lette da Adriano in occasione del report
settimanale.
```python
class SentimentClient(McpClient):
async def funding_cross_median(self, asset: str) -> Decimal: ...
```
### `cerbero-macro`
| Tool | Uso |
|---|---|
| `get_calendar(days_ahead=18)` | Filtro eventi macro pre-entry |
Eventi rilevanti (filtra per `severity = high` e `country in {US, EU}`):
FOMC, FED minutes, CPI, NFP, ECB, GDP, Powell speech, Lagarde speech.
```python
class MacroClient(McpClient):
async def upcoming_events(self, days_ahead: int) -> list[MacroEvent]: ...
async def first_high_severity_within(self, days: int) -> int | None:
"""Days until first high-severity event, None if none in window."""
```
### `cerbero-portfolio`
| Tool | Uso |
|---|---|
| `get_holdings()` | Capitale corrente complessivo |
| `get_holdings_by_asset()` | Filtro entry §2.7 (ETH < 30% portfolio) |
| `get_correlation()` | Sanity check, non bloccante |
```python
class PortfolioClient(McpClient):
async def total_equity_usd(self) -> Decimal: ...
async def asset_pct(self, asset: str) -> Decimal: ...
```
### `cerbero-memory`
| Tool | Uso |
|---|---|
| `push_user_instruction(payload, source="cerbero-bite")` | Invio istruzione apertura/chiusura a Cerbero core |
| `get_pending(source="cerbero-bite")` | Verifica ack di Cerbero core |
```python
class MemoryClient(McpClient):
async def push_instruction(self, instruction: CerberoInstruction) -> str:
"""Returns instruction_id."""
async def is_acknowledged(self, instruction_id: str) -> bool: ...
```
**Payload `CerberoInstruction`** (schema condiviso con Cerbero core,
documentato in `Cerbero/prompt.base v4`):
```json
{
"source": "cerbero-bite",
"kind": "open_combo" | "close_combo",
"exchange": "deribit",
"asset": "ETH",
"proposal_id": "uuid-...",
"legs": [
{"instrument": "ETH-13MAY26-1900-P", "side": "SELL", "size": 2,
"limit_price_eth": "0.0048"},
{"instrument": "ETH-13MAY26-1810-P", "side": "BUY", "size": 2,
"limit_price_eth": "0.0021"}
],
"limit_combo_eth": "0.0027",
"tif": "GTC",
"expires_at": "2026-04-27T16:00:00Z",
"max_slippage_eth": "0.0005",
"reason": "weekly_open" | "profit_take" | "stop_loss" | "vol_stop" |
"time_stop" | "delta_breach" | "adverse_move",
"milestone": "advisory_only" | "approved_by_user"
}
```
Cerbero core deduplica per `proposal_id` (idempotenza in caso di retry).
### `cerbero-telegram`
| Tool | Uso |
|---|---|
| `send_message(text, parse_mode="MarkdownV2")` | Report pre/post trade, alert |
| `send_with_buttons(text, buttons)` | Conferma ad Adriano (yes/no) |
Le conferme devono ritornare entro 60 minuti (entry) o 30 minuti (exit).
Implementazione: l'engine si mette in `await` su una coda interna
alimentata dal callback Telegram via webhook locale.
```python
class TelegramClient(McpClient):
async def send(self, text: str, parse_mode: str = "MarkdownV2") -> int: ...
async def request_confirmation(self, text: str, timeout_s: int) -> bool: ...
```
### `cerbero-brain-bridge`
| Tool | Uso |
|---|---|
| `kb_search(query)` | Lookup pre-trade su pattern simili (consultivo) |
| `kb_read(path)` | Lettura nota wiki specifica |
| `kb_write(path, content)` | Salvataggio learning post-trade |
**Importante:** il brain-bridge non partecipa al decision loop. Le
chiamate `kb_search` sono **consultive** e i risultati allegati al
report di Adriano per contesto, mai consumati come input ai filtri
deterministici.
```python
class BrainBridgeClient(McpClient):
async def search(self, query: str, limit: int = 5) -> list[KbHit]: ...
async def write_note(self, path: str, content: str) -> None: ...
```
### `cerbero-scheduler`
**Non usato** dal decision loop. Cerbero Bite ha il proprio scheduler
APScheduler interno. Il MCP scheduler resta a disposizione del core
Cerbero per altre routine.
## Errori e degradation
| Server down | Comportamento |
|---|---|
| `cerbero-deribit` | Skip ciclo entry; per monitor → alert e marca posizione come `unknown_state` (non chiude alla cieca) |
| `cerbero-hyperliquid` | Skip filtro funding §2.6 con warning; entry può proseguire se altre condizioni soddisfatte |
| `cerbero-sentiment` | Bias §3.1 cade in `no_entry` per default (no funding cross → niente direzione) |
| `cerbero-macro` | **Hard fail**: senza calendar non si apre. È un filtro irrinunciabile |
| `cerbero-portfolio` | Skip filtro §2.7 con warning; sizing usa ultimo capitale noto da SQLite con warning |
| `cerbero-memory` | Hard fail per esecuzione: senza push_user_instruction non si può aprire/chiudere |
| `cerbero-telegram` | Skip ciclo: senza canale di conferma niente proposta |
| `cerbero-brain-bridge` | Skip lookup, log warning. Mai bloccante |
Ogni "hard fail" → alert sonoro su Telegram via canale di backup
(BotPapà), kill switch armato fino al ripristino.
## Versioning
Cerbero Bite verifica all'avvio la versione di ciascun MCP via
`get_version()` (tool standard). Schema di versioning attesa:
```python
EXPECTED_MCP_VERSIONS = {
"cerbero-deribit": "^2.0.0",
"cerbero-hyperliquid": "^1.5.0",
"cerbero-memory": "^4.0.0",
"cerbero-portfolio": "^1.2.0",
...
}
```
Mismatch → kill switch e alert manuale. Mai partire con MCP a versione
incompatibile.
+241
View File
@@ -0,0 +1,241 @@
# 05 — Data Model
Persistenza locale su SQLite (`data/state.sqlite`) + log append-only su
file `.jsonl`. Niente database remoti, niente broker esterni.
## Filosofia
- **Lo stato è la verità per ciò che è in volo.** Posizioni aperte,
proposte in attesa di conferma, ack di Cerbero core.
- **Il log è la verità per ciò che è successo.** Ogni evento (entry,
exit, errore, decisione di hold) viene scritto in modo immutabile
prima di toccare lo stato.
- Lo stato si può sempre **ricostruire dal log** in caso di corruzione.
## Schema SQLite
Migrazione gestita con file SQL semplici sotto `state/migrations/0001_init.sql`,
applicati in sequenza. No ORM heavy: si usa `sqlalchemy.core` o
addirittura `sqlite3` nativo con piccole utility.
### `positions`
Posizione Cerberus Bite (aperta, in chiusura, chiusa).
```sql
CREATE TABLE positions (
proposal_id TEXT PRIMARY KEY, -- UUID v4
spread_type TEXT NOT NULL, -- bull_put, bear_call, iron_condor
asset TEXT NOT NULL DEFAULT 'ETH',
expiry TEXT NOT NULL, -- ISO8601 UTC
short_strike NUMERIC NOT NULL,
long_strike NUMERIC NOT NULL,
short_instrument TEXT NOT NULL,
long_instrument TEXT NOT NULL,
n_contracts INTEGER NOT NULL,
spread_width_usd NUMERIC NOT NULL,
spread_width_pct NUMERIC NOT NULL,
credit_eth NUMERIC NOT NULL, -- ricevuto effettivo (post fill)
credit_usd NUMERIC NOT NULL,
max_loss_usd NUMERIC NOT NULL,
spot_at_entry NUMERIC NOT NULL,
dvol_at_entry NUMERIC NOT NULL,
delta_at_entry NUMERIC NOT NULL,
eth_price_at_entry NUMERIC NOT NULL,
proposed_at TEXT NOT NULL,
opened_at TEXT, -- NULL se non ancora confermato fill
closed_at TEXT, -- NULL se aperta
close_reason TEXT, -- ENUM exit action
debit_paid_eth NUMERIC, -- pagato a chiusura
pnl_eth NUMERIC,
pnl_usd NUMERIC,
status TEXT NOT NULL, -- proposed, awaiting_fill, open, closing, closed, cancelled
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX idx_positions_status ON positions(status);
CREATE INDEX idx_positions_closed_at ON positions(closed_at);
```
Stati validi:
```
proposed → proposta inviata a Adriano, in attesa di conferma
awaiting_fill → istruzione push_user_instruction inviata, in attesa fill
open → fill confermato, monitoring attivo
closing → istruzione close inviata, in attesa fill
closed → close fill confermato, P&L calcolato
cancelled → proposta rifiutata da Adriano o no fill
```
### `instructions`
Tracciamento di ogni `push_user_instruction` inviata a Cerbero core.
```sql
CREATE TABLE instructions (
instruction_id TEXT PRIMARY KEY, -- UUID dato a Cerbero core
proposal_id TEXT NOT NULL REFERENCES positions(proposal_id),
kind TEXT NOT NULL, -- open_combo, close_combo
payload_json TEXT NOT NULL, -- JSON dell'istruzione completa
sent_at TEXT NOT NULL,
acknowledged_at TEXT,
filled_at TEXT,
cancelled_at TEXT,
actual_fill_eth NUMERIC, -- prezzo medio fill effettivo
actual_fees_eth NUMERIC
);
CREATE INDEX idx_instructions_proposal ON instructions(proposal_id);
```
### `decisions`
Storia delle valutazioni del decision loop (anche quando l'esito è
HOLD o no_entry). Serve per audit e analisi delle scelte.
```sql
CREATE TABLE decisions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
decision_type TEXT NOT NULL, -- entry_check, exit_check, kelly_recalib
proposal_id TEXT, -- NULL per entry_check senza proposta
timestamp TEXT NOT NULL,
inputs_json TEXT NOT NULL, -- snapshot input al modulo core
outputs_json TEXT NOT NULL, -- output del modulo core
action_taken TEXT, -- HOLD, no_entry, propose_open, propose_close
notes TEXT
);
CREATE INDEX idx_decisions_timestamp ON decisions(timestamp);
CREATE INDEX idx_decisions_proposal ON decisions(proposal_id);
```
### `dvol_history`
Snapshot DVOL ad ogni evaluation (utile per analisi di lungo periodo).
```sql
CREATE TABLE dvol_history (
timestamp TEXT PRIMARY KEY,
dvol NUMERIC NOT NULL,
eth_spot NUMERIC NOT NULL
);
```
### `manual_actions`
Coda di azioni manuali generate dalla GUI Streamlit (vedi `11-gui-streamlit.md`).
L'engine consuma le righe non processate via job APScheduler ogni 30s.
```sql
CREATE TABLE manual_actions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL, -- approve_proposal, reject_proposal, force_close, arm_kill, disarm_kill
proposal_id TEXT, -- NULL se l'azione non è legata a una proposta
payload_json TEXT, -- JSON con motivo, conferma typed, ecc.
created_at TEXT NOT NULL,
consumed_at TEXT, -- NULL = ancora da processare
consumed_by TEXT,
result TEXT
);
CREATE INDEX idx_manual_actions_unconsumed ON manual_actions(consumed_at);
```
Le `manual_actions` non bypassano i risk control: il consumer applica
gli stessi check di `safety.system_healthy()` prima di eseguire.
### `system_state`
Singleton di stato globale (kill switch, last health check).
```sql
CREATE TABLE system_state (
id INTEGER PRIMARY KEY CHECK (id = 1),
kill_switch INTEGER NOT NULL DEFAULT 0,
kill_reason TEXT,
kill_at TEXT,
last_health_check TEXT NOT NULL,
last_kelly_calib TEXT,
config_version TEXT NOT NULL,
started_at TEXT NOT NULL
);
```
## Log file
Sotto `data/log/` un file per giorno: `cerbero-bite-YYYY-MM-DD.jsonl`.
Ogni linea è un evento JSON.
### Schema evento
```json
{
"ts": "2026-04-27T14:00:01.234Z",
"event": "ENTRY_PROPOSED",
"level": "INFO",
"module": "runtime.orchestrator",
"proposal_id": "uuid-...",
"data": { ... },
"decision_id": 12345
}
```
### Eventi previsti
| Event | Quando |
|---|---|
| `ENGINE_START` | Avvio engine |
| `ENGINE_STOP` | Stop pulito |
| `HEALTH_OK` / `HEALTH_DEGRADED` | Health check periodico |
| `MCP_CALL` | Ogni chiamata MCP (livello DEBUG) |
| `MCP_FAIL` | Fallimento MCP (livello WARN/ERROR) |
| `ENTRY_EVALUATED` | Ciclo settimanale completato |
| `ENTRY_REJECTED` | Filtri non passati |
| `ENTRY_PROPOSED` | Proposta inviata a Telegram |
| `ENTRY_CONFIRMED` | Adriano conferma |
| `ENTRY_REJECTED_BY_USER` | Adriano nega |
| `ENTRY_TIMEOUT` | Adriano non risponde nei 60 min |
| `INSTRUCTION_SENT` | push_user_instruction completato |
| `INSTRUCTION_ACK` | Cerbero core ack |
| `FILL_CONFIRMED` | Posizione effettivamente aperta |
| `EXIT_EVALUATED` | Ciclo monitor completato (anche HOLD) |
| `EXIT_PROPOSED` | Proposta chiusura |
| `EXIT_CONFIRMED` | Adriano conferma chiusura |
| `EXIT_FILLED` | Fill chiusura confermato |
| `KILL_SWITCH_ARMED` | Disarm manuale richiesto |
| `KILL_SWITCH_DISARMED` | Manuale via CLI |
| `KELLY_RECALIBRATED` | Output report mensile |
### Rotation e ritenzione
- File giornaliero, gzip dopo 30 giorni.
- Ritenzione 365 giorni; oltre, archiviazione manuale.
- Mai cancellati i log relativi a posizioni con `pnl != 0`: archivio
permanente per fini fiscali.
## Stato in memoria
L'engine mantiene una cache delle posizioni aperte in RAM, ricaricata
da SQLite all'avvio e refreshed dopo ogni transizione di stato.
Ogni mutazione segue il pattern:
1. Append evento al log file (fsync).
2. Begin transaction SQLite.
3. Update tabelle.
4. Commit.
5. Update cache RAM.
Se il sistema crasha tra (1) e (5), al restart un riconciliatore confronta
log vs SQLite e ripristina la consistenza.
## Migrations
Lo schema viene tracciato con un counter `pragma user_version`. La
prima volta `0001_init.sql` viene applicato e versione → 1. Aggiunte
future incrementano. Nessun rollback supportato (migrations forward-only).
## Backup
Backup orari di `state.sqlite` in `data/backups/state-YYYYMMDD-HH.sqlite`,
ritenuti per 30 giorni. Operazione idempotente con SQLite `VACUUM INTO`.
+216
View File
@@ -0,0 +1,216 @@
# 06 — Flussi operativi
Tre flussi principali, tutti orchestrati dallo scheduler interno
APScheduler.
## Flusso 1 — Avvio engine
```
1. Carica strategy.yaml + strategy.local.yaml (override)
2. Validazione schema (pydantic) — fail veloce
3. Acquisisci lock file (data/.lockfile)
4. Apri SQLite, esegui migrations
5. Health check MCP server (versioni + ping)
6. Riconciliatore stato:
a. legge positions con status in {awaiting_fill, open, closing}
b. per ognuna chiama cerbero-memory.is_acknowledged + Deribit get_positions
c. allinea SQLite alla realtà del broker (sempre la realtà vince)
d. log delle discrepanze come WARNING
7. Health check sistema OK? → arma scheduler
KO? → kill switch + alert + idle
8. Log ENGINE_START
```
L'avvio è progettato per essere **safe**: se qualcosa non torna, il
sistema si rifiuta di operare. Mai partire con uno stato dubbio.
## Flusso 2 — Settimanale (entry)
Trigger: cron `0 14 * * MON` (lunedì 14:00 UTC).
```
START
├── safety.system_healthy()? → no → log + skip
├── state.has_open_position()? → yes → log + skip
├── snapshot dati di mercato (parallel):
│ spot, dvol, funding_perp, funding_cross, macro_calendar,
│ holdings_pct, capital
├── entry_validator.validate_entry → fail → log ENTRY_REJECTED + reasons
├── entry_validator.compute_bias → None → log ENTRY_REJECTED ("no_bias")
├── deribit.get_options_chain (DTE 14-21)
├── combo_builder.select_strikes → None → log ENTRY_REJECTED ("no_strike")
├── deribit.get_orderbook per le 2 gambe
├── liquidity_gate.check → fail → log ENTRY_REJECTED ("illiquid")
├── sizing_engine.compute_contracts → 0 → log ENTRY_REJECTED ("undersize")
├── combo_builder.build → ComboProposal
├── greeks_aggregator.aggregate
├── reporting.pre_trade(proposal) → testo Telegram
├── telegram.request_confirmation(timeout=60min)
│ ├── confermato:
│ │ ├── state.create_position (status="proposed")
│ │ ├── memory.push_user_instruction
│ │ ├── state.update(status="awaiting_fill")
│ │ └── log ENTRY_CONFIRMED
│ ├── rifiutato:
│ │ └── log ENTRY_REJECTED_BY_USER
│ └── timeout:
│ └── log ENTRY_TIMEOUT
└── END
```
### Tempo previsto end-to-end
- Snapshot dati: 3-5 secondi
- Algoritmi puri: < 0.5 secondi
- Conferma utente: variabile (max 60 min)
Il critical path automatico è **sotto 10 secondi**. La latenza umana
non rallenta i prezzi: se Adriano risponde dopo 30 minuti, l'engine
prima di inviare l'istruzione **rivaluta** spot, mid e dvol e abortisce
l'apertura se le condizioni sono cambiate (slippage > 8% o dvol fuori
banda o macro nuova).
## Flusso 3 — Monitoring (12h)
Trigger: cron `0 2,14 * * *` (02:00 e 14:00 UTC, ogni giorno).
```
START
├── safety.system_healthy()? → no → kill switch (no auto-close ciechi)
├── per ogni position con status="open":
│ ├── snapshot:
│ │ spot, dvol, mark_combo, delta_short, return_4h
│ ├── exit_decision.evaluate
│ ├── action == HOLD:
│ │ └── log EXIT_EVALUATED("hold")
│ ├── action == CLOSE_*:
│ │ ├── reporting.exit_proposal(position, decision)
│ │ ├── telegram.request_confirmation(timeout=30min)
│ │ │ ├── confermato:
│ │ │ │ ├── memory.push_user_instruction (close)
│ │ │ │ ├── state.update(status="closing")
│ │ │ │ └── log EXIT_CONFIRMED
│ │ │ ├── rifiutato:
│ │ │ │ └── log EXIT_DEFERRED (resterà in HOLD fino a prossimo ciclo)
│ │ │ └── timeout:
│ │ │ └── escalation: per CLOSE_STOP/CLOSE_VOL/CLOSE_DELTA
│ │ │ → invia comunque l'istruzione (urgenza prevale)
│ │ │ per altri motivi → log EXIT_TIMEOUT, attende prossimo ciclo
│ └── continue
└── END
```
### Politica di escalation
I trigger di uscita non sono uguali. Alcuni hanno **urgenza alta** che
giustifica l'esecuzione anche senza conferma utente entro la finestra:
| Trigger | Urgenza | Comportamento su timeout |
|---|---|---|
| `CLOSE_PROFIT` | Bassa | Attendi prossimo ciclo (può migliorare) |
| `CLOSE_STOP` | **Alta** | Esegui chiusura senza conferma esplicita |
| `CLOSE_VOL` | **Alta** | Esegui chiusura senza conferma |
| `CLOSE_DELTA` | **Alta** | Esegui chiusura senza conferma |
| `CLOSE_TIME` | Media | Attendi 1 ciclo extra; al secondo timeout esegui |
| `CLOSE_AVERSE` | Media | Attendi prossimo ciclo |
Su escalation hard, Telegram riceve un messaggio **post-fact**: "ho
chiuso la posizione X per stop loss perché non ho ricevuto risposta in
30 min e l'urgenza non consentiva attesa".
## Flusso 4 — Mensile (Kelly recalibration)
Trigger: cron `0 12 1 * *` (primo di ogni mese alle 12:00 UTC).
```
1. Carica trades chiusi negli ultimi 365 giorni
2. kelly_recalibration.recalibrate
3. Genera report mensile:
- performance vs simulazione MC
- win rate, avg win/loss, edge
- kelly suggerito vs corrente
- violazioni regole (deve essere 0)
4. telegram.send (informativo, no conferma)
5. brain_bridge.write_note(
path="strategies/cerberus-bite/monthly-report-YYYY-MM.md",
content=report
)
6. log KELLY_RECALIBRATED
```
L'aggiornamento di `strategy.yaml` resta **manuale**: Adriano legge il
report, discute con Milito, e committa il nuovo file di config (con
giustificazione nel commit message). Nessun auto-update.
## Flusso 5 — Health check periodico
Trigger: ogni 5 minuti.
```
1. ping ogni MCP usato → ms latency
2. SQLite read-write probe (transazione fittizia)
3. lock file ancora valido
4. ultima fill di Cerbero core ricevuta entro tempo atteso
5. state.system_state.kill_switch == 0
Se qualsiasi passo fallisce:
- log HEALTH_DEGRADED
- se 3 fallimenti consecutivi → kill_switch + alert
- se 5 fallimenti consecutivi → restart automatico (1 sola volta al giorno)
```
## Flusso 6 — Recovery dopo crash
Quando l'engine riparte:
1. Riapre SQLite + log della giornata.
2. Per ogni posizione `awaiting_fill`:
- Chiede a `cerbero-memory.get_status(instruction_id)`
- Se filled → aggiorna a `open`
- Se cancelled → aggiorna a `cancelled`
- Se ancora pending → mantiene stato e riprende il monitoraggio
3. Per ogni posizione `closing`:
- Stessa logica
4. Per ogni posizione `open`:
- Verifica che esista realmente sul broker (Deribit `get_positions`)
- Se non esiste → flag `state_inconsistent` + alert + kill switch
5. Riconciliazione completata → ENGINE_START.
Il sistema **mai** prende decisioni di trading durante recovery: solo
allinea lo stato. Il primo decision loop avviene al prossimo trigger
naturale.
## Diagramma di stato di una posizione
```
┌─ rejected_by_user ──→ cancelled
(entry) ├─ timeout ──────────→ cancelled
proposed ─────────────► │
├─ confirmed
awaiting_fill
├─ no_fill ──────────→ cancelled
├─ filled ───────────► open
│ │
│ (exit_decision != HOLD)
│ │
│ ▼
│ closing
│ │
│ ├─ no_fill (escalated) → manual_intervention
│ └─ filled ──────────→ closed
```
## Cron summary
| Cron | Trigger | Frequenza |
|---|---|---|
| `0 14 * * MON` | Entry evaluation | Settimanale |
| `0 2,14 * * *` | Position monitoring | 2× giorno |
| `0 12 1 * *` | Kelly recalibration | Mensile |
| `*/5 * * * *` | Health check | 5 min |
| `0 0 * * *` | Backup SQLite + rotation log | Giornaliero |
| `0 8 * * *` | Daily digest Telegram | Giornaliero |
Tutti gli orari in UTC.
+193
View File
@@ -0,0 +1,193 @@
# 07 — Risk Controls
Controlli di sicurezza trasversali che non sono parte della strategia
ma proteggono il sistema da bug, dati corrotti, fallimenti
infrastrutturali o decisioni umane fuori posto.
## Filosofia
- **Default deny**: in caso di dubbio, il sistema non fa nulla.
- **Disarm manuale**: ogni kill switch viene disarmato esplicitamente
da Adriano via CLI o comando Telegram, mai automaticamente.
- **Visibilità**: ogni evento di sicurezza viene loggato e notificato
immediatamente.
- **No silent close**: una posizione viene chiusa solo a seguito di
decisione esplicita dei trigger di strategia, mai per "evento
sospetto".
## Kill switch
### Stato
`system_state.kill_switch ∈ {0, 1}`. Quando = 1, l'engine:
- continua i flussi di **sola lettura** (health check, monitoring di
stato, log)
- **non** invia istruzioni di apertura
- **non** invia istruzioni di chiusura **automatiche** (richiede
intervento manuale via CLI con flag `--force-close-position`)
- continua a notificare i trigger di uscita ad Adriano via Telegram
con escalation manuale
### Trigger automatici
| Causa | Auto-arm | Note |
|---|---|---|
| MCP `cerbero-deribit` versione mismatch | Sì | Non procede senza schema atteso |
| MCP `cerbero-macro` non risponde per >30 min | Sì | No entry possibile |
| MCP `cerbero-memory` non risponde per >5 min | Sì | No esecuzione possibile |
| Stato SQLite incoerente con broker | Sì | Diff tra DB e Deribit positions |
| 3 health check consecutivi falliti | Sì | Engine instabile |
| Perdita giornaliera > 3% equity | Sì | Hard prohibition Cerbero v4 |
| Numero posizioni concorrenti > cap | Sì | Bug del sizing engine |
| Push instruction respinta da Cerbero core | Sì | Dopo 2 retry |
| Schema config invalido al reload | Sì | Mai partire con config rotta |
| Comando Adriano `/kill` su Telegram | Sì | Trigger volontario |
### Disarm
```bash
cerbero-bite kill-switch --disarm --reason "<motivo>"
```
oppure via Telegram:
```
/disarm <motivo>
```
In entrambi i casi il sistema chiede una conferma esplicita ("yes I am
sure").
## Cap di rischio (oltre alle regole di strategia)
Questi cap sono ridondanti rispetto a quelli del §5 della strategia,
ma sono applicati in modo difensivo come **ultima linea**:
| Misura | Limite | Comportamento se superato |
|---|---|---|
| Notional combo singolo | 200 EUR | Sizing engine reject |
| Engagement totale aperto | 1.000 EUR | Sizing engine reject |
| Posizioni concorrenti CB | 4 (default 1 per strategia) | Reject pre-push |
| Trade aperti per giorno | 6 (intera Cerbero suite) | Lookup `cerbero-memory.daily_trade_count` |
| Perdita giornaliera | 3% equity | Kill switch |
| Distance short strike | < 15% spot OTM | Combo builder reject |
| Credit / width | < 0.30 | Combo builder reject |
Doppia verifica: il sizing_engine applica il cap, **e** il watchdog
runtime ricontrolla il payload prima di chiamare `push_user_instruction`.
Se le due misure divergono → kill switch + alert.
## Dead-man switch
Se l'engine non scrive un evento `HEALTH_OK` per **15 minuti** consecutivi:
1. Un processo separato `data/dead-man.sh` (cron in user crontab,
indipendente dall'engine) rileva il silenzio.
2. Invia alert su canale Telegram di backup.
3. Marca SQLite con `system_state.kill_switch=1`.
4. Adriano interviene manualmente.
Il dead-man è scritto in shell minimale (no dipendenze Python) per
sopravvivere a corruzioni dell'env Python.
## Audit log immutabile
Oltre al log JSONL standard, ogni decisione di trading produce una
linea append-only in `data/audit.log` con il **digest SHA-256** della
linea precedente (chain di hash, stile blockchain semplificato).
Esempio:
```
2026-04-27T14:00:01Z|ENTRY_PROPOSED|{...full payload...}|prev_hash=abc123...|hash=def456...
```
Verificabile retroattivamente con `cerbero-bite audit verify`. Una
discrepanza dell'hash chain è trattata come tampering e arma il kill
switch.
## Dry-run mode
Tutti i flussi possono essere eseguiti in modalità `--dry-run`:
- Tutti i tool MCP **read-only** vengono chiamati normalmente.
- Tool MCP **write** (`push_user_instruction`, `kb_write`, `send`) vengono
loggati ma **non** chiamati.
- `state.create_position` viene scritto in tabella `dry_positions`
(schema identico a `positions` ma separata).
Usato per:
- Testing in produzione prima di andare live.
- Replay di giornate storiche per validazione.
- Test di nuove versioni di config o algoritmi.
## Versionamento config
Ogni `strategy.yaml` ha:
```yaml
config_version: "1.0.0"
config_hash: "<sha256 del file senza questa riga>"
last_review: "2026-04-26"
last_reviewer: "Adriano"
```
All'avvio l'engine verifica che `config_hash` corrisponda al contenuto.
Mismatch → kill switch (qualcuno ha modificato la config senza
aggiornare l'hash, possibile tampering o errore umano).
## Escalation tree
```
Evento anomalo
├── Severity LOW (es. 1 health check fallito)
│ └── Log WARNING, continua
├── Severity MEDIUM (es. MCP timeout occasionale)
│ ├── Log WARNING + Telegram digest giornaliero
│ └── Continua, retry next cycle
├── Severity HIGH (es. 3 health check consecutivi falliti)
│ ├── Kill switch ARM
│ ├── Telegram alert immediato
│ └── Adriano interviene
└── Severity CRITICAL (es. stato incoerente, hash chain rotto)
├── Kill switch ARM
├── Telegram + canale backup BotPapà
├── Engine si mette in idle (no decisioni, solo monitoring)
└── Richiede intervento umano per disarmo
```
## Test di resilienza obbligatori
Prima del go-live e ad ogni release minor:
1. **Chaos test MCP**: simula timeout/errori su ogni MCP, verifica
che il comportamento documentato in `04-mcp-integration.md` sia
rispettato.
2. **State corruption test**: corrompi una riga `positions` e verifica
che il riconciliatore lo rilevi.
3. **Hash chain test**: modifica una linea audit e verifica che
`audit verify` fallisca.
4. **Replay test**: rigioca una giornata storica in dry-run, confronta
le decisioni con un set golden.
5. **Cap saturation test**: simula 4 posizioni concorrenti, verifica
che il quinto trade venga rifiutato.
I risultati sono documentati in `tests/golden/results-YYYY-MM-DD.md`.
## Cosa NON è un risk control
Per chiarezza, queste cose **non** sono cap né kill switch — sono
parte della strategia, gestite altrove:
- Profit take 50%: regola di strategia.
- Stop loss 1.5×: regola di strategia.
- Vol stop +10 DVOL: regola di strategia.
- Time stop 7 DTE: regola di strategia.
I risk control proteggono il **sistema**. La strategia protegge il
**capitale**. Sono livelli diversi.
+268
View File
@@ -0,0 +1,268 @@
# 08 — Testing & Validation
Approccio TDD imposto dalla skill `superpowers:test-driven-development`
e coerente con `mcp_cerbero_brain`. Niente codice senza test che fallisce
prima e passa dopo.
## Piramide dei test
```
┌────────────────┐
│ golden / e2e │ ~10 scenari, lenti, eseguiti pre-release
└────────────────┘
┌──────────────────────┐
│ integration tests │ ~50, MCP fake, eseguiti su PR
└──────────────────────┘
┌──────────────────────────┐
│ unit tests │ ~300, < 5 sec totali, ad ogni save
└──────────────────────────┘
┌────────────────────┐
│ property tests │ hypothesis su algoritmi puri
└────────────────────┘
```
## Unit tests (`tests/unit/`)
Coprono ogni funzione in `core/`. Sono **veloci** (< 5 sec totali),
**deterministici** (no rete, no time, no random).
Convenzioni:
- Un file di test per modulo: `test_sizing_engine.py`, `test_exit_decision.py`, ecc.
- Naming: `test_<funzione>_<scenario>_<aspettativa>`. Es:
- `test_compute_contracts_capital_720_dvol_40_returns_one`
- `test_evaluate_mark_at_50pct_returns_close_profit`
- Fixture: dataclasses pre-costruite in `tests/fixtures/scenarios.py`.
### Coverage minima richiesta
| Modulo | Coverage |
|---|---|
| `core/*` | 100% statement + 100% branch |
| `safety/*` | 100% statement |
| `state/*` | ≥ 90% |
| `clients/*` | ≥ 80% |
| `runtime/*` | ≥ 80% |
Coverage misurata con `coverage.py`, soglia bloccante in CI.
### Esempi di test obbligatori
**`test_entry_validator.py`**:
```python
def test_validate_entry_capital_below_minimum_returns_fail(default_cfg):
ctx = EntryContext(capital_usd=Decimal("700"), dvol_now=Decimal("40"), ...)
result = validate_entry(ctx, default_cfg)
assert result.accepted is False
assert "capital_below_720" in result.reasons
def test_validate_entry_dvol_too_high_returns_fail(default_cfg):
ctx = EntryContext(capital_usd=Decimal("1500"), dvol_now=Decimal("95"), ...)
result = validate_entry(ctx, default_cfg)
assert "dvol_above_90" in result.reasons
def test_validate_entry_macro_event_inside_dte_returns_fail(default_cfg):
ctx = EntryContext(..., next_macro_event_in_days=5)
result = validate_entry(ctx, default_cfg)
assert "macro_event_within_dte" in result.reasons
def test_validate_entry_all_conditions_met_returns_accepted(default_cfg):
ctx = EntryContext(...)
result = validate_entry(ctx, default_cfg)
assert result.accepted is True
assert result.reasons == []
```
**`test_sizing_engine.py`**:
```python
@pytest.mark.parametrize("capital,dvol,expected_n", [
(720, 40, 1),
(1500, 40, 2),
(1500, 50, 1), # adj 0.85, 195*0.85/93 ≈ 1.78 → 1
(5000, 40, 2), # cap 200 EUR ≈ 215 USD; 215/93 ≈ 2
(100000, 40, 2), # cap saturo
(500, 40, 0), # undersize
])
def test_compute_contracts(capital, dvol, expected_n, default_cfg):
ctx = SizingContext(
capital_usd=Decimal(capital),
max_loss_per_contract_usd=Decimal("93"),
dvol_now=Decimal(dvol),
eur_to_usd=Decimal("1.075"),
open_engagement_usd=Decimal(0),
other_open_positions=0,
)
assert sizing_engine.compute_contracts(ctx, default_cfg).n_contracts == expected_n
```
**`test_exit_decision.py`**: ogni branch dell'ordine di valutazione
deve avere almeno un test.
## Property tests (`tests/unit/test_*_properties.py`)
Usiamo `hypothesis` per le invarianti:
```python
@given(
capital=decimals(min_value=720, max_value=200_000),
dvol=decimals(min_value=20, max_value=89),
max_loss=decimals(min_value=50, max_value=300),
)
def test_sizing_never_exceeds_cap_eur(capital, dvol, max_loss, default_cfg):
"""Invariante: il rischio totale non eccede mai il cap EUR."""
ctx = SizingContext(capital_usd=capital, dvol_now=dvol, ...)
result = sizing_engine.compute_contracts(ctx, default_cfg)
cap_usd = Decimal(200) * default_cfg.eur_to_usd
assert result.risk_dollars <= cap_usd
```
Property tests obbligatori:
- Sizing: rischio ≤ cap; n_contracts ≥ 0; mai > 4.
- Exit decision: ordine dei trigger rispettato (CLOSE_PROFIT prima di CLOSE_DELTA, ecc.).
- Combo builder: short_strike < long_strike per bear_call, > per bull_put.
## Integration tests (`tests/integration/`)
Testano l'interazione tra `core/` + `clients/` + `state/` con MCP
**fake** (in-memory).
### Fake MCP
```python
class FakeDeribit(McpClient):
def __init__(self, scenario: dict): ...
async def index_price(self, asset): return Decimal(self._scenario["spot"])
async def dvol(self): return Decimal(self._scenario["dvol"])
# ...
```
I fake sono guidati da uno scenario YAML:
```yaml
# tests/fixtures/scenarios/happy_path.yaml
spot: 2330
dvol: 42
funding_perp: 0.05
funding_cross: 0.04
macro_calendar: []
chain:
- {instrument: ETH-13MAY26-1900-P, strike: 1900, delta: -0.12, mid: 0.0048, ...}
- ...
```
### Cosa coprono
| Test | Scenario |
|---|---|
| `test_weekly_open_happy_path` | Tutto OK → proposta inviata |
| `test_weekly_open_no_strike_available` | Chain vuota nel range delta |
| `test_weekly_open_macro_blocks` | FOMC entro 5 giorni |
| `test_monitor_profit_take` | Mark = 50% credito → close_profit |
| `test_monitor_vol_stop` | DVOL +12 → close_vol |
| `test_recovery_after_crash_open_position` | Crash mid-fill, restart, riconcilia |
| `test_kill_switch_blocks_new_entries` | Kill switch armed → no proposta |
| `test_user_rejection_logs_and_skips` | Adriano dice no → cancelled |
| `test_user_timeout_with_revaluation` | Adriano risponde dopo 30 min, slippage > 8% → abort |
## Golden tests (`tests/golden/`)
Replay di scenari deterministici end-to-end con tutti gli MCP fake.
Output (decisioni, log) confrontato byte-per-byte con un golden file
checked-in.
```
tests/golden/
├── 2026-04-27_weekly_open_bull_put.yaml # input snapshot
├── 2026-04-27_weekly_open_bull_put.golden # output atteso
└── runner.py
```
Modifica intenzionale di un algoritmo richiede aggiornamento del golden,
con commit message che spiega perché.
## Backtest deterministico (replay storico)
Un comando dedicato:
```bash
cerbero-bite replay --from 2024-01-01 --to 2026-04-25 --capital 1500 --dry-run
```
Carica:
- Storia oraria spot ETH (CSV)
- Storia DVOL (CSV)
- Calendar macro storico (CSV)
- Chain opzioni storiche (Deribit API archive, dove possibile)
Itera giorno per giorno applicando esattamente le stesse regole. Output:
- File CSV con tutte le posizioni, P&L, trigger di uscita
- Plot equity curve
- Confronto con simulazione Monte Carlo del documento
Il replay è **non sostituto** del Monte Carlo ma utile per validare
l'engine su dati reali una volta avuti.
## Paper trading (fase di go-live)
Pre-live di **3 mesi minimo**:
- Engine in `--dry-run` ma con Telegram alert reali (Adriano riceve i
segnali ma non li esegue)
- Adriano replica manualmente su Deribit testnet alcuni trade per
toccare con mano
- Confronto giornaliero tra paper P&L e replica reale per misurare
slippage realistico
Soglie di go-live:
- ≥ 30 trade in paper completati con esito coerente con Monte Carlo
- 0 incidenti operativi (timeout, stato incoerente, hash chain rotto)
- Win rate paper ≥ 70%
- Adriano firma esplicitamente l'autorizzazione su Telegram (logga in audit chain)
## Validazione live (fase iniziale)
Primi 30 giorni live:
- Cap dimezzato: 100 EUR per trade, 500 EUR engagement totale
- Monitoraggio quotidiano via daily digest
- Review settimanale con Milito (in chat) per anomalie
- Promozione a cap pieno (200 / 1.000 EUR) solo dopo 10 trade reali
conclusi entro range atteso
## Linting e static analysis
Ogni PR:
- `ruff check` — passing
- `ruff format --check` — passing
- `mypy --strict src/` — passing
- `pytest --cov` — coverage soglie rispettate
- `bandit -r src/` — no security warning
Nessun merge se uno dei check fallisce.
## CI
Anche locale (no GitHub remoto al momento). Pre-commit hook esegue
unit + integration in < 30 sec. Pre-push esegue golden suite (~3 min).
```yaml
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: ruff
- id: ruff-format
- id: mypy
- id: pytest-fast
entry: uv run pytest tests/unit tests/integration -x
stages: [commit]
- id: pytest-golden
entry: uv run pytest tests/golden -x
stages: [push]
```
+269
View File
@@ -0,0 +1,269 @@
# 09 — Development Roadmap
Sviluppo per fasi, ognuna con obiettivo chiaro, criteri di completamento
e dipendenze. La sequenza è stretta: ogni fase aggiunge valore e si
costruisce sulla precedente.
## Fase 0 — Setup (3-5 giorni)
**Obiettivo:** scheletro di progetto eseguibile, niente logica di
business.
Tasks:
1. `git init` + branch `main` + `.gitignore` (esclude `data/`, `.lockfile`,
`*.local.yaml`).
2. `pyproject.toml` con `uv`, deps base: pydantic, sqlalchemy, apscheduler,
mcp, rich (CLI), pytest, mypy, ruff.
3. Layout cartelle come da `02-architecture.md`.
4. Logger strutturato con `structlog`, output JSONL su file.
5. `__main__.py` con CLI Click: comandi `start`, `stop`, `status`,
`dry-run`, `kill-switch`, `replay`.
6. Pre-commit hooks installati.
7. `tests/conftest.py` con fixtures di base.
8. Hello-world test che valida l'avvio dell'engine senza nessun
tool MCP reale (tutti fake).
Definition of Done:
- `uv run cerbero-bite status` stampa "engine idle, kill_switch=0"
- `uv run pytest` passa con almeno 5 test triviali
- `mypy --strict src/` passa
- Repository committato
## Fase 1 — Core algorithms (7-10 giorni)
**Obiettivo:** i 7 algoritmi puri implementati, con test al 100%.
Tasks (ordine TDD):
1. `entry_validator.validate_entry` + `compute_bias`
2. `liquidity_gate.check`
3. `sizing_engine.compute_contracts`
4. `combo_builder.select_strikes` + `build`
5. `greeks_aggregator.aggregate`
6. `exit_decision.evaluate` (questo è il più complesso, due giornate dedicate)
7. `kelly_recalibration.recalibrate`
Per ogni modulo:
- scrivere test che falliscono
- implementare la versione minima che li passa
- aggiungere property test con hypothesis
- code review (skill `superpowers:requesting-code-review`)
Definition of Done:
- 100% statement+branch coverage su `core/`
- Property test per ogni invariante documentata
- 0 errori `mypy --strict`
- Tutti i test obbligatori in `08-testing-validation.md` presenti
## Fase 2 — Persistenza e safety (5-7 giorni)
**Obiettivo:** state machine + risk control implementati.
Tasks:
1. Schema SQLite + migration 0001
2. `state/repository.py` con CRUD typed
3. Audit log con hash chain
4. `safety/kill_switch.py` con tutti i trigger automatici
5. `safety/dead_man.sh` script shell separato
6. Backup utility + retention policy
7. CLI `audit verify`, `kill-switch arm/disarm`, `state inspect`
Definition of Done:
- Coverage `state/` ≥ 90%, `safety/` 100%
- Test di corruzione SQLite e hash chain passano
- Recovery test: kill engine mid-write, restart, stato consistente
## Fase 3 — MCP clients (4-6 giorni)
**Obiettivo:** wrapper tipizzati per tutti i server MCP usati.
Tasks (parallelizzabili tra collaboratori se ci fossero):
1. `clients/_base.py`: McpClient abstract + retry/timeout
2. `clients/deribit.py` con tutti i metodi documentati
3. `clients/hyperliquid.py`
4. `clients/macro.py`
5. `clients/sentiment.py`
6. `clients/portfolio.py`
7. `clients/memory.py`
8. `clients/telegram.py` (anche listener webhook per conferme)
9. `clients/brain_bridge.py`
Per ogni client: fake corrispondente in `tests/fixtures/fakes/`.
Definition of Done:
- Coverage `clients/` ≥ 80%
- Fake test scenari happy/timeout/anomaly per ognuno
- `cerbero-bite ping` lista lo stato di ogni MCP
## Fase 4 — Orchestrator e flussi (5-7 giorni)
**Obiettivo:** i flussi di `06-operational-flow.md` cablati.
Tasks:
1. `runtime/orchestrator.py` — composizione core + clients + state
2. `runtime/scheduler.py` — APScheduler con i cron documentati
3. `runtime/monitoring.py` — loop ogni 12h
4. `runtime/alert_manager.py` — escalation policy
Test integration su scenari completi (vedi `08-testing-validation.md`):
- weekly open happy path
- monitor profit take
- monitor vol stop
- recovery dopo crash
- kill switch blocca entry
Definition of Done:
- Tutti gli integration test passano
- Engine può girare in `--dry-run` per 24h senza errori
- I log sono leggibili e completi
## Fase 4.5 — GUI Streamlit (4 giorni)
**Obiettivo:** dashboard locale per osservazione e azioni manuali. Spec
dettagliata in `11-gui-streamlit.md`.
Tasks:
1. Setup `gui/main.py` + sidebar nav + auto-refresh
2. Pagina Status (engine, capitale, MCP health, kill switch panel)
3. Pagina Equity (curve, drawdown, monthly stats)
4. Pagina Position (legs, payoff plotly, decision history, force-close)
5. Pagina History (filtri, KPI, export CSV)
6. Pagina Audit (log live, verify chain, search)
7. Tabella `manual_actions` + consumer job APScheduler nell'engine
8. Test integration con `streamlit.testing.v1.AppTest`
Definition of Done:
- `cerbero-bite gui` lancia la dashboard su `127.0.0.1:8765`
- Tutte le 5 pagine raggiungibili e popolate
- Disarm da GUI loggato in audit chain ed effettivo entro 30 sec
- Force-close da GUI consumato dall'engine entro 30 sec
- Test integration su ogni pagina passing
## Fase 5 — Reporting e UX (3-5 giorni)
**Obiettivo:** report Telegram puliti, daily digest, replay command.
Tasks:
1. `reporting/pre_trade.py` — testo Markdown con sezioni
2. `reporting/exit_proposal.py`
3. `reporting/daily_digest.py`
4. `reporting/monthly_report.py`
5. CLI `cerbero-bite replay` per backtest
6. CLI `cerbero-bite recalibrate-kelly --dry-run`
Definition of Done:
- Tre messaggi Telegram esempio committati come fixtures
- Replay command funziona su 30 giorni di dati storici simulati
## Fase 6 — Golden tests e backtest (3-4 giorni)
**Obiettivo:** suite di scenari golden + replay storico.
Tasks:
1. Almeno 10 scenari golden coprenti tutti i path principali
2. Storia ETH spot + DVOL caricata da CSV in `tests/data/historical/`
3. Confronto P&L replay vs simulazione Monte Carlo del documento
Definition of Done:
- Replay 2024-01 → 2026-04 senza errori
- Equity curve riprodotta con stesso seed numerico
- Differenza con MC entro intervalli attesi
## Fase 7 — Paper trading (90 giorni)
**Obiettivo:** validazione su segnali reali ma niente capitale.
Setup:
- Engine live in `--dry-run`
- Telegram alert reali
- Adriano replica trade su **testnet Deribit** o paper book per misurare
slippage reale
Metriche da raccogliere:
- Numero proposte settimanali emesse
- Quante passano i filtri
- Win rate, avg P&L paper
- Discrepanze tra mid stimato e fill reale (slippage)
- Errori di sistema, kill switch armati
Definition of Done:
- ≥ 30 trade paper concluso
- 0 errori critici
- Win rate ≥ 70% (paper ETH credit spread baseline atteso)
- Sign-off Adriano via audit chain
## Fase 8 — Soft launch (30 giorni live, cap dimezzato)
**Obiettivo:** primi trade reali con capitale ridotto.
Setup:
- Cap 100 EUR per trade, 500 EUR engagement totale
- Solo Bull Put Spread (no IC) per ridurre superficie di rischio
- Daily digest manuale ogni mattina con Adriano
Metriche di passaggio a full:
- ≥ 10 trade reali conclusi
- P&L coerente con Monte Carlo (entro 1 sigma)
- 0 incidenti operativi
- Spread fill mediano ≤ 5% slippage rispetto al mid stimato
## Fase 9 — Full launch
**Obiettivo:** operatività regime.
- Cap pieno: 200 EUR per trade, 1.000 EUR engagement
- Iron Condor abilitato (regime laterale)
- Mensile review Kelly + report Brain-Bridge
- Ogni 6 mesi review completa con Milito
## Roadmap futura (post-launch, idee)
- **Sottostante BTC**: stessa strategia, parametri ricalibrati. Richiede
fase di paper-trading dedicata.
- **Multi-exchange execution**: alternativa a Deribit (Bybit options,
Binance options) per ridurre counterparty risk.
- **Dashboard locale**: pagina HTML statica generata dai log per
visualizzare equity curve e trade list senza tool esterni.
- **Auto Kelly update**: dopo 100 trade reali, valutare l'auto-update
con sand-box di review Milito.
- **Promozione MCP options-engine**: estrarre `core/` come server MCP
dedicato, riusabile da Cerbero core stesso e da Milito.
## Stima cumulativa
| Fase | Giorni di lavoro | Calendar (con review) |
|---|---:|---:|
| 0 — Setup | 3-5 | 1 settimana |
| 1 — Core | 7-10 | 2 settimane |
| 2 — State & Safety | 5-7 | 1.5 settimane |
| 3 — Clients | 4-6 | 1 settimana |
| 4 — Orchestrator | 5-7 | 1.5 settimane |
| 4.5 — GUI Streamlit | 4 | 1 settimana |
| 5 — Reporting | 3-5 | 1 settimana |
| 6 — Golden + Backtest | 3-4 | 1 settimana |
| **Totale fino a paper-ready** | **34-48** | **~10 settimane** |
| 7 — Paper trading | — | 12-13 settimane |
| 8 — Soft launch | — | 4-5 settimane |
| 9 — Full launch | — | ongoing |
**Tempo a regime: ~6 mesi dal kickoff.**
+309
View File
@@ -0,0 +1,309 @@
# 10 — Config Spec (`strategy.yaml`)
Tutti i parametri della strategia sono esposti in un file YAML
versionato. Modificare il file = decisione esplicita di Adriano.
Modifiche richiedono nuovo `config_version`, ricalcolo `config_hash`
e commit con motivazione.
## Schema completo (golden default)
```yaml
# strategy.yaml — Cerberus Bite golden config v1.0.0
# Riferimento: docs/01-strategy-rules.md
# NB: hash auto-calcolato dal CLI `cerbero-bite config hash`
config_version: "1.0.0"
config_hash: "0000000000000000000000000000000000000000000000000000000000000000" # placeholder
last_review: "2026-04-26"
last_reviewer: "Adriano"
asset:
symbol: "ETH"
exchange: "deribit"
# === ENTRY ===
entry:
# finestra di valutazione settimanale
cron: "0 14 * * MON" # lunedì 14:00 UTC
skip_holidays_country: "IT"
# filtri di accesso (vedi 01-strategy-rules.md §2)
capital_min_usd: 720
dvol_min: 35
dvol_max: 90
funding_perp_abs_max_annualized: 0.80
eth_holdings_pct_max: 0.30
no_position_concurrent: true # 1 sola posizione alla volta
exclude_macro_severity: ["high"]
exclude_macro_countries: ["US", "EU"]
# bias direzionale (§3.1)
trend_window_days: 30
trend_bull_threshold_pct: 0.05
trend_bear_threshold_pct: -0.05
funding_bull_threshold_annualized: 0.20
funding_bear_threshold_annualized: -0.20
iron_condor_dvol_min: 55
iron_condor_adx_max: 20
iron_condor_trend_neutral_band_pct: 0.05
# === STRUCTURE ===
structure:
dte_target: 18
dte_min: 14
dte_max: 21
short_strike:
delta_target: 0.12
delta_min: 0.10
delta_max: 0.15
distance_otm_pct_min: 0.15
distance_otm_pct_max: 0.25
spread_width:
target_pct_of_spot: 0.04
min_pct_of_spot: 0.03
max_pct_of_spot: 0.05
credit_to_width_ratio_min: 0.30
# === LIQUIDITY GATE ===
liquidity:
open_interest_min: 100
volume_24h_min: 20
bid_ask_spread_pct_max: 0.15
book_depth_top3_min: 5
slippage_pct_of_credit_max: 0.08
# === SIZING ===
sizing:
kelly_fraction: 0.13 # Quarter Kelly basato su MC
# cap nominali (Cerbero v4 hard prohibitions)
cap_per_trade_eur: 200
cap_aggregate_open_eur: 1000
max_concurrent_positions: 1 # da 2+ solo via override esplicito
# soft cap difensivi (interni a Cerbero Bite)
max_contracts_per_trade: 4
# aggiustamento volatilità
dvol_adjustment:
- {dvol_under: 45, multiplier: 1.00}
- {dvol_under: 60, multiplier: 0.85}
- {dvol_under: 80, multiplier: 0.65}
# >= 80 → no entry (gestito da entry filters)
# === EXIT ===
exit:
# ordine di valutazione vincolante
profit_take_pct_of_credit: 0.50
stop_loss_mark_x_credit: 2.50 # mark = 250% credito → perdita 1.5×
vol_stop_dvol_increase: 10
time_stop_dte_remaining: 7
time_stop_skip_if_close_to_profit_pct: 0.70 # skip se mark ≤ 70% di profit_take
delta_breach_threshold: 0.30
adverse_move_4h_pct: 0.05
monitor_cron: "0 2,14 * * *" # 02 + 14 UTC
user_confirmation_timeout_min: 30
# escalation per urgenza alta
escalate_on_timeout:
- "CLOSE_STOP"
- "CLOSE_VOL"
- "CLOSE_DELTA"
# === EXECUTION ===
execution:
combo_only: true # mai due ordini separati
initial_limit: "mid"
reprice_step_ticks: 1
reprice_max_steps: 3
reprice_max_steps_urgent: 5 # per chiusure urgenti
order_tif: "GTC"
order_expiry_min: 30
ack_timeout_s: 300 # ack Cerbero core
# === MONITORING ===
monitoring:
health_check_interval_s: 300
health_failures_before_kill: 3
health_failures_before_restart: 5
daily_digest_cron: "0 8 * * *"
monthly_report_cron: "0 12 1 * *"
# === STORAGE ===
storage:
sqlite_path: "data/state.sqlite"
log_path: "data/log/"
log_retention_days: 365
backup_path: "data/backups/"
backup_retention_days: 30
# === MCP ===
mcp:
config_file: "~/.config/cerbero-suite/mcp.json"
call_timeout_s: 8
retry_max: 3
retry_base_delay_s: 1
required_versions:
cerbero-deribit: "^2.0.0"
cerbero-hyperliquid: "^1.5.0"
cerbero-memory: "^4.0.0"
cerbero-portfolio: "^1.2.0"
cerbero-macro: "^1.0.0"
cerbero-sentiment: "^1.0.0"
cerbero-telegram: "^1.0.0"
cerbero-brain-bridge: "^1.0.0"
# === TELEGRAM ===
telegram:
parse_mode: "MarkdownV2"
confirmation_timeout_min: 60
exit_confirmation_timeout_min: 30
backup_channel_on_critical: true # canale BotPapà su severity critical
# === KELLY RECALIBRATION ===
kelly_recalibration:
lookback_days: 365
min_sample_low_confidence: 30
min_sample_high_confidence: 100
weight_when_medium_confidence: 0.50
```
## Validazione (`config/schema.py`)
```python
from pydantic import BaseModel, Field, model_validator
from decimal import Decimal
class StrategyConfig(BaseModel):
config_version: str
config_hash: str
last_review: str
last_reviewer: str
asset: AssetConfig
entry: EntryConfig
structure: StructureConfig
liquidity: LiquidityConfig
sizing: SizingConfig
exit: ExitConfig
execution: ExecutionConfig
monitoring: MonitoringConfig
storage: StorageConfig
mcp: McpConfig
telegram: TelegramConfig
kelly_recalibration: KellyConfig
@model_validator(mode="after")
def validate_consistency(self) -> "StrategyConfig":
# short delta range coerente
s = self.structure.short_strike
if not (s.delta_min <= s.delta_target <= s.delta_max):
raise ValueError("delta_target outside [delta_min, delta_max]")
# OTM range coerente
if s.distance_otm_pct_min >= s.distance_otm_pct_max:
raise ValueError("OTM range invalid")
# Kelly range
if not (0 < self.sizing.kelly_fraction < 0.5):
raise ValueError("kelly_fraction out of safe range (0, 0.5)")
# spread width range
sw = self.structure.spread_width
if not (sw.min_pct_of_spot <= sw.target_pct_of_spot <= sw.max_pct_of_spot):
raise ValueError("spread_width target outside min/max")
# exit thresholds sensati
if self.exit.profit_take_pct_of_credit >= 1.0:
raise ValueError("profit_take >= 100% impossible")
if self.exit.stop_loss_mark_x_credit <= 1.0:
raise ValueError("stop_loss multiple <= 1× makes no sense")
return self
```
## Override locale
Se esiste `strategy.local.yaml` (gitignored), i suoi campi sovrascrivono
`strategy.yaml`. Usato per:
- Test in dry-run con cap dimezzati
- Personalizzazioni dello sviluppatore senza inquinare il main
- Override emergenza ("max_concurrent_positions: 0" per congelare entry
senza disabilitare l'engine)
L'override viene loggato all'avvio:
```
2026-04-27T10:00:00Z | OVERRIDE_APPLIED | sizing.cap_per_trade_eur: 200 → 100
```
## Hash integrity
```bash
# Calcolo manuale
cerbero-bite config hash
# Aggiornamento automatico (richiede review prompt)
cerbero-bite config update --reason "Reduce kelly to 0.10 after Apr drawdown"
```
Il comando:
1. Apre il file in editor.
2. All'esci, ricalcola SHA-256 escludendo la riga `config_hash`.
3. Aggiorna `config_hash`, `last_review`, `last_reviewer`.
4. Mostra diff ad Adriano.
5. Firma in audit chain.
Mismatch hash a runtime = kill switch immediato (vedi `07-risk-controls.md`).
## Esempi di modifiche valide
### Riduzione difensiva post-drawdown
```diff
sizing:
- kelly_fraction: 0.13
+ kelly_fraction: 0.10
```
Commit: `chore(config): reduce kelly to 0.10 after April -20% DD`.
### Aggiunta nuovo evento macro escluso
```diff
entry:
exclude_macro_severity: ["high"]
+ exclude_macro_keywords: ["Powell", "Lagarde", "ETF approval"]
```
Commit: `chore(config): exclude ETF-related events from entry window`.
### Disabilitazione temporanea iron condor
```diff
entry:
- iron_condor_dvol_min: 55
+ iron_condor_dvol_min: 999 # de facto disabled
```
Commit: `chore(config): disable IC pending review of vol regime`.
## Cosa NON è in config
Non è permesso parametrizzare:
- L'**ordine** dei trigger di uscita (è una scelta strategica codificata).
- Le **strutture** ammesse (vietato vendita nuda, ecc., scolpito nel codice).
- Le **hard prohibitions** Cerbero v4 (cap 200/1000 EUR sono limiti
superiori, non ulteriormente liberalizzabili).
- Lo **scheduler** per intervalli più stretti (un'ottimizzazione che
non si fa via config).
+276
View File
@@ -0,0 +1,276 @@
# 11 — GUI Streamlit
GUI desktop locale per osservazione e azioni manuali. Implementazione
con **Streamlit**: un singolo processo Python, niente HTML/JS custom,
niente bundler, niente porte esposte all'esterno.
## Filosofia
- **Read-mostly**: la dashboard è soprattutto per *guardare* cosa fa il
sistema. Le azioni interattive sono poche e ben delimitate (arm/disarm,
force-close di emergenza, conferma manuale di una proposta in coda).
- **Stesso state, due frontend**: Streamlit non duplica logica. Legge
da `state/repository.py` e dai log, esattamente come fa il decision
loop. Non parla mai direttamente al broker.
- **Localhost only**: `streamlit run --server.address 127.0.0.1`.
Mai bind su `0.0.0.0`. Niente autenticazione, niente HTTPS: la
protezione è "macchina di Adriano, browser locale".
- **Single-user, single-tab**: Streamlit non è progettato per concorrenza.
Una sola sessione attiva alla volta.
## Stack tecnico
| Componente | Scelta |
|---|---|
| Framework | `streamlit` ≥ 1.40 |
| Charts | `streamlit` builtin (`st.line_chart`, `st.bar_chart`) + `plotly` per payoff diagram |
| Tabelle | `streamlit.dataframe` con filtri |
| Auto-refresh | `st_autorefresh` (component) ogni 5-10 sec sulle pagine live |
| Config interna | letta da `strategy.yaml` via `config.loader` (sola lettura) |
| Sicurezza | localhost-only, lock file condiviso con engine |
| Process model | Processo separato da quello dell'engine, comunicazione **solo via SQLite e log** |
## Avvio
```bash
cerbero-bite gui # alias per `streamlit run src/cerbero_bite/gui/main.py`
```
oppure manualmente:
```bash
uv run streamlit run src/cerbero_bite/gui/main.py \
--server.address 127.0.0.1 \
--server.port 8765 \
--server.headless true \
--browser.gatherUsageStats false
```
## Layout cartelle
```
src/cerbero_bite/gui/
├── __init__.py
├── main.py # entry point streamlit, sidebar nav
├── pages/
│ ├── 1_📊_status.py
│ ├── 2_📈_equity.py
│ ├── 3_💼_position.py
│ ├── 4_📜_history.py
│ └── 5_🔍_audit.py
├── components/
│ ├── kill_switch_panel.py
│ ├── mcp_health_grid.py
│ ├── pending_proposal_card.py
│ ├── payoff_chart.py
│ └── greeks_panel.py
└── data_layer.py # wrapper read-only verso state.repository
```
## Pagine
### 1. 📊 Status (home)
Vista a colpo d'occhio dello stato corrente.
Sezioni:
- **Engine status**: badge verde/giallo/rosso (running/degraded/killed),
uptime, ultimo health check, kill_switch state, kill_reason se armato.
- **Capitale**: equity corrente da `cerbero-portfolio` (cache ultimo
valore noto + timestamp), variazione % vs giorno prima, vs settimana,
vs mese.
- **Posizione attiva**: card con riepilogo (proposal_id, expiry, credit,
P&L unrealized stimato, days_to_expiry) o "nessuna posizione aperta".
- **MCP health grid**: 8 box, uno per server, con latenza ms e semaforo.
- **Pending action**: se l'engine ha una proposta in attesa di conferma
e il timeout Telegram è scaduto, qui appare una card con `Approve`/`Reject`.
Effetto: la decisione viene scritta in coda e il decision orchestrator
la legge al prossimo health-check.
- **Big buttons**: `🟢 Disarm` / `🔴 Arm Kill Switch` (con conferma
typed `"yes I am sure"`).
Auto-refresh: 5 secondi.
### 2. 📈 Equity
Grafico storia capitale e analitica.
Sezioni:
- **Equity curve** (line chart): capitale nel tempo dall'inizio del
tracking. Risoluzione giornaliera. Sovrapposizione opzionale:
- banda Monte Carlo P5/P50/P95 (statica, dal documento)
- DVOL nel tempo (asse Y secondario)
- eventi macro (vertical lines sui giorni FOMC/CPI)
- **Drawdown rolling** (sotto curve): area chart del DD% corrente.
- **P&L distribution** (histogram): trade chiusi raggruppati per outcome
(profit_take, stop_loss, vol_stop, time_stop, ecc.).
- **Tabella mensile**: per ogni mese — n trade, win rate, P&L, max DD.
Filtri: range temporale, asset (solo ETH per ora).
Auto-refresh: 30 secondi (cambia raramente).
### 3. 💼 Position
Drill-down sulla posizione attualmente aperta (se esiste).
Sezioni:
- **Header**: proposal_id, opened_at, expiry, days_left, status.
- **Legs table**: instrument, side, size, mid corrente, delta,
theta, vega — refresh periodico via `clients.deribit`.
- **Greche aggregate**: delta/theta/vega netti.
- **Payoff diagram** (plotly): P&L vs spot ETH a scadenza, con
breakeven, max profit, max loss, spot corrente come marker.
- **Decision history**: tabella con tutte le `decisions` di tipo
`exit_check` per questa posizione, in ordine cronologico, con
outcome HOLD / CLOSE_*.
- **Distance metrics**: short strike a `X% OTM`, delta corrente,
distanza in sigma.
- **Force close** (collapsibile): typed confirmation + reason field.
Su submit: scrive in coda azione `manual_close`, l'engine la consuma
al prossimo monitor cycle.
Auto-refresh: 10 secondi.
### 4. 📜 History
Storico trade chiusi.
Sezioni:
- **Filtri**: range temporale, outcome (multiselect), P&L > 0 / < 0 / tutti.
- **Tabella trade chiusi** (`st.dataframe` sortable): proposal_id,
opened_at, closed_at, expiry, n_contracts, credit_usd, debit_paid_usd,
pnl_usd, outcome, days_held.
- **KPI strip**: n trade, win rate, avg win, avg loss, edge per trade,
edge cumulato.
- **Confronto Monte Carlo**: side-by-side delle metriche reali vs
attese da simulazione, con delta in %.
- **Export CSV**: bottone download per uso fiscale.
Auto-refresh: manuale (button).
### 5. 🔍 Audit
Log e audit chain.
Sezioni:
- **Live log stream**: ultimi 100 eventi, filtro per `level` e `event`.
Auto-refresh 5 sec.
- **Audit chain status**: bottone `Verify`. Mostra "✅ chain integra
fino a 14.382 eventi" o "❌ tampering rilevato a evento N".
- **Search**: ricerca testuale negli ultimi 30 giorni di log.
- **Stats engine**: numero kill switch armati nell'ultimo mese, MCP
failure count per server, average decision loop latency.
- **Export log**: download `.jsonl.gz` per analisi forensica.
Auto-refresh: manuale.
## Comunicazione GUI ↔ Engine
La GUI **non importa** moduli `runtime/` né chiama direttamente i client
MCP. Tutto passa via:
| Azione GUI | Effetto |
|---|---|
| Visualizzazione stato | Read da `state/repository.py` (SQLite) |
| Equity / storico | Read da SQLite + `data/log/*.jsonl` |
| MCP health | Read da `state.system_state.last_health_check` (l'engine fa il check) |
| **Disarm kill switch** | Write su `system_state` con `kill_switch=0`; l'engine al prossimo health check rileva e log `KILL_SWITCH_DISARMED` |
| **Arm kill switch** | Write su `system_state` con `kill_switch=1, kill_reason="manual via GUI"` |
| **Force close** | Insert riga in tabella `manual_actions` (nuova) con `kind="force_close", proposal_id=...`; l'engine al prossimo monitor cycle la consuma |
| **Approve pending proposal** | Insert riga in `manual_actions` con `kind="approve_proposal", proposal_id=...` |
**Nuova tabella SQLite** (`05-data-model.md` da estendere):
```sql
CREATE TABLE manual_actions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL, -- approve_proposal, reject_proposal, force_close, etc.
proposal_id TEXT,
payload_json TEXT,
created_at TEXT NOT NULL,
consumed_at TEXT, -- NULL = ancora da processare
consumed_by TEXT,
result TEXT
);
CREATE INDEX idx_manual_actions_unconsumed ON manual_actions(consumed_at);
```
L'engine include un nuovo job APScheduler `every 30s`:
```python
async def consume_manual_actions():
actions = state.fetch_unconsumed_manual_actions()
for a in actions:
if a.kind == "force_close":
await orchestrator.handle_force_close(a.proposal_id, a.payload)
elif a.kind == "approve_proposal":
await orchestrator.handle_proposal_approved(a.proposal_id)
# etc.
state.mark_action_consumed(a.id, result="ok")
```
Le azioni write **non bypassano** i risk control: una `force_close` deve
comunque passare dal `safety.system_healthy()` e da una conferma typed
nella GUI prima di essere scritta in coda.
## Lock e concorrenza
- L'engine tiene `data/.lockfile` esclusivo.
- La GUI tiene `data/.gui-lockfile` esclusivo (impedisce due tab/Streamlit aperti).
- Entrambi possono leggere SQLite (modalità WAL).
- Le `manual_actions` sono il **canale di scrittura** condiviso, con
primary key auto-increment e flag `consumed_at` per consumo idempotente.
## Sicurezza
- Bind solo `127.0.0.1`. Mai `0.0.0.0`.
- Streamlit avviato con `--server.headless true` per evitare apertura
automatica del browser via tunnel.
- Nessuna autenticazione HTTP: la barriera è il fatto che la macchina
è personale di Adriano. Se il sistema fosse mai esposto, va aggiunto
reverse proxy con basic auth — ma non è il caso.
- Le azioni write richiedono **typed confirmation** (`"yes I am sure"`,
identico al flusso CLI). Mai `st.button` senza challenge.
- CSRF: non rilevante in Streamlit (no form HTML, tutto via session state).
## Cosa la GUI **non** fa
Per chiarezza:
- Non interroga direttamente Deribit o altri exchange.
- Non chiama mai `cerbero-memory.push_user_instruction`.
- Non muta `strategy.yaml` (modifiche restano da CLI con audit chain).
- Non riavvia l'engine (start/stop sono CLI).
- Non sostituisce Telegram come canale di conferma di apertura.
Telegram resta il canale primario; la GUI è canale di **fallback**
per quando Adriano è davanti al laptop e non al telefono.
## Stima di sforzo
Inserita come **Fase 4.5** nella roadmap, tra Orchestrator e Reporting:
| Task | Giorni |
|---|---|
| Setup `gui/main.py` + sidebar nav + autorefresh | 0.5 |
| Pagina Status + MCP health grid + kill_switch panel | 0.5 |
| Pagina Equity + drawdown + plot mensili | 0.5 |
| Pagina Position + payoff plotly + decision history | 1.0 |
| Pagina History + filtri + export CSV | 0.5 |
| Pagina Audit + search log + verify chain | 0.5 |
| `manual_actions` table + consumer job APScheduler | 0.5 |
| Test integration (Streamlit AppTest framework) | 0.5 |
| **Totale** | **~4 giorni** |
Definition of Done:
- `cerbero-bite gui` lancia la dashboard
- Tutte le 5 pagine raggiungibili e popolate (anche con dati fake)
- Disarm da GUI loggato in audit chain ed effettivo entro 30 sec
- Force-close da GUI consumato dall'engine entro 30 sec
- Test integration con `streamlit.testing.v1.AppTest` per ogni pagina
+139
View File
@@ -0,0 +1,139 @@
[project]
name = "cerbero-bite"
version = "0.0.1"
description = "Rule-based deterministic engine for Cerberus Bite credit spread strategy on ETH options"
readme = "README.md"
requires-python = ">=3.12"
authors = [{ name = "Adriano Dal Pastro", email = "adrianodalpastro@tielogic.com" }]
license = { text = "Proprietary" }
dependencies = [
"pydantic>=2.9",
"pydantic-settings>=2.5",
"click>=8.1",
"rich>=13.9",
"structlog>=24.4",
"apscheduler>=3.10",
"sqlalchemy>=2.0",
"aiosqlite>=0.20",
"pyyaml>=6.0",
"httpx>=0.27",
"mcp>=1.0",
"tenacity>=9.0",
"python-dateutil>=2.9",
]
[project.optional-dependencies]
gui = [
"streamlit>=1.40",
"plotly>=5.24",
"pandas>=2.2",
"numpy>=2.0",
]
backtest = [
"scipy>=1.14",
"matplotlib>=3.9",
"pandas>=2.2",
"numpy>=2.0",
]
[project.scripts]
cerbero-bite = "cerbero_bite.cli:main"
[dependency-groups]
dev = [
"pytest>=8.3",
"pytest-asyncio>=0.24",
"pytest-cov>=5.0",
"hypothesis>=6.115",
"mypy>=1.13",
"ruff>=0.7",
"pre-commit>=4.0",
"types-PyYAML>=6.0",
"types-python-dateutil>=2.9",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/cerbero_bite"]
[tool.ruff]
line-length = 100
target-version = "py312"
src = ["src", "tests"]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"N", # pep8-naming
"SIM", # flake8-simplify
"RET", # flake8-return
"ARG", # flake8-unused-arguments
"PTH", # flake8-use-pathlib
"ERA", # eradicate
"PL", # pylint
"RUF", # ruff-specific
]
ignore = [
"PLR0913", # too many arguments (we accept config-heavy functions)
"PLR2004", # magic value (we have many domain constants in tests)
]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["PLR2004", "ARG", "S101"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
[tool.mypy]
python_version = "3.12"
strict = true
warn_unreachable = true
warn_redundant_casts = true
warn_unused_ignores = true
disallow_any_generics = true
disallow_untyped_defs = true
no_implicit_reexport = true
files = ["src/cerbero_bite"]
[[tool.mypy.overrides]]
module = ["apscheduler.*", "mcp.*"]
ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
addopts = [
"-ra",
"--strict-markers",
"--strict-config",
]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: integration tests with fake MCP",
"golden: golden scenario tests",
]
[tool.coverage.run]
source = ["src/cerbero_bite"]
branch = true
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"if TYPE_CHECKING:",
"if __name__ == .__main__.:",
]
show_missing = true
skip_covered = false
+3
View File
@@ -0,0 +1,3 @@
"""Cerbero Bite — rule-based deterministic engine for ETH credit spreads."""
__version__ = "0.0.1"
+6
View File
@@ -0,0 +1,6 @@
"""Allow ``python -m cerbero_bite`` to invoke the CLI."""
from cerbero_bite.cli import _entrypoint
if __name__ == "__main__":
_entrypoint()
+152
View File
@@ -0,0 +1,152 @@
"""Command-line interface for Cerbero Bite.
The CLI is the single user-facing entry point. Each subcommand maps to a
specific operational action documented in ``docs/06-operational-flow.md``
and ``docs/07-risk-controls.md``. All commands are placeholders in
Phase 0; subsequent phases will replace the bodies with real logic
without changing the surface.
"""
from __future__ import annotations
import sys
from pathlib import Path
import click
from rich.console import Console
from cerbero_bite import __version__
from cerbero_bite.logging import configure as configure_logging
from cerbero_bite.logging import get_logger
console = Console()
log = get_logger("cli")
def _phase0_notice(action: str) -> None:
console.print(f"[yellow]\\[phase 0 placeholder][/yellow] {action}")
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
@click.version_option(__version__, prog_name="cerbero-bite")
@click.option(
"--log-dir",
type=click.Path(file_okay=False, path_type=Path),
default=Path("data/log"),
show_default=True,
help="Directory for JSONL log files.",
)
@click.option(
"--log-level",
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"], case_sensitive=False),
default="INFO",
show_default=True,
)
@click.pass_context
def main(ctx: click.Context, log_dir: Path, log_level: str) -> None:
"""Cerbero Bite — rule-based ETH credit spread engine."""
configure_logging(log_dir=log_dir, level=log_level.upper())
ctx.ensure_object(dict)
ctx.obj["log_dir"] = log_dir
@main.command()
def status() -> None:
"""Print engine status snapshot."""
console.print(
f"[bold cyan]Cerbero Bite[/bold cyan] v{__version__}\n"
f"engine state: [yellow]idle[/yellow]\n"
f"kill_switch: [green]0 (disarmed)[/green]\n"
f"open positions: 0\n"
f"phase: 0 (skeleton)"
)
@main.command()
def start() -> None:
"""Start the engine main loop (scheduler + monitoring)."""
_phase0_notice("start command not yet implemented; engine remains idle.")
@main.command()
def stop() -> None:
"""Gracefully stop a running engine."""
_phase0_notice("stop command not yet implemented.")
@main.command(name="dry-run")
def dry_run() -> None:
"""Run the decision loop once in dry-run mode (no MCP writes)."""
_phase0_notice("dry-run command not yet implemented.")
@main.group(name="kill-switch")
def kill_switch() -> None:
"""Manage the engine kill switch."""
@kill_switch.command(name="arm")
@click.option("--reason", required=True, help="Why you are arming the kill switch.")
def kill_switch_arm(reason: str) -> None:
"""Arm the kill switch (engine refuses new entries)."""
_phase0_notice(f"kill-switch arm placeholder (reason: {reason!r}).")
@kill_switch.command(name="disarm")
@click.option("--reason", required=True, help="Why you are disarming.")
def kill_switch_disarm(reason: str) -> None:
"""Disarm the kill switch."""
_phase0_notice(f"kill-switch disarm placeholder (reason: {reason!r}).")
@main.command()
def gui() -> None:
"""Launch the Streamlit dashboard."""
_phase0_notice("gui command not yet implemented (will run streamlit on 127.0.0.1:8765).")
@main.command()
@click.option("--from", "date_from", required=True, help="ISO date YYYY-MM-DD.")
@click.option("--to", "date_to", required=True, help="ISO date YYYY-MM-DD.")
@click.option("--capital", type=float, default=1500.0, show_default=True)
@click.option("--dry-run/--no-dry-run", default=True, show_default=True)
def replay(date_from: str, date_to: str, capital: float, dry_run: bool) -> None:
"""Replay historical period through the decision engine."""
_phase0_notice(
f"replay {date_from}{date_to}, capital={capital}, dry_run={dry_run}"
)
@main.group()
def config() -> None:
"""Strategy configuration utilities."""
@config.command(name="hash")
def config_hash() -> None:
"""Compute and print SHA-256 of strategy.yaml."""
_phase0_notice("config hash placeholder; will read strategy.yaml and compute SHA-256.")
@main.group()
def audit() -> None:
"""Audit log utilities."""
@audit.command(name="verify")
def audit_verify() -> None:
"""Verify audit chain integrity."""
_phase0_notice("audit verify placeholder; will walk audit.log hash chain.")
def _entrypoint() -> None:
"""Wrapper used by ``cerbero-bite`` console script."""
try:
main(prog_name="cerbero-bite")
except KeyboardInterrupt:
console.print("[red]interrupted[/red]")
sys.exit(130)
if __name__ == "__main__":
_entrypoint()
View File
View File
View File
+109
View File
@@ -0,0 +1,109 @@
"""Structured logging setup for Cerbero Bite.
Outputs JSONL to a daily file under ``data/log/`` and pretty-prints to
stderr in development mode. Every event line is the canonical record for
audit purposes; the audit chain (see ``safety/audit_log.py``) is built on
top of this stream for trade-relevant events.
"""
from __future__ import annotations
import logging
import sys
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
import structlog
from structlog.stdlib import BoundLogger
from structlog.types import EventDict, Processor
def _utc_timestamp(_: object, __: str, event_dict: EventDict) -> EventDict:
"""Add ISO-8601 UTC timestamp with millisecond precision."""
event_dict["ts"] = datetime.now(UTC).isoformat(timespec="milliseconds")
return event_dict
def _daily_log_path(log_dir: Path) -> Path:
today = datetime.now(UTC).date().isoformat()
return log_dir / f"cerbero-bite-{today}.jsonl"
def configure(
log_dir: Path | str = "data/log",
*,
level: str = "INFO",
pretty_console: bool = True,
) -> None:
"""Configure structlog + stdlib logging.
Args:
log_dir: Directory where the daily JSONL log file is written.
Created if missing.
level: Minimum log level for both handlers.
pretty_console: When True, also write a human-readable stream to
stderr. Disable in production daemon mode if not desired.
"""
log_dir_path = Path(log_dir)
log_dir_path.mkdir(parents=True, exist_ok=True)
log_file = _daily_log_path(log_dir_path)
file_handler = logging.FileHandler(log_file, encoding="utf-8")
file_handler.setLevel(level)
handlers: list[logging.Handler] = [file_handler]
if pretty_console:
console_handler = logging.StreamHandler(sys.stderr)
console_handler.setLevel(level)
handlers.append(console_handler)
logging.basicConfig(
format="%(message)s",
handlers=handlers,
level=level,
force=True,
)
shared_processors: list[Processor] = [
structlog.contextvars.merge_contextvars,
structlog.stdlib.add_log_level,
structlog.stdlib.add_logger_name,
_utc_timestamp,
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
]
structlog.configure(
processors=[
*shared_processors,
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.stdlib.BoundLogger,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
if pretty_console:
# Replace stderr handler's formatter with a pretty renderer
formatter = structlog.stdlib.ProcessorFormatter(
processors=[
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
structlog.dev.ConsoleRenderer(colors=True),
],
foreign_pre_chain=shared_processors,
)
for handler in handlers:
if isinstance(handler, logging.StreamHandler) and not isinstance(
handler, logging.FileHandler
):
handler.setFormatter(formatter)
def get_logger(name: str | None = None, **initial_context: Any) -> BoundLogger:
"""Return a bound structlog logger with optional initial context."""
logger: BoundLogger = structlog.get_logger(name)
if initial_context:
logger = logger.bind(**initial_context)
return logger
View File
View File
View File
+23
View File
@@ -0,0 +1,23 @@
"""Shared pytest fixtures for Cerbero Bite tests."""
from __future__ import annotations
from collections.abc import Iterator
from pathlib import Path
import pytest
@pytest.fixture
def tmp_data_dir(tmp_path: Path) -> Iterator[Path]:
"""Isolated data directory with the standard subfolder layout."""
data = tmp_path / "data"
(data / "log").mkdir(parents=True)
(data / "backups").mkdir()
yield data
@pytest.fixture
def project_root() -> Path:
"""Path to the project root (parent of ``src/``)."""
return Path(__file__).resolve().parent.parent
View File
View File
View File
View File
View File
View File
+55
View File
@@ -0,0 +1,55 @@
"""Smoke tests: package importable, version exposed, CLI invokable."""
from __future__ import annotations
from pathlib import Path
from click.testing import CliRunner
import cerbero_bite
from cerbero_bite import __version__
from cerbero_bite.cli import main as cli_main
def test_package_version_string() -> None:
assert isinstance(__version__, str)
assert __version__.count(".") == 2
def test_package_module_attribute() -> None:
assert cerbero_bite.__version__ == __version__
def test_cli_help_lists_status_command() -> None:
runner = CliRunner()
result = runner.invoke(cli_main, ["--help"])
assert result.exit_code == 0
assert "status" in result.output
def test_cli_status_runs(tmp_data_dir: Path) -> None:
runner = CliRunner()
result = runner.invoke(
cli_main,
["--log-dir", str(tmp_data_dir / "log"), "status"],
)
assert result.exit_code == 0
assert "Cerbero Bite" in result.output
assert "phase: 0" in result.output
def test_cli_kill_switch_arm_placeholder(tmp_data_dir: Path) -> None:
runner = CliRunner()
result = runner.invoke(
cli_main,
["--log-dir", str(tmp_data_dir / "log"), "kill-switch", "arm", "--reason", "test"],
)
assert result.exit_code == 0
assert "phase 0 placeholder" in result.output
def test_cli_version_flag() -> None:
runner = CliRunner()
result = runner.invoke(cli_main, ["--version"])
assert result.exit_code == 0
assert __version__ in result.output
Generated
+2299
View File
File diff suppressed because it is too large Load Diff