Files
Cerbero-Bite/docs/08-testing-validation.md
T
root 6ff021fbf4 feat(strategy): abbandono gating settimanale — entry daily 24/7
Crypto opera 24/7: la cadenza settimanale lunedì-only era un retaggio
TradFi senza giustificazione. La nuova cadenza è giornaliera (cron
0 14 * * *), con i gate quantitativi a decidere se entrare o saltare.

Cambiamenti principali:

* runtime/orchestrator.py — _CRON_ENTRY 0 14 * * * (era MON)
* runtime/auto_pause.py — pause_until(days=) (era weeks=); minimo
  clamp 1 giorno (era 1 settimana)
* core/backtest.py — MondayPick→DailyPick, monday_picks→daily_picks
  (1 pick per calendar-day all'ora target); Sharpe annualization su
  ~120 trade/anno (era 52)
* config/schema.py — default cron daily; max_concurrent_positions 1→5;
  AutoPauseConfig.pause_weeks→pause_days, default 14
* runtime/option_chain_snapshot_cycle.py + orchestrator — cron */15
  per accumulo continuo dataset di backtest empirico

Strategy yamls (config_version 1.3.0 → 1.4.0, hash rigenerati):

* strategy.yaml — max_concurrent 1→5, cap_aggregate coerente
* strategy.aggressiva.yaml — max_concurrent 2→8, cap_aggregate
  3200→6400, max_contracts_per_trade invariato a 16
* strategy.conservativa.yaml — max_concurrent 1→3
* tutti — pause_weeks→pause_days: 14

GUI (pages/7_📚_Strategia.py):

* slider Trade/anno: range 20-200 (era 8-30), default 110, help
  riallineato sulla math 365 candidature × pass-rate 30-40%
* card profili: versione letta dinamicamente da config_version invece
  che hard-coded "v1.2.0"
* warning "entrambi perdono soldi" ora valuta i P/L effettivi
  (cons['annual_pl'], aggr['annual_pl']) invece del win_rate grezzo;
  aggiunto stato intermedio quando solo conservativo è in perdita

Tests (450/450 passati):

* test_auto_pause: pause_days, clamp ≥1 giorno
* test_backtest: rinomina + ridisegno daily picks (assert su
  calendar-day dedupe e hour filter)
* test_sizing_engine: other_open_positions=5 per cap default
* test_config_loader: version 1.4.0

Docs (README + 9 file in docs/) — tutti i riferimenti weekly/lunedì
allineati a daily/24-7, volume option_chain ricalcolato per cron
*/15 (~1.1 MB/giorno, ~400 MB/anno).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 16:21:16 +00:00

269 lines
8.7 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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]
```