diff --git a/tests/test_ops_market_opportunities.py b/tests/test_ops_market_opportunities.py index e3c17123..c4cf7ace 100644 --- a/tests/test_ops_market_opportunities.py +++ b/tests/test_ops_market_opportunities.py @@ -205,3 +205,50 @@ def test_ops_market_opportunities_reuses_prewarmed_terminal_snapshot(monkeypatch assert captured["filters"]["limit"] == 180 assert captured["filters"]["min_edge_pct"] == 2 assert captured["filters"]["min_liquidity"] == 500 + + +def test_collect_events_and_prices_uses_gamma_hint_prices_without_clob_fanout(): + class FakeScanner: + def fetch_event(self, _slug): + return { + "slug": "highest-temperature-in-shanghai-on-july-3-2026", + "markets": [ + { + "question": "Will the highest temperature in Shanghai be 33°C on July 3?", + "outcomes": '["Yes", "No"]', + "clobTokenIds": '["yes-33", "no-33"]', + "outcomePrices": '["0.12", "0.88"]', + } + ], + } + + def fetch_ask_price(self, _token_id): + raise AssertionError("CLOB should not be called when Gamma outcomePrices are present") + + events, prices, status = market_opportunities._collect_events_and_prices( + [_row()], + FakeScanner(), + ) + + assert status == "ready" + assert "shanghai" in events + assert prices == {"yes-33": 0.12, "no-33": 0.88} + + +def test_collect_events_and_prices_stops_on_time_budget(): + class SlowScanner: + def fetch_event(self, _slug): + raise AssertionError("time budget should stop external event fetching") + + def fetch_ask_price(self, _token_id): + raise AssertionError("time budget should stop external price fetching") + + events, prices, status = market_opportunities._collect_events_and_prices( + [_row()], + SlowScanner(), + time_budget_sec=0, + ) + + assert status == "partial" + assert events == {} + assert prices == {} diff --git a/web/services/ops/market_opportunities.py b/web/services/ops/market_opportunities.py index 603d69b1..d1c04b1b 100644 --- a/web/services/ops/market_opportunities.py +++ b/web/services/ops/market_opportunities.py @@ -18,6 +18,7 @@ GAMMA_API_BASE = "https://gamma-api.polymarket.com" CLOB_API_BASE = "https://clob.polymarket.com" _QUOTE_CACHE_TTL_SEC = 60 _EVENT_CACHE_TTL_SEC = 180 +_QUOTE_COLLECTION_TIME_BUDGET_SEC = 18.0 _CACHE_LOCK = threading.Lock() _EVENT_CACHE: Dict[str, Tuple[float, Optional[Dict[str, Any]]]] = {} @@ -405,11 +406,21 @@ class PolymarketQuoteScanner: def _collect_events_and_prices( rows: List[Dict[str, Any]], scanner: PolymarketQuoteScanner, + *, + time_budget_sec: float = _QUOTE_COLLECTION_TIME_BUDGET_SEC, ) -> Tuple[Dict[str, Optional[Dict[str, Any]]], Dict[str, Optional[float]], str]: events_by_city: Dict[str, Optional[Dict[str, Any]]] = {} prices: Dict[str, Optional[float]] = {} status = "ready" + started_at = time.monotonic() + + def budget_exhausted() -> bool: + return time.monotonic() - started_at >= max(0.0, float(time_budget_sec)) + for row in rows: + if budget_exhausted(): + status = "partial" + break city_key = _normalize_key(row.get("city")) if not city_key or city_key in events_by_city: continue @@ -433,9 +444,12 @@ def _collect_events_and_prices( if not token_id or token_id in prices: continue hint_price = hint_prices.get(market_side) - if hint_price is not None and hint_price > 0.30: + if hint_price is not None: prices[token_id] = hint_price continue + if budget_exhausted(): + status = "partial" + continue try: prices[token_id] = scanner.fetch_ask_price(token_id) except Exception: