"""Opportunity engine math tests.""" from __future__ import annotations from decimal import Decimal as D from arbitrage.book.l2 import BookRegistry from arbitrage.engine.opportunity import EngineConfig, EventIndex, OpportunityEngine from arbitrage.models import Event, Outcome def _two_outcome_event() -> Event: return Event( id="e1", slug="e1", title="two-outcome", is_neg_risk=True, end_date=None, outcomes=( Outcome(token_id="A", name="A", outcome_index=0), Outcome(token_id="B", name="B", outcome_index=1), ), ) def _engine( reg: BookRegistry, index: EventIndex, *, min_bps: int = 50, max_basket_usd: D = D("500"), ) -> OpportunityEngine: return OpportunityEngine( books=reg, index=index, config=EngineConfig( min_net_edge_bps=min_bps, fees_per_share_usd=D("0"), gas_per_basket_usd=D("0.10"), max_basket_usd=max_basket_usd, ), ) class TestEvaluate: def test_detects_arb_when_asks_sum_below_one(self) -> None: ev = _two_outcome_event() idx = EventIndex() idx.upsert(ev) reg = BookRegistry() reg.apply_snapshot("A", bids=[], asks=[(D("0.40"), D("200"))]) reg.apply_snapshot("B", bids=[], asks=[(D("0.50"), D("200"))]) opp = _engine(reg, idx).evaluate(ev) assert opp is not None assert opp.event_id == "e1" assert opp.net_edge_bps >= 50 assert opp.max_baskets > 0 def test_rejects_when_asks_sum_above_one(self) -> None: ev = _two_outcome_event() idx = EventIndex() idx.upsert(ev) reg = BookRegistry() reg.apply_snapshot("A", bids=[], asks=[(D("0.70"), D("200"))]) reg.apply_snapshot("B", bids=[], asks=[(D("0.50"), D("200"))]) assert _engine(reg, idx).evaluate(ev) is None def test_rejects_when_any_leg_has_no_asks(self) -> None: ev = _two_outcome_event() idx = EventIndex() idx.upsert(ev) reg = BookRegistry() reg.apply_snapshot("A", bids=[], asks=[]) reg.apply_snapshot("B", bids=[], asks=[(D("0.50"), D("200"))]) assert _engine(reg, idx).evaluate(ev) is None def test_rejects_when_edge_below_threshold(self) -> None: ev = _two_outcome_event() idx = EventIndex() idx.upsert(ev) reg = BookRegistry() # Sum = 0.995 -> 50bps gross, net will be below 50 after gas reg.apply_snapshot("A", bids=[], asks=[(D("0.495"), D("200"))]) reg.apply_snapshot("B", bids=[], asks=[(D("0.500"), D("200"))]) assert _engine(reg, idx, min_bps=50).evaluate(ev) is None def test_depth_clips_basket_count(self) -> None: ev = _two_outcome_event() idx = EventIndex() idx.upsert(ev) reg = BookRegistry() # Leg A is very thin reg.apply_snapshot("A", bids=[], asks=[(D("0.40"), D("12"))]) reg.apply_snapshot("B", bids=[], asks=[(D("0.50"), D("10000"))]) opp = _engine(reg, idx).evaluate(ev) assert opp is not None assert opp.max_baskets <= D("12") def test_vwap_degrades_with_size(self) -> None: """At large sizes we consume worse levels; edge per basket must shrink.""" ev = _two_outcome_event() idx = EventIndex() idx.upsert(ev) reg = BookRegistry() # Both legs: cheap top, expensive deep levels reg.apply_snapshot( "A", bids=[], asks=[(D("0.40"), D("10")), (D("0.48"), D("10000"))] ) reg.apply_snapshot( "B", bids=[], asks=[(D("0.50"), D("10")), (D("0.52"), D("10000"))] ) opp = _engine(reg, idx).evaluate(ev) assert opp is not None # At size 10, sum_vwap = 0.90. At larger sizes it'll rise toward 1.00. # The engine chose the size maximizing expected profit, so sum is <= 1.0. assert opp.sum_vwap_asks <= D("1.0")