# 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_daily_open_happy_path` | Tutto OK → proposta inviata |
| `test_daily_open_no_strike_available` | Chain vuota nel range delta |
| `test_daily_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_daily_open_bull_put.yaml       # input snapshot
├── 2026-04-27_daily_open_bull_put.golden     # output atteso
└── runner.py
```

Modifica intenzionale di un algoritmo richiede aggiornamento del golden,
con commit message che spiega perché.

## Backtest determinist​​ico (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]
```
