diff --git a/docs/02-architecture.md b/docs/02-architecture.md index 35013e1..4d0faf4 100644 --- a/docs/02-architecture.md +++ b/docs/02-architecture.md @@ -5,15 +5,17 @@ ``` ┌─────────────────────────────────┐ │ ADRIANO (utente) │ - └──────┬──────────────────────────┘ - │ Telegram (report + conferma) - ▼ + └──────▲──────────────────────────┘ + │ Telegram (notifiche post-fact) + │ ┌─────────────────────────────────────────────────────────────────┐ │ CERBERO BITE (rule engine) │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │ │ scheduler │ │ state store │ │ audit logger │ │ -│ │ (APScheduler)│ │ (SQLite) │ │ (append-only) │ │ +│ │ (APScheduler)│ │ (SQLite) │ │ (append-only, │ │ +│ │ │ │ │ │ hash chain + │ │ +│ │ │ │ │ │ anchor SQLite) │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ @@ -21,7 +23,7 @@ │ │ decision orchestrator (core/) │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │ │ entry │ │ sizing │ │ exit │ │ greeks │ │ │ -│ │ │validator │ │ engine │ │ decision │ │ aggregator│ │ │ +│ │ │validator │ │ engine │ │ decision │ │aggregator│ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │ │liquidity │ │ combo │ │ kelly │ │ │ @@ -31,42 +33,50 @@ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────┐ │ -│ │ MCP client wrappers (clients/) │ │ +│ │ MCP HTTP client wrappers (clients/) │ │ │ │ Deribit │ Hyperliquid │ Macro │ Sentiment │ Portfolio │ │ -│ │ Memory │ Telegram │ Brain-Bridge │ │ +│ │ Telegram │ │ │ └─────────────────────────────────────────────────────────┘ │ └────────────────────────┬────────────────────────────────────────┘ - │ MCP JSON-RPC + │ HTTP (Bearer token, rete Docker + │ `cerbero-suite`) ▼ - ┌───────────────────────────────┐ ┌───────────────────┐ - │ MCP servers │ │ Cerbero core │ - │ (esistenti in CerberoSuite) │───▶│ (esecuzione) │ - └───────────────────────────────┘ └───────────────────┘ - │ - ▼ - ┌────────────────┐ - │ Deribit │ - │ API │ - └────────────────┘ + ┌──────────────────────────────────┐ + │ 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.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 | +| 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` + `hypothesis` (property-based) | TDD imposto | +| 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 `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`) | +| Dependency manager | `uv` | Coerente con `Cerbero_mcp` | +| Client MCP | `httpx.AsyncClient` long-lived (pooling) + `tenacity` per retry | HTTP REST diretto, non SDK `mcp` | +| Notifiche | MCP `cerbero-telegram` (notify-only) | Riusa il canale esistente | +| GUI | `streamlit` ≥ 1.40 + `plotly` (Fase 4.5) | Dashboard locale, processo separato | ## Layout cartelle @@ -75,66 +85,76 @@ Cerbero_Bite/ ├── README.md ├── pyproject.toml ├── uv.lock -├── strategy.yaml # config golden -├── strategy.local.yaml.example # override locale (gitignored) -├── docs/ # questa documentazione +├── strategy.yaml # config golden + execution.environment +├── strategy.local.yaml.example # override locale (gitignored) +├── Dockerfile # image runtime + HEALTHCHECK +├── docker-compose.yml # rete external cerbero-suite + secrets +├── docs/ # questa documentazione +├── secrets/ # gitignored (solo .gitkeep + README) ├── src/cerbero_bite/ │ ├── __init__.py -│ ├── __main__.py # entry point CLI -│ ├── core/ # algoritmi puri (no I/O) +│ ├── __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 -│ ├── clients/ # wrapper MCP +│ │ ├── 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 -│ │ ├── memory.py -│ │ ├── telegram.py -│ │ └── brain_bridge.py -│ ├── runtime/ # I/O, scheduling, orchestrazione -│ │ ├── scheduler.py -│ │ ├── orchestrator.py -│ │ ├── monitoring.py -│ │ └── alert_manager.py -│ ├── state/ # persistenza +│ │ └── 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 # SQLAlchemy o dataclasses +│ │ ├── models.py +│ │ ├── db.py # connect, transaction, run_migrations │ │ └── migrations/ -│ ├── config/ # caricamento e validazione yaml +│ │ ├── 0001_init.sql +│ │ └── 0002_audit_anchor.sql +│ ├── 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 +│ │ ├── loader.py +│ │ └── mcp_endpoints.py # URL + token loader +│ ├── reporting/ # report umani (Fase 5) +│ ├── gui/ # Streamlit dashboard (Fase 4.5) +│ └── safety/ # kill switch, dead man, audit │ ├── kill_switch.py -│ ├── dead_man.py -│ └── audit_log.py +│ ├── audit_log.py # hash chain + on_append callback +│ └── __init__.py ├── tests/ -│ ├── unit/ # test puri sui moduli core/ -│ ├── integration/ # test con MCP fake -│ ├── golden/ # scenari deterministici di riferimento +│ ├── unit/ # test puri sui moduli core/ +│ ├── integration/ # test con MCP fake (httpx mock) +│ ├── golden/ # scenari deterministici (Fase 6) │ └── fixtures/ ├── scripts/ -│ ├── reset_state.py -│ ├── replay_day.py # replay forensico di una giornata -│ └── recalibrate_kelly.py -└── data/ - ├── state.sqlite # gitignored - └── log/ # gitignored +│ ├── backup.py # VACUUM INTO orario +│ └── dead_man.sh # watchdog shell indipendente +└── data/ # gitignored + ├── state.sqlite + ├── audit.log + ├── log/ + └── backups/ ``` ## Principio di separazione @@ -142,123 +162,147 @@ Cerbero_Bite/ - **`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. +- **`clients/`** contiene wrapper HTTP sui server MCP. Ogni client + espone una API tipizzata che usa internamente `httpx.AsyncClient`. + Mai logica di business qui. +- **`runtime/`** orchestrazione: composizione di `clients/` + `core/` + + `state/` + `safety/`. È l'unico strato che può fare I/O e ha + effetti collaterali. Espone `Orchestrator` come façade per il CLI. - **`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. +- **`config/`** caricamento di `strategy.yaml`, validazione, + esposizione immutabile dei parametri. Risolve gli URL MCP e legge + il bearer token al boot. - **`safety/`** controlli trasversali (vedere `07-risk-controls.md`). +- **`reporting/`** generazione di stringhe per Telegram. Niente + logica di trading, solo formatting. ## 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 +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", ...) - # 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 + # 1. Snapshot dati di mercato in parallelo (asyncio.gather) + snap = await _gather_snapshot( + deribit, hyperliquid, sentiment, macro, portfolio, cfg, now ) - 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") + # 2. Algoritmi puri + entry_decision = validate_entry(EntryContext(...), cfg) + if not entry_decision.accepted: + return EntryCycleResult(status="no_entry", reason=entry_decision.reasons) - n = sizing_engine.compute_contracts( - capital, max_loss_per_contract, dvol, config - ) - if n < 1: return log_and_skip("undersize") + bias = compute_bias(TrendContext(...), cfg) + if bias is None: + return EntryCycleResult(status="no_entry", reason="no_bias") - proposal = combo_builder.build(short_strike, long_strike, n, mid_price) + 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") - # 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") + short, long_ = selection + sizing = compute_contracts(SizingContext(...), cfg) + if sizing.n_contracts < 1: + return EntryCycleResult(status="no_entry", reason="undersize") - # 5. Esecuzione via Cerbero core - instruction = combo_builder.to_cerbero_instruction(proposal) - await memory.push_user_instruction(instruction, source="cerbero-bite") + if not check(short_leg=..., long_leg=..., credit=..., n_contracts=...).accepted: + return EntryCycleResult(status="no_entry", reason="illiquid") - # 6. Stato - state.create_position(proposal, dvol_at_entry=dvol) - audit_logger.log("ENTRY_PROPOSED", proposal) + 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" ```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 +async def run_monitor_cycle(ctx: RuntimeContext, *, now): + if ctx.kill_switch.is_armed(): + return MonitorCycleResult(outcomes=[]) - 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, - ) + 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_logger.log("HOLD", position.id, decision.reason) - continue + audit.append("HOLD", {...}); 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" + repo.update_position_status(record.proposal_id, status="closing") + order = await deribit.place_combo_order( + legs=[short_buy, long_sell], side="buy", ... ) - state.mark_closing(position.id) + 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 (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 | +| 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 (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. +- Una sola istanza dell'engine alla volta (`fcntl.flock` esclusivo su + `data/.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_state` ricostruisce + lo stato confrontando SQLite con `deribit.get_positions()` e prosegue. +- Ogni proposta ha un `proposal_id` UUID; il `label` inviato a Deribit + contiene questo id, così posso correlare gli ordini sul broker con + la riga `positions` in caso di indagine forense. + +## Lifecycle del container + +1. `docker compose up cerbero-bite` → `cerbero-bite start` con + `enforce_hash=True` su `strategy.yaml`. +2. `Orchestrator.run_forever` acquisisce `data/.lockfile`. +3. `boot()` esegue, in ordine: verifica anchor audit chain → recover + state → environment check → primo health probe → log + `ENGINE_START`. +4. Lo scheduler arma i 4 job documentati (entry, monitor, health, + backup). +5. `asyncio.Event` blocca il main task fino a SIGTERM/SIGINT. +6. Alla ricezione del segnale: `scheduler.shutdown(wait=False)` → + `RuntimeContext.aclose()` (chiude `httpx.AsyncClient` condiviso) → + uscita pulita; il lock file viene rilasciato dal context manager. +7. Il container Docker, in modalità `restart: unless-stopped`, + resta down se l'engine ha ricevuto SIGTERM dal compose `stop`; + riparte automaticamente solo se il processo è morto per crash. diff --git a/docs/05-data-model.md b/docs/05-data-model.md index 1c711c6..9faa5d1 100644 --- a/docs/05-data-model.md +++ b/docs/05-data-model.md @@ -6,21 +6,30 @@ 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. + proposte in attesa di fill, snapshot DVOL. - **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. + exit, health, decisione di hold, alert) viene scritto in modo + immutabile prima di toccare lo stato. +- **L'audit chain è la verità per ciò che era stato registrato.** + Hash chain SHA-256 su `data/audit.log`, ancorata in + `system_state.last_audit_hash`: una manomissione del file viene + rilevata al boot successivo dall'orchestrator. +- Lo stato si può sempre **ricostruire dal broker** in caso di + corruzione tramite `runtime/recovery.py`. ## 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. +Migrazione gestita con file SQL semplici sotto +`src/cerbero_bite/state/migrations/`, applicati in sequenza dal runner +in `state/db.py` che incrementa `PRAGMA user_version`. Forward-only. +Tutti i pragmi runtime (`foreign_keys = ON`, `journal_mode = WAL`, +`synchronous = NORMAL`) vengono applicati a ogni connessione aperta da +`state.db.connect`. ### `positions` -Posizione Cerberus Bite (aperta, in chiusura, chiusa). +Posizione Cerberus Bite (proposta, in attesa di fill, aperta, in +chiusura, chiusa, cancellata). ```sql CREATE TABLE positions ( @@ -30,7 +39,7 @@ CREATE TABLE positions ( expiry TEXT NOT NULL, -- ISO8601 UTC short_strike NUMERIC NOT NULL, long_strike NUMERIC NOT NULL, - short_instrument TEXT NOT NULL, + short_instrument TEXT NOT NULL, -- ETH-15MAY26-2475-P long_instrument TEXT NOT NULL, n_contracts INTEGER NOT NULL, spread_width_usd NUMERIC NOT NULL, @@ -61,27 +70,36 @@ 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 +proposed → riga creata, place_combo_order in volo +awaiting_fill → ordine accettato da Deribit, in attesa di fill open → fill confermato, monitoring attivo -closing → istruzione close inviata, in attesa fill +closing → close combo inviata, in attesa di fill di chiusura closed → close fill confermato, P&L calcolato -cancelled → proposta rifiutata da Adriano o no fill +cancelled → ordine respinto dal broker o nessun fill nella finestra ``` +I `Decimal` documentati come `NUMERIC` vengono persistiti come `TEXT` +con `str(Decimal)`: la conversione avviene nel layer `Repository` per +preservare la precisione e non scivolare mai su `float`. + ### `instructions` -Tracciamento di ogni `push_user_instruction` inviata a Cerbero core. +Tracciamento di ogni ordine inviato a Deribit tramite +`mcp-deribit.place_combo_order` o `cancel_order`. La tabella esiste +per audit forense: a fronte di una posizione chiusa serve poter +ricostruire la sequenza di ordini broker che hanno portato al fill. ```sql CREATE TABLE instructions ( - instruction_id TEXT PRIMARY KEY, -- UUID dato a Cerbero core + instruction_id TEXT PRIMARY KEY, -- UUID Bite-side 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 + payload_json TEXT NOT NULL, -- JSON della response Deribit + -- (combo_instrument, order_id, state, ...) sent_at TEXT NOT NULL, - acknowledged_at TEXT, - filled_at TEXT, + acknowledged_at TEXT, -- riservato a un futuro hook ack + filled_at TEXT, -- compilato dal recovery o da un + -- hook che monitora i fill cancelled_at TEXT, actual_fill_eth NUMERIC, -- prezzo medio fill effettivo actual_fees_eth NUMERIC @@ -90,6 +108,12 @@ CREATE TABLE instructions ( CREATE INDEX idx_instructions_proposal ON instructions(proposal_id); ``` +Cerbero Bite invia direttamente l'ordine al broker: il payload qui +salvato è la response di `mcp-deribit.place_combo_order` (che a sua +volta wrappa `private/create_combo` + `private/buy|sell`). Niente +relazioni con un eventuale `cerbero-memory`: quel servizio non è +parte del decision loop. + ### `decisions` Storia delle valutazioni del decision loop (anche quando l'esito è @@ -103,7 +127,7 @@ CREATE TABLE decisions ( 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 + action_taken TEXT, -- HOLD, no_entry, propose_open, broker_error, ... notes TEXT ); @@ -113,7 +137,9 @@ CREATE INDEX idx_decisions_proposal ON decisions(proposal_id); ### `dvol_history` -Snapshot DVOL ad ogni evaluation (utile per analisi di lungo periodo). +Snapshot DVOL + ETH spot ad ogni evaluation. Utile per il calcolo di +`return_4h` durante il monitor (vedi `runtime/monitor_cycle.py +_fetch_return_4h`) e per analisi post-mortem. ```sql CREATE TABLE dvol_history ( @@ -125,13 +151,15 @@ CREATE TABLE dvol_history ( ### `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. +Coda di azioni manuali generate dalla GUI Streamlit (vedi +`11-gui-streamlit.md`). Schema previsto in vista della Fase 4.5; al +momento la GUI non è implementata e la tabella resta vuota. ```sql CREATE TABLE manual_actions ( id INTEGER PRIMARY KEY AUTOINCREMENT, - kind TEXT NOT NULL, -- approve_proposal, reject_proposal, force_close, arm_kill, disarm_kill + 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, @@ -142,12 +170,14 @@ CREATE TABLE manual_actions ( 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. +Le `manual_actions` non bypassano i risk control: il consumer +(quando esisterà) applicherà gli stessi check di +`safety.system_healthy()` prima di eseguire. ### `system_state` -Singleton di stato globale (kill switch, last health check). +Singleton di stato globale (kill switch, last health check, anchor +audit chain). ```sql CREATE TABLE system_state ( @@ -158,54 +188,66 @@ CREATE TABLE system_state ( last_health_check TEXT NOT NULL, last_kelly_calib TEXT, config_version TEXT NOT NULL, - started_at TEXT NOT NULL + started_at TEXT NOT NULL, + last_audit_hash TEXT -- aggiunto dalla migration 0002 ); ``` +Il campo `last_audit_hash` viene aggiornato dal callback +`AuditLog.on_append` registrato in `runtime/dependencies.build_runtime`. +Al boot l'orchestrator confronta questo valore con il tail del file +`audit.log`: discrepanza → kill switch CRITICAL, vedi +`07-risk-controls.md`. + ## Log file Sotto `data/log/` un file per giorno: `cerbero-bite-YYYY-MM-DD.jsonl`. -Ogni linea è un evento JSON. +Ogni linea è un evento JSON arricchito automaticamente da +`structlog.contextvars` con `cycle` e `cycle_id` quando il record è +emesso dentro un ciclo (entry/monitor/health), in modo da poter +filtrare tutti i log relativi a una specifica esecuzione. ### Schema evento ```json { "ts": "2026-04-27T14:00:01.234Z", - "event": "ENTRY_PROPOSED", - "level": "INFO", - "module": "runtime.orchestrator", + "event": "entry placed", + "level": "info", + "logger": "cerbero_bite.runtime.entry", + "cycle": "entry", + "cycle_id": "9d2a8a6a-7f58-4f10-8b33-...", "proposal_id": "uuid-...", - "data": { ... }, - "decision_id": 12345 + "data": { ... } } ``` +### Audit log + +Il file `data/audit.log` è separato dai log JSONL ed è alimentato +esclusivamente da `safety.audit_log.AuditLog`. Ogni riga ha la forma: + +``` +|||prev_hash=|hash= +``` + +con `hash = SHA256("|||")`. +La chain è ancorata in `system_state.last_audit_hash`. Il file è +fsync'd ad ogni append. + ### Eventi previsti | Event | Quando | |---|---| -| `ENGINE_START` | Avvio engine | -| `ENGINE_STOP` | Stop pulito | +| `ENGINE_START` | Avvio engine completato (boot OK) | +| `RECOVERY_DONE` | Fine del recovery loop | | `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 | +| `ENTRY_PLACED` | Combo aperto e fill (o awaiting_fill) confermato | +| `HOLD` | Monitor ha valutato una posizione e nessun trigger ha fired | +| `EXIT_FILLED` | Close confermato dal broker | +| `KILL_SWITCH_ARMED` / `KILL_SWITCH_DISARMED` | Transizioni manuali o automatiche | +| `ALERT` | Routing severity-based dell'alert manager | +| `KELLY_RECALIBRATED` | Output del job mensile (Fase 5) | ### Rotation e ritenzione @@ -216,26 +258,35 @@ Ogni linea è un evento JSON. ## 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: +L'engine non mantiene una cache delle posizioni aperte: ogni ciclo +legge fresco da SQLite. Lo scheduler APScheduler è in-process e i job +ricevono il `RuntimeContext` (con `Repository`, `AuditLog`, +`KillSwitch`, alert manager, client MCP) dalla closure di +`Orchestrator.install_scheduler`. Ogni mutazione dei `positions` +segue il pattern: -1. Append evento al log file (fsync). -2. Begin transaction SQLite. -3. Update tabelle. -4. Commit. -5. Update cache RAM. +1. (Opzionale) Audit append per gli eventi tracciati nella chain. +2. `with transaction(conn):` → `repository.update_position_status(...)` + o `repository.create_*(...)`. +3. Notifica Telegram post-fact, dove pertinente. -Se il sistema crasha tra (1) e (5), al restart un riconciliatore confronta -log vs SQLite e ripristina la consistenza. +Il connessione SQLite è aperta e chiusa dentro la singola operazione, +mai mantenuta long-lived: WAL e auto-commit garantiscono che letture +da altri processi (es. CLI `state inspect`) non vedano stati parziali. ## 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). +Lo schema viene tracciato con il counter `PRAGMA user_version`. La +prima volta `0001_init.sql` viene applicato e versione → 1; alla +seconda esecuzione (o su DB già a versione 1) `0002_audit_anchor.sql` +viene applicato e versione → 2. `state.db.run_migrations` è +idempotente. 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`. +Backup orari di `state.sqlite` in +`data/backups/state-YYYYMMDD-HH.sqlite`, ritenuti per 30 giorni. +Operazione idempotente con SQLite `VACUUM INTO`. Lo job è registrato +nello scheduler dell'orchestrator (`backup_cron = "0 * * * *"`); è +disponibile anche come CLI `python scripts/backup.py` per backup +ad-hoc. diff --git a/docs/07-risk-controls.md b/docs/07-risk-controls.md index c079599..116debf 100644 --- a/docs/07-risk-controls.md +++ b/docs/07-risk-controls.md @@ -8,7 +8,7 @@ infrastrutturali o decisioni umane fuori posto. - **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. + da Adriano via CLI, mai automaticamente. - **Visibilità**: ogni evento di sicurezza viene loggato e notificato immediatamente. - **No silent close**: una posizione viene chiusa solo a seguito di @@ -19,45 +19,43 @@ infrastrutturali o decisioni umane fuori posto. ### Stato -`system_state.kill_switch ∈ {0, 1}`. Quando = 1, l'engine: +`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 +- **non** invia istruzioni di chiusura **automatiche** (il monitor + cycle salta quando il kill switch è armato) +- continua a notificare via Telegram gli alert con la severity + appropriata (vedi escalation tree) ### 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 | +| Causa | Auto-arm | Implementato | Note | +|---|---|---|---| +| MCP `cerbero-deribit` non risponde per 3 health check consecutivi | Sì | `runtime/health_check.py` | Severity HIGH | +| MCP `cerbero-macro` / `cerbero-portfolio` / `cerbero-hyperliquid` / `cerbero-sentiment` non risponde per 3 health check consecutivi | Sì | `runtime/health_check.py` | Severity HIGH | +| `mcp-deribit.environment_info.environment` ≠ `strategy.execution.environment` | Sì | `runtime/orchestrator.boot` + health check | Severity CRITICAL al boot, HIGH a runtime | +| Mismatch tra il tail del file `data/audit.log` e `system_state.last_audit_hash` (truncation o tampering) | Sì | `runtime/orchestrator._verify_audit_anchor` | Severity CRITICAL al boot | +| Stato SQLite incoerente con il broker (recovery non risolutivo) | Sì | `runtime/recovery.py` | Severity CRITICAL al boot | +| `place_combo_order` di chiusura respinto dal broker | Sì | `runtime/monitor_cycle.py` | Severity CRITICAL; la posizione torna in `open` per ritentare | +| `place_combo_order` di apertura respinto dal broker | Sì | `runtime/entry_cycle.py` | Severity HIGH; la posizione viene marcata `cancelled` | +| Hash chain audit non verifica (`audit verify` fallisce) | Manuale per ora; CLI `audit verify` segnala l'anomalia con exit 2 | `cli.py audit verify` + `safety/audit_log.verify_chain` | Severity CRITICAL quando integrata nel boot | +| Comando manuale via `cerbero-bite kill-switch arm` | Sì | `cli.py kill_switch_arm` | Severity HIGH (operator-initiated) | ### Disarm ```bash -cerbero-bite kill-switch --disarm --reason "" +cerbero-bite kill-switch disarm --reason "" \ + --db data/state.sqlite \ + --audit data/audit.log ``` -oppure via Telegram: - -``` -/disarm -``` - -In entrambi i casi il sistema chiede una conferma esplicita ("yes I am -sure"). +L'operazione è transazionale: SQLite `system_state.kill_switch = 0` + +una linea `KILL_SWITCH_DISARMED` nella audit chain con il motivo. Il +disarm non riavvia automaticamente lo scheduler; è il prossimo tick +naturale (entry settimanale o monitor 12h) a far ripartire la +decisione. ## Cap di rischio (oltre alle regole di strategia) @@ -66,61 +64,113 @@ ma sono applicati in modo difensivo come **ultima linea**: | Misura | Limite | Comportamento se superato | |---|---|---| -| Notional combo singolo | 200 EUR | Sizing engine reject | +| Notional combo singolo | 200 EUR | Sizing engine reject (`undersize`) | | 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 | +| Posizioni concorrenti CB | 1 (default per la strategia ETH) | Entry cycle reject (`has_open_position`) | +| Trade aperti per giorno | 6 (intera Cerbero suite) | Non implementato — richiede integrazione cross-suite, lasciato a Cerbero core | +| Distance short strike | < 15 % spot OTM | Combo builder reject (`no_strike`) | +| Credit / width | < 0.30 | Combo builder reject (`no_strike`) | +| Slippage ≥ 8 % credito | Hard | Liquidity gate reject (`illiquid`) | +| DVOL fuori `[35, 90]` | Hard | Entry validator reject (`dvol`) | +| Funding ETH-PERP `|·| > 80%` annualizzato | Hard | Entry validator reject (`funding`) | +| ETH holdings > 30 % portfolio | Hard | Entry validator reject (`holdings`) | +| Macro evento `high` entro DTE | Hard | Entry validator reject (`macro`) | +| Dealer net gamma < `dealer_gamma_min` | Soft (gate disabilitabile) | Entry validator reject (`dealer short-gamma regime`) — vedi `01-strategy-rules.md §2.8` | +| `liquidation_squeeze_risk == "high"` | Soft (gate disabilitabile) | Entry validator reject (`imminent liquidation squeeze risk`) | -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. +I primi sei cap sono applicati direttamente dai moduli `core/`; gli +altri tre filtri quant-grade (DVOL, holdings, dealer-gamma, +liquidation) sono applicati da `entry_validator.validate_entry` con +soglie esplicite in `strategy.yaml`. + +## Single-instance lock + +Cerbero Bite acquisisce `data/.lockfile` con `fcntl.flock(LOCK_EX | +LOCK_NB)` all'avvio dell'engine (`runtime/lockfile.EngineLock`). Un +secondo container che provasse a partire sullo stesso file di stato +fallirebbe immediatamente con `LockError`, prima di toccare SQLite o i +client MCP. Il lock viene rilasciato in modo automatico dal kernel +quando il processo termina, anche su crash, quindi non rimane mai +"appeso". + +Caveat: `flock` è efficace solo all'interno dello stesso host +(filesystem locale del container o bind mount). Per uno scenario +distribuito multi-host servirebbe un lock service esterno (es. Redis +SET NX); al momento Cerbero Bite gira in un singolo container e il +lock locale è sufficiente. ## Dead-man switch -Se l'engine non scrive un evento `HEALTH_OK` per **15 minuti** consecutivi: +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`. +1. Il processo separato `scripts/dead_man.sh` (cron in user crontab, + indipendente dall'engine) rileva il silenzio cercando l'ultimo + `HEALTH_OK` nel JSONL del giorno. +2. Invia un alert al canale Telegram di backup (variabile + `DEAD_MAN_ALERT_CMD` o file `data/log/dead-man-alert.txt`). +3. Marca SQLite con `system_state.kill_switch=1` direttamente via + sqlite3 CLI. 4. Adriano interviene manualmente. -Il dead-man è scritto in shell minimale (no dipendenze Python) per -sopravvivere a corruzioni dell'env Python. +Lo script è scritto in shell minimale (no dipendenze Python) per +sopravvivere a corruzioni dell'env Python. La presenza del binario +`sqlite3` è opzionale: in sua assenza il dead-man genera comunque +l'alert ma salta lo step di arming SQLite. ## 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). +linea precedente (chain di hash, stile blockchain semplificato). Il +file viene `flush + os.fsync` a ogni append. Esempio: ``` -2026-04-27T14:00:01Z|ENTRY_PROPOSED|{...full payload...}|prev_hash=abc123...|hash=def456... +2026-04-27T14:00:01+00:00|ENTRY_PLACED|{"proposal_id":"...","spread_type":"bull_put"}|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. +Verificabile retroattivamente con `cerbero-bite audit verify`. La +verifica controlla: + +* parsing della struttura (`|||prev_hash=...|hash=...`); +* consistenza del JSON payload (oggetto, non lista o scalare); +* `prev_hash` di ogni linea uguale all'`hash` della precedente; +* `hash` ricalcolato uguale a quello memorizzato. + +Una discrepanza è trattata come tampering e produce `exit 2` dal +comando CLI; in regime servirà che lo stesso check, integrato nel +ciclo di health, armi il kill switch CRITICAL. + +### Anti-truncation + +La chain così com'è descritta resta valida anche se il file viene +**troncato** alla fine: i restanti record verificano l'uno con +l'altro. Per coprire questo caso Cerbero Bite mantiene un *anchor*: +ogni `AuditLog.append` invoca un callback registrato in +`runtime/dependencies.build_runtime` che persiste l'hash appena +scritto in `system_state.last_audit_hash`. Al boot +`Orchestrator._verify_audit_anchor` confronta il valore persistito con +il tail del file: in caso di mismatch (truncation, sostituzione, file +mancante) viene armato il kill switch CRITICAL prima che qualsiasi +ciclo trading parta. ## Dry-run mode -Tutti i flussi possono essere eseguiti in modalità `--dry-run`: +Il comando `cerbero-bite dry-run --cycle entry|monitor|health` esegue +**un singolo ciclo** senza avviare lo scheduler. Il ciclo usa lo +stesso codice di produzione (snapshot reali, `place_combo_order` reale +sul testnet), quindi non è "lettura sola" — è un'esecuzione one-shot. +Per testare flussi senza toccare il broker si usa il +`Cerbero_mcp` con `DERIBIT_TESTNET=true` (default), così +`mcp-deribit.environment_info` riporta `testnet` e gli ordini vanno +sul paper book. -- 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. +`enforce_hash` è disattivato in dry-run per agevolare il debug; il +comando `start` invece carica `strategy.yaml` con +`enforce_hash=True`, quindi mismatch dell'hash producono exit 1 +prima che l'engine tocchi qualsiasi stato. ## Versionamento config @@ -128,14 +178,33 @@ Ogni `strategy.yaml` ha: ```yaml config_version: "1.0.0" -config_hash: "" +config_hash: "" 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). +All'avvio di `cerbero-bite start` l'engine verifica che `config_hash` +corrisponda al contenuto del file (il calcolo esclude il valore stesso +del campo `config_hash`, vedi `config/loader.compute_config_hash`). +Mismatch → exit 1 prima del boot. La verifica protegge da modifiche +silenziose alla config, accidentali o malevole. + +Nuovi campi proposti dalla migration di Fase 4 hardening: + +```yaml +execution: + environment: "testnet" # testnet|mainnet — kill switch su mismatch broker + eur_to_usd: "1.075" # FX di sizing, override-able via CLI flag + +entry: + dealer_gamma_min: "0" # filtro §2.8 + dealer_gamma_filter_enabled: true + liquidation_filter_enabled: true +``` + +Ogni cambio richiede una nuova versione di `config_version`, +ricalcolo dell'hash via `cerbero-bite config hash` e commit con +giustificazione testuale nel messaggio. ## Escalation tree @@ -143,39 +212,51 @@ aggiornare l'hash, possibile tampering o errore umano). Evento anomalo │ ├── Severity LOW (es. 1 health check fallito) - │ └── Log WARNING, continua + │ └── Append in audit chain (event=ALERT severity=low), + │ continua │ - ├── Severity MEDIUM (es. MCP timeout occasionale) - │ ├── Log WARNING + Telegram digest giornaliero - │ └── Continua, retry next cycle + ├── Severity MEDIUM (es. snapshot dato mancante non bloccante) + │ ├── Append in audit chain + │ └── Telegram notify (priority=high), continua │ - ├── Severity HIGH (es. 3 health check consecutivi falliti) - │ ├── Kill switch ARM - │ ├── Telegram alert immediato - │ └── Adriano interviene + ├── Severity HIGH (es. 3 health check consecutivi falliti, + │ entry rejected dal broker) + │ ├── Append in audit chain + │ ├── Telegram notify_alert (priority=high) + │ ├── Kill switch ARM (idempotente) + │ └── Adriano interviene per disarmare │ - └── 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 + └── Severity CRITICAL (es. mismatch environment al boot, + hash chain rotta, close fallito su monitor) + ├── Append in audit chain + ├── Telegram notify_system_error (priority=critical) + ├── Kill switch ARM (idempotente) + └── Engine resta in idle finché Adriano non disarma ``` +L'implementazione vive in `runtime/alert_manager.AlertManager`; +ciascun modulo runtime accede al manager tramite +`RuntimeContext.alert_manager` e chiama +`am.low(...)` / `am.medium(...)` / `am.high(...)` / `am.critical(...)` +con `source` (modulo emittente) e `message` (descrizione human-friendly). + ## 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. + rispettato (retry, fallback, kill switch). +2. **State corruption test**: corrompi una riga `positions` e + verifica che il `recover_state` 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. + `audit verify` fallisca; tronca il file e verifica che il check + anchor al boot armi il kill switch. +4. **Replay test**: rigioca una giornata storica via + `cerbero-bite replay` (Fase 5/6), confronta le decisioni con un + set golden. +5. **Cap saturation test**: simula posizioni concorrenti, verifica + che il sizing engine rifiuti. I risultati sono documentati in `tests/golden/results-YYYY-MM-DD.md`. @@ -184,7 +265,7 @@ I risultati sono documentati in `tests/golden/results-YYYY-MM-DD.md`. Per chiarezza, queste cose **non** sono cap né kill switch — sono parte della strategia, gestite altrove: -- Profit take 50%: regola di strategia. +- 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.