ce158a92dd
Adegua Cerbero Bite alla nuova versione 2.0.0 del server MCP unificato (testnet/mainnet routing per token, header X-Bot-Tag obbligatorio) e introduce due interruttori operativi indipendenti per separare la raccolta dati dall'esecuzione di strategia. Auth e collegamento MCP - Token bearer letto dalla nuova variabile CERBERO_BITE_MCP_TOKEN; il valore sceglie l'ambiente upstream (testnet vs mainnet) sul server. Rimosso il caricamento da file (`secrets/core.token`, CERBERO_BITE_CORE_TOKEN_FILE, Docker secret /run/secrets/core_token). - Aggiunto header X-Bot-Tag (default `BOT__CERBERO_BITE`, override via CERBERO_BITE_MCP_BOT_TAG) su ogni call MCP, con validazione lato client (non vuoto, ≤ 64 caratteri). - Cartella `secrets/` rimossa, `.gitignore` ripulito, Dockerfile e docker-compose.yml aggiornati con env passthrough e fail-fast quando manca il token. Modalità operativa (RuntimeFlags) - Nuovo modulo `config/runtime_flags.py` con `RuntimeFlags( data_analysis_enabled, strategy_enabled)` e loader che parserizza CERBERO_BITE_ENABLE_DATA_ANALYSIS e CERBERO_BITE_ENABLE_STRATEGY (true/false/yes/no/on/off/enabled/disabled, case-insensitive). - L'orchestratore espone i flag, audita e logga la modalità al boot (`engine started: env=… data_analysis=… strategy=…`), e in `install_scheduler` esclude i job `entry`/`monitor` quando strategy è off e il job `market_snapshot` quando data analysis è off. I job di infrastruttura (health, backup, manual_actions) restano sempre attivi. - Default profile = "solo analisi dati" (data_analysis=true, strategy=false), pensato per la finestra di soak post-deploy. GUI saldi - `gui/live_data.py::_fetch_deribit_currency` riconosce il campo soft `error` nel payload V2 (HTTP 200 con `error` valorizzato dal server quando l'auth Deribit fallisce) e lo propaga come `BalanceRow.error`, evitando di mostrare un fuorviante equity = 0,00. CLI - Sostituita l'opzione `--token-file` con `--token` (stringa) sui comandi start/dry-run/ping; il default proviene dall'env. Le chiamate al builder dell'orchestrator passano anche `bot_tag` e `flags`. Documentazione - `docs/04-mcp-integration.md`: descrizione del nuovo flusso di auth V2 (token = ambiente, X-Bot-Tag nell'audit) e router unificati. - `docs/06-operational-flow.md`: nuova sezione "Modalità operativa" con i tre profili canonici e tabella di gating per ogni job; aggiunto `market_snapshot` al cron summary. - `docs/10-config-spec.md`: nuova sezione "Variabili d'ambiente" tabellare con tutti gli env, comprese le bool dei flag operativi. - `docs/02-architecture.md`: layout del repo aggiornato (`secrets/` rimosso, `runtime_flags.py` aggiunto), descrizione di `config/` estesa. Test - 5 nuovi test su `_fetch_deribit_currency` (soft-error, payload pulito, eccezione, error blank, signature parity). - 7 nuovi test su `load_runtime_flags` (default, override, parsing truthy/falsy, blank fallback, valore invalido). - 4 nuovi test su `HttpToolClient` (X-Bot-Tag default e custom, blank e troppo lungo rifiutati). - 3 nuovi test integration sull'orchestratore (gating dei job in base ai flag). - Test esistenti su token/CLI ping/orchestrator aggiornati al nuovo schema. Suite intera: 404 passed, 1 skipped (sqlite3 CLI assente sull'host di sviluppo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
18 KiB
18 KiB
02 — Architettura
Vista a blocchi
┌─────────────────────────────────┐
│ ADRIANO (utente) │
└──────▲──────────────────────────┘
│ Telegram (notifiche post-fact)
│
┌─────────────────────────────────────────────────────────────────┐
│ CERBERO BITE (rule engine) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ scheduler │ │ state store │ │ audit logger │ │
│ │ (APScheduler)│ │ (SQLite) │ │ (append-only, │ │
│ │ │ │ │ │ hash chain + │ │
│ │ │ │ │ │ anchor SQLite) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ decision orchestrator (core/) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ entry │ │ sizing │ │ exit │ │ greeks │ │ │
│ │ │validator │ │ engine │ │ decision │ │aggregator│ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │liquidity │ │ combo │ │ kelly │ │ │
│ │ │ gate │ │ builder │ │ recalib │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ MCP HTTP client wrappers (clients/) │ │
│ │ Deribit │ Hyperliquid │ Macro │ Sentiment │ Portfolio │ │
│ │ Telegram │ │
│ └─────────────────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────────────┘
│ HTTP (Bearer token, rete Docker
│ `cerbero-suite`)
▼
┌──────────────────────────────────┐
│ MCP servers │
│ (FastAPI, repo Cerbero_mcp) │
└────────────────────┬─────────────┘
│
▼
┌────────────────┐
│ Deribit │
│ (testnet o │
│ mainnet) │
└────────────────┘
Cerbero Bite è completamente autonomo: l'esecuzione degli ordini
combo passa direttamente per mcp-deribit.place_combo_order, senza
intermediazioni. Telegram serve esclusivamente per notificare ad
Adriano gli eventi post-fact (entry placed, exit filled, alert).
Stack tecnologico
| Strato | Scelta | Motivazione |
|---|---|---|
| Linguaggio | Python 3.13 | Allineato a Cerbero_mcp |
| Async runtime | asyncio |
Necessario per i client HTTP MCP e lo scheduler |
| Scheduler | APScheduler AsyncIOScheduler |
Cron-like + interval-based, in-process |
| Persistenza stato | SQLite (file singolo) con WAL + synchronous=NORMAL |
Zero deploy overhead, transazionale, sufficiente per ≤ 4 posizioni |
| Persistenza log | File .jsonl rotativi giornalieri (gzip > 30 g) |
Audit-friendly, append-only, parseable |
| Audit immutabile | data/audit.log con hash chain SHA-256 + anchor in system_state.last_audit_hash |
Anti-tampering + anti-truncation |
| Validazione config | pydantic v2 |
Schema a runtime su strategy.yaml |
| Test | pytest + pytest-asyncio + pytest-httpx + hypothesis |
TDD, integration con MCP fake |
| Type checking | mypy --strict |
Disciplina, niente sorprese a runtime |
| Format/lint | ruff |
Standard del progetto |
| Dependency manager | uv |
Coerente con Cerbero_mcp |
| Client MCP | httpx.AsyncClient long-lived (pooling) + tenacity per retry |
HTTP REST diretto, non SDK mcp |
| Notifiche | Bot API Telegram in-process (notify-only) | Token e chat-id da env, no-op se non configurati |
| GUI | streamlit ≥ 1.40 + plotly (Fase 4.5) |
Dashboard locale, processo separato |
Layout cartelle
Cerbero_Bite/
├── README.md
├── pyproject.toml
├── uv.lock
├── strategy.yaml # config golden + execution.environment
├── strategy.local.yaml.example # override locale (gitignored)
├── Dockerfile # image runtime + HEALTHCHECK
├── docker-compose.yml # rete external cerbero-suite, env passthrough
├── .env.example # template variabili (token MCP, bot tag, modalità)
├── docs/ # questa documentazione
├── src/cerbero_bite/
│ ├── __init__.py
│ ├── __main__.py # entry point CLI
│ ├── cli.py # status, start, dry-run, stop,
│ │ # ping, healthcheck, kill-switch,
│ │ # state, audit, config
│ ├── 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
│ │ └── types.py # OptionQuote, OptionLeg
│ ├── clients/ # wrapper HTTP sui MCP
│ │ ├── _base.py # HttpToolClient + retry/timeout
│ │ ├── _exceptions.py # McpError gerarchia
│ │ ├── deribit.py
│ │ ├── hyperliquid.py
│ │ ├── macro.py
│ │ ├── sentiment.py
│ │ ├── portfolio.py
│ │ └── telegram.py
│ ├── runtime/ # I/O, scheduling, orchestrazione
│ │ ├── orchestrator.py # façade boot/run_*
│ │ ├── dependencies.py # RuntimeContext factory
│ │ ├── scheduler.py # APScheduler builder
│ │ ├── lockfile.py # fcntl.flock single-instance
│ │ ├── alert_manager.py # severity routing
│ │ ├── health_check.py # ping + 3-strikes kill switch
│ │ ├── entry_cycle.py # weekly entry auto-execute
│ │ ├── monitor_cycle.py # 12h exit auto-execute
│ │ └── recovery.py # state reconcile al boot
│ ├── state/ # persistenza
│ │ ├── repository.py
│ │ ├── models.py
│ │ ├── db.py # connect, transaction, run_migrations
│ │ └── migrations/
│ │ ├── 0001_init.sql
│ │ └── 0002_audit_anchor.sql
│ ├── config/ # caricamento e validazione yaml
│ │ ├── schema.py
│ │ ├── loader.py
│ │ ├── mcp_endpoints.py # URL + token + bot tag (da .env)
│ │ └── runtime_flags.py # ENABLE_DATA_ANALYSIS / ENABLE_STRATEGY
│ ├── reporting/ # report umani (Fase 5)
│ ├── gui/ # Streamlit dashboard (Fase 4.5)
│ └── safety/ # kill switch, dead man, audit
│ ├── kill_switch.py
│ ├── audit_log.py # hash chain + on_append callback
│ └── __init__.py
├── tests/
│ ├── unit/ # test puri sui moduli core/
│ ├── integration/ # test con MCP fake (httpx mock)
│ ├── golden/ # scenari deterministici (Fase 6)
│ └── fixtures/
├── scripts/
│ ├── backup.py # VACUUM INTO orario
│ └── dead_man.sh # watchdog shell indipendente
└── data/ # gitignored
├── state.sqlite
├── audit.log
├── log/
└── backups/
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 HTTP sui server MCP. Ogni client espone una API tipizzata che usa internamentehttpx.AsyncClient. Mai logica di business qui.runtime/orchestrazione: composizione diclients/+core/state/+safety/. È l'unico strato che può fare I/O e ha effetti collaterali. EsponeOrchestratorcome façade per il CLI.
state/persistenza. Mai logica di business. Solo CRUD.config/caricamento distrategy.yaml, validazione, esposizione immutabile dei parametri. Risolve gli URL MCP, legge il bearer token + il bot tag al boot ed espone i due interruttori operativiRuntimeFlags(data_analysis_enabled, strategy_enabled).safety/controlli trasversali (vedere07-risk-controls.md).reporting/generazione di stringhe per Telegram. Niente logica di trading, solo formatting.
Decision orchestrator — sequenza tipo per "Lunedì 14:00 UTC"
async def run_entry_cycle(ctx: RuntimeContext, *, eur_to_usd_rate, now):
if ctx.kill_switch.is_armed():
return EntryCycleResult(status="kill_switch_armed", ...)
if ctx.repository.count_concurrent_positions() > 0:
return EntryCycleResult(status="has_open_position", ...)
# 1. Snapshot dati di mercato in parallelo (asyncio.gather)
snap = await _gather_snapshot(
deribit, hyperliquid, sentiment, macro, portfolio, cfg, now
)
# 2. Algoritmi puri
entry_decision = validate_entry(EntryContext(...), cfg)
if not entry_decision.accepted:
return EntryCycleResult(status="no_entry", reason=entry_decision.reasons)
bias = compute_bias(TrendContext(...), cfg)
if bias is None:
return EntryCycleResult(status="no_entry", reason="no_bias")
chain = await deribit.options_chain(currency="ETH", expiry_from=..., expiry_to=...)
quotes = await _build_quotes(deribit, chain)
selection = select_strikes(chain=quotes, bias=bias, spot=spot, now=now, cfg=cfg)
if selection is None:
return EntryCycleResult(status="no_entry", reason="no_strike")
short, long_ = selection
sizing = compute_contracts(SizingContext(...), cfg)
if sizing.n_contracts < 1:
return EntryCycleResult(status="no_entry", reason="undersize")
if not check(short_leg=..., long_leg=..., credit=..., n_contracts=...).accepted:
return EntryCycleResult(status="no_entry", reason="illiquid")
proposal = build(short=short, long_=long_, n_contracts=..., spot=spot, ...)
# 3. Persistenza + auto-execute
repo.create_position(proposal, status="proposed")
order = await deribit.place_combo_order(legs=[short, long_], side="sell", ...)
if order.state in {"filled", "open"}:
repo.update_position_status(proposal_id, status="open" if filled else "awaiting_fill")
repo.create_instruction(InstructionRecord(...))
await telegram.notify_position_opened(...)
audit.append("ENTRY_PLACED", {...})
else:
repo.update_position_status(proposal_id, status="cancelled")
await alert_manager.high(source="entry_cycle", message="broker_reject")
Sequenza tipo per "monitoring 12h"
async def run_monitor_cycle(ctx: RuntimeContext, *, now):
if ctx.kill_switch.is_armed():
return MonitorCycleResult(outcomes=[])
spot = await deribit.index_price_eth()
dvol = await deribit.latest_dvol(currency="ETH", now=now)
return_4h = await _fetch_return_4h(ctx, now=now) # usa dvol_history o
# fallback get_historical
repo.record_dvol_snapshot(DvolSnapshot(timestamp=now, dvol=dvol, eth_spot=spot))
for record in repo.list_positions(status="open"):
snapshot = await _build_position_snapshot(...)
decision = evaluate(snapshot, cfg)
if decision.action == "HOLD":
audit.append("HOLD", {...}); continue
repo.update_position_status(record.proposal_id, status="closing")
order = await deribit.place_combo_order(
legs=[short_buy, long_sell], side="buy", ...
)
if order.state in {"filled", "open"}:
repo.update_position_status(record.proposal_id, status="closed", ...)
audit.append("EXIT_FILLED", {...})
await telegram.notify_position_closed(...)
else:
repo.update_position_status(record.proposal_id, status="open")
await alert_manager.critical(source="monitor_cycle", ...)
Failure modes e retry
| Modalità | Risposta |
|---|---|
| MCP non risponde (timeout) | Retry esponenziale 3 tentativi (1 s, 5 s, 30 s); poi McpTimeoutError propagato e tradotto in alert HIGH dal modulo che ha originato la chiamata |
MCP risponde con dato palesemente rotto (es. mark_iv == 7%) |
Wrapper solleva McpDataAnomalyError; alert HIGH e ciclo saltato |
| Stato SQLite corrotto al boot | Recovery riconcilia con il broker; se non possibile arma kill switch CRITICAL |
Mismatch testnet/mainnet rispetto a strategy.execution.environment |
Kill switch CRITICAL al boot, prima di qualsiasi ciclo trading |
Mismatch hash anchor tra system_state.last_audit_hash e tail del file audit.log |
Kill switch CRITICAL al boot (truncation o tampering del log) |
place_combo_order respinto dal broker |
Posizione marcata cancelled, alert HIGH; in monitor la posizione torna a open per ritentare al ciclo successivo |
| Lock file occupato | Boot fallisce con LockError, exit immediato (un'altra istanza è già attiva) |
Vedi 07-risk-controls.md per il dettaglio completo dei kill switch.
Concorrenza e idempotenza
- Una sola istanza dell'engine alla volta (
fcntl.flockesclusivo sudata/.lockfile). - Lo scheduler è in-process e i job sono coalesce + misfire grace 300 s, quindi un riavvio del container non duplica l'ultimo trigger perso.
- Tutti i ticker dello scheduler sono idempotenti: se il sistema
crasha durante un'apertura, al riavvio il
recover_statericostruisce lo stato confrontando SQLite conderibit.get_positions()e prosegue. - Ogni proposta ha un
proposal_idUUID; illabelinviato a Deribit contiene questo id, così posso correlare gli ordini sul broker con la rigapositionsin caso di indagine forense.
Lifecycle del container
docker compose up cerbero-bite→cerbero-bite startconenforce_hash=Truesustrategy.yaml.Orchestrator.run_foreveracquisiscedata/.lockfile.boot()esegue, in ordine: verifica anchor audit chain → recover state → environment check → primo health probe → logENGINE_START.- Lo scheduler arma i 4 job documentati (entry, monitor, health, backup).
asyncio.Eventblocca il main task fino a SIGTERM/SIGINT.- Alla ricezione del segnale:
scheduler.shutdown(wait=False)→RuntimeContext.aclose()(chiudehttpx.AsyncClientcondiviso) → uscita pulita; il lock file viene rilasciato dal context manager. - Il container Docker, in modalità
restart: unless-stopped, resta down se l'engine ha ricevuto SIGTERM dal composestop; riparte automaticamente solo se il processo è morto per crash.