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:
@@ -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.
|
||||
Reference in New Issue
Block a user