"""TDD for :mod:`cerbero_bite.core.liquidity_gate`. Spec: ``docs/01-strategy-rules.md §4`` and ``docs/03-algorithms.md §2``. """ from __future__ import annotations from decimal import Decimal import pytest from cerbero_bite.config import StrategyConfig, golden_config from cerbero_bite.core.liquidity_gate import InstrumentSnapshot, check def _snap( *, instrument: str = "ETH-13MAY26-2400-P", bid: str = "0.090", ask: str = "0.100", mid: str = "0.095", open_interest: int = 500, volume_24h: int = 80, book_depth_top3: int = 20, ) -> InstrumentSnapshot: return InstrumentSnapshot( instrument=instrument, bid=Decimal(bid), ask=Decimal(ask), mid=Decimal(mid), open_interest=open_interest, volume_24h=volume_24h, book_depth_top3=book_depth_top3, ) @pytest.fixture def cfg() -> StrategyConfig: return golden_config() # Two-leg vertical: short at 0.095 mid, long at 0.060 mid → credit 0.035 ETH. # Tight book: per-contract slippage 0.001 ETH → ~2.86% of credit at n=1. def _good_pair() -> tuple[InstrumentSnapshot, InstrumentSnapshot]: short = _snap(bid="0.0945", ask="0.0955", mid="0.0950") long_ = _snap( instrument="ETH-13MAY26-2300-P", bid="0.0595", ask="0.0605", mid="0.0600", ) return short, long_ def test_clean_pair_passes(cfg: StrategyConfig) -> None: # tight book: per-contract slippage 0.001 ETH → 2.86% of 0.035 credit. short, long_ = _good_pair() res = check( short_leg=short, long_leg=long_, credit=Decimal("0.035"), n_contracts=1, cfg=cfg, ) assert res.accepted is True assert res.reasons == [] def test_short_leg_oi_below_threshold_fails(cfg: StrategyConfig) -> None: short, long_ = _good_pair() short = _snap( instrument=short.instrument, bid=str(short.bid), ask=str(short.ask), mid=str(short.mid), open_interest=99, ) res = check( short_leg=short, long_leg=long_, credit=Decimal("0.035"), n_contracts=1, cfg=cfg, ) assert res.accepted is False assert any("open interest" in r for r in res.reasons) def test_long_leg_volume_below_threshold_fails(cfg: StrategyConfig) -> None: short, long_ = _good_pair() long_ = _snap( instrument=long_.instrument, bid=str(long_.bid), ask=str(long_.ask), mid=str(long_.mid), volume_24h=10, ) res = check( short_leg=short, long_leg=long_, credit=Decimal("0.035"), n_contracts=1, cfg=cfg, ) assert res.accepted is False assert any("volume" in r for r in res.reasons) def test_spread_pct_above_cap_fails(cfg: StrategyConfig) -> None: short, long_ = _good_pair() # Make bid-ask huge: bid 0.05, ask 0.20 → spread/mid > 0.15 short = _snap( instrument=short.instrument, bid="0.050", ask="0.200", mid="0.125", ) res = check( short_leg=short, long_leg=long_, credit=Decimal("0.060"), n_contracts=1, cfg=cfg, ) assert res.accepted is False assert any("bid-ask" in r for r in res.reasons) def test_book_depth_below_threshold_fails(cfg: StrategyConfig) -> None: short, long_ = _good_pair() short = _snap( instrument=short.instrument, bid=str(short.bid), ask=str(short.ask), mid=str(short.mid), book_depth_top3=4, ) res = check( short_leg=short, long_leg=long_, credit=Decimal("0.035"), n_contracts=1, cfg=cfg, ) assert res.accepted is False assert any("depth" in r for r in res.reasons) def test_slippage_above_8pct_fails(cfg: StrategyConfig) -> None: # tight book * n=10 → 0.010 / 0.035 ≈ 28% slippage short, long_ = _good_pair() res = check( short_leg=short, long_leg=long_, credit=Decimal("0.035"), n_contracts=10, cfg=cfg, ) assert res.accepted is False assert any("slippage" in r for r in res.reasons) assert res.estimated_slippage_pct_of_credit > Decimal("0.08") def test_slippage_well_below_cap_passes(cfg: StrategyConfig) -> None: # tight book: short ask-mid = 0.0005, long mid-bid = 0.0005 → slip = 0.001 per # contract, credit 0.035 → 2.85% per contract. short = _snap(bid="0.0945", ask="0.0955", mid="0.0950") long_ = _snap( instrument="ETH-13MAY26-2300-P", bid="0.0595", ask="0.0605", mid="0.0600", ) res = check( short_leg=short, long_leg=long_, credit=Decimal("0.035"), n_contracts=1, cfg=cfg, ) assert res.accepted is True assert res.estimated_slippage_pct_of_credit < Decimal("0.08") def test_zero_credit_is_rejected(cfg: StrategyConfig) -> None: short, long_ = _good_pair() res = check( short_leg=short, long_leg=long_, credit=Decimal("0"), n_contracts=1, cfg=cfg, ) assert res.accepted is False assert any("credit" in r for r in res.reasons) def test_negative_credit_is_rejected(cfg: StrategyConfig) -> None: short, long_ = _good_pair() res = check( short_leg=short, long_leg=long_, credit=Decimal("-0.001"), n_contracts=1, cfg=cfg, ) assert res.accepted is False def test_zero_or_negative_n_contracts_is_rejected(cfg: StrategyConfig) -> None: short, long_ = _good_pair() for n in (0, -1): res = check( short_leg=short, long_leg=long_, credit=Decimal("0.035"), n_contracts=n, cfg=cfg, ) assert res.accepted is False assert any("contracts" in r for r in res.reasons) def test_non_positive_mid_is_flagged(cfg: StrategyConfig) -> None: short, long_ = _good_pair() bad_short = _snap( instrument=short.instrument, bid="0", ask="0", mid="0", open_interest=short.open_interest, volume_24h=short.volume_24h, book_depth_top3=short.book_depth_top3, ) res = check( short_leg=bad_short, long_leg=long_, credit=Decimal("0.035"), n_contracts=1, cfg=cfg, ) assert res.accepted is False assert any("mid price not positive" in r for r in res.reasons) def test_failures_accumulate(cfg: StrategyConfig) -> None: short = _snap(open_interest=10, volume_24h=2, book_depth_top3=1, bid="0.001", ask="0.500") long_ = _snap( instrument="ETH-13MAY26-2300-P", open_interest=5, volume_24h=1, book_depth_top3=1, bid="0.001", ask="0.500", ) res = check( short_leg=short, long_leg=long_, credit=Decimal("0.001"), n_contracts=10, cfg=cfg, ) assert res.accepted is False # at least 4 categories per leg + slippage = many reasons assert len(res.reasons) >= 5