"""TDD per :mod:`cerbero_bite.runtime.auto_pause` (§7-bis F).""" from __future__ import annotations from datetime import UTC, datetime, timedelta from decimal import Decimal import pytest from cerbero_bite.config.schema import AutoPauseConfig from cerbero_bite.runtime.auto_pause import ( evaluate_drawdown_breach, is_paused, pause_until, ) from cerbero_bite.state.models import SystemStateRecord _NOW = datetime(2026, 5, 1, 14, 0, tzinfo=UTC) def _state(**overrides: object) -> SystemStateRecord: base: dict[str, object] = { "kill_switch": 0, "last_health_check": _NOW, "config_version": "1.0.0", "started_at": _NOW - timedelta(hours=1), } base.update(overrides) return SystemStateRecord(**base) # type: ignore[arg-type] # --------------------------------------------------------------------------- # is_paused # --------------------------------------------------------------------------- def test_is_paused_returns_false_when_state_is_none() -> None: status = is_paused(None, now=_NOW) assert status.paused is False def test_is_paused_returns_false_when_until_is_none() -> None: status = is_paused(_state(), now=_NOW) assert status.paused is False def test_is_paused_returns_true_when_until_in_future() -> None: status = is_paused( _state(auto_pause_until=_NOW + timedelta(weeks=2), auto_pause_reason="DD breach"), now=_NOW, ) assert status.paused is True assert status.reason == "DD breach" def test_is_paused_returns_false_when_until_in_past() -> None: status = is_paused( _state(auto_pause_until=_NOW - timedelta(seconds=1)), now=_NOW, ) assert status.paused is False # --------------------------------------------------------------------------- # pause_until # --------------------------------------------------------------------------- def test_pause_until_adds_weeks() -> None: until = pause_until(_NOW, weeks=2) assert until == _NOW + timedelta(weeks=2) def test_pause_until_clamps_to_one_week_minimum() -> None: # weeks <= 0 deve cmq dare almeno 1 settimana di pausa, altrimenti # la cron settimanale potrebbe scattare comunque. assert pause_until(_NOW, weeks=0) == _NOW + timedelta(weeks=1) assert pause_until(_NOW, weeks=-3) == _NOW + timedelta(weeks=1) # --------------------------------------------------------------------------- # evaluate_drawdown_breach # --------------------------------------------------------------------------- def _cfg(**overrides: object) -> AutoPauseConfig: base: dict[str, object] = { "enabled": True, "lookback_trades": 5, "max_drawdown_pct": Decimal("0.10"), "pause_weeks": 2, } base.update(overrides) return AutoPauseConfig(**base) # type: ignore[arg-type] def test_drawdown_breach_when_enabled_and_threshold_exceeded() -> None: decision = evaluate_drawdown_breach( cfg=_cfg(), recent_pnl_usd=[Decimal("-50"), Decimal("-60"), Decimal("-40"), Decimal("-30"), Decimal("-20")], # cum −200 USD capital_usd=Decimal("1500"), ) # |200| / 1500 = 0.133 > 0.10 assert decision.should_pause is True assert decision.reason is not None assert "rolling DD" in decision.reason def test_no_breach_when_filter_disabled() -> None: decision = evaluate_drawdown_breach( cfg=_cfg(enabled=False), recent_pnl_usd=[Decimal("-200")] * 5, # massacro capital_usd=Decimal("1500"), ) assert decision.should_pause is False def test_no_breach_when_lookback_insufficient() -> None: decision = evaluate_drawdown_breach( cfg=_cfg(lookback_trades=5), recent_pnl_usd=[Decimal("-100")] * 3, # solo 3 trade, serve 5 capital_usd=Decimal("1500"), ) assert decision.should_pause is False def test_no_breach_when_cumulative_positive() -> None: # Anche con tante perdite, se la somma è positiva non scattiamo. decision = evaluate_drawdown_breach( cfg=_cfg(), recent_pnl_usd=[Decimal("-100"), Decimal("-50"), Decimal("300"), Decimal("-20"), Decimal("-10")], capital_usd=Decimal("1500"), ) assert decision.should_pause is False def test_no_breach_when_below_threshold() -> None: decision = evaluate_drawdown_breach( cfg=_cfg(), recent_pnl_usd=[Decimal("-30")] * 5, # cum −150 / 1500 = 10% esatto capital_usd=Decimal("1500"), ) # esattamente alla soglia (>=) ⇒ pausa armata assert decision.should_pause is True def test_no_breach_when_capital_zero_or_negative() -> None: decision = evaluate_drawdown_breach( cfg=_cfg(), recent_pnl_usd=[Decimal("-100")] * 5, capital_usd=Decimal("0"), ) assert decision.should_pause is False