"""Unit tests for :mod:`cerbero_bite.config.schema`.""" from __future__ import annotations from decimal import Decimal import pytest from cerbero_bite.config import ( EntryConfig, ExitConfig, ShortStrikeSpec, SizingConfig, SpreadWidthSpec, StrategyConfig, StructureConfig, golden_config, ) def test_golden_config_builds_with_defaults() -> None: cfg = golden_config() assert cfg.config_version == "1.0.0-test" assert cfg.sizing.kelly_fraction == Decimal("0.13") assert cfg.exit.profit_take_pct_of_credit == Decimal("0.50") assert cfg.entry.dvol_min == Decimal("35") assert cfg.entry.dvol_max == Decimal("90") def test_dvol_adjustment_default_bands_ordered() -> None: cfg = golden_config() bands = cfg.sizing.dvol_adjustment assert [b.dvol_under for b in bands] == [Decimal("45"), Decimal("60"), Decimal("80")] assert [b.multiplier for b in bands] == [ Decimal("1.00"), Decimal("0.85"), Decimal("0.65"), ] def test_validator_rejects_kelly_out_of_safe_range() -> None: with pytest.raises(ValueError, match="kelly_fraction"): golden_config(sizing=SizingConfig(kelly_fraction=Decimal("0.6"))) def test_validator_rejects_delta_target_outside_band() -> None: bad = ShortStrikeSpec( delta_target=Decimal("0.20"), delta_min=Decimal("0.10"), delta_max=Decimal("0.15"), ) with pytest.raises(ValueError, match="delta_target"): golden_config(structure=StructureConfig(short_strike=bad)) def test_validator_rejects_inverted_otm_range() -> None: bad = ShortStrikeSpec( distance_otm_pct_min=Decimal("0.30"), distance_otm_pct_max=Decimal("0.20"), ) with pytest.raises(ValueError, match="OTM range"): golden_config(structure=StructureConfig(short_strike=bad)) def test_validator_rejects_inverted_spread_width() -> None: bad = SpreadWidthSpec( target_pct_of_spot=Decimal("0.06"), min_pct_of_spot=Decimal("0.03"), max_pct_of_spot=Decimal("0.05"), ) with pytest.raises(ValueError, match="spread_width"): golden_config(structure=StructureConfig(spread_width=bad)) def test_validator_rejects_profit_take_ge_100pct() -> None: with pytest.raises(ValueError, match="profit_take"): golden_config(exit=ExitConfig(profit_take_pct_of_credit=Decimal("1.0"))) def test_validator_rejects_stop_loss_le_1x() -> None: with pytest.raises(ValueError, match="stop_loss"): golden_config(exit=ExitConfig(stop_loss_mark_x_credit=Decimal("1.0"))) def test_validator_rejects_inverted_dvol_range() -> None: with pytest.raises(ValueError, match="dvol_min"): golden_config(entry=EntryConfig(dvol_min=Decimal("90"), dvol_max=Decimal("35"))) def test_strategy_config_is_immutable() -> None: cfg = golden_config() with pytest.raises(Exception): # pydantic raises ValidationError on frozen cfg.entry = EntryConfig() # type: ignore[misc] def test_extra_fields_rejected_on_root() -> None: with pytest.raises(ValueError): StrategyConfig( config_version="x", config_hash="0" * 64, last_review="2026-04-26", last_reviewer="test", unknown_field=42, # type: ignore[call-arg] )