- 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>
8.7 KiB
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_onetest_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:
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:
@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:
@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
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:
# 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:
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-runma 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— passingruff format --check— passingmypy --strict src/— passingpytest --cov— coverage soglie rispettatebandit -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).
# .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]