# 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.