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

8.7 KiB
Raw Blame History

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:

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_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:

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).

# .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]