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,268 @@
|
||||
# 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_one`
|
||||
- `test_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`**:
|
||||
|
||||
```python
|
||||
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`**:
|
||||
|
||||
```python
|
||||
@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:
|
||||
|
||||
```python
|
||||
@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
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```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:
|
||||
|
||||
```bash
|
||||
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-run` ma 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` — passing
|
||||
- `ruff format --check` — passing
|
||||
- `mypy --strict src/` — passing
|
||||
- `pytest --cov` — coverage soglie rispettate
|
||||
- `bandit -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).
|
||||
|
||||
```yaml
|
||||
# .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]
|
||||
```
|
||||
Reference in New Issue
Block a user