From bb6fe211665a6b95e42ee843104c153ceb72ab92 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Tue, 21 Apr 2026 21:54:20 +0800 Subject: [PATCH] feat: implement read-only Polymarket data collection layer with market discovery and price fetching --- src/data_collection/polymarket_readonly.py | 26 ++++++---- tests/test_polymarket_readonly.py | 56 ++++++++++++++++++++++ 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/data_collection/polymarket_readonly.py b/src/data_collection/polymarket_readonly.py index 93a033a2..443eb301 100644 --- a/src/data_collection/polymarket_readonly.py +++ b/src/data_collection/polymarket_readonly.py @@ -690,9 +690,15 @@ class PolymarketReadOnlyLayer: bucket["no_buy"] = no_buy if no_sell is not None: bucket["no_sell"] = no_sell - if yes_midpoint is not None: - bucket["market_price"] = yes_midpoint - bucket["probability"] = yes_midpoint + reference_price = yes_midpoint + if reference_price is None and yes_buy is not None and yes_sell is not None: + reference_price = (yes_buy + yes_sell) / 2.0 + if reference_price is None: + reference_price = yes_buy if yes_buy is not None else yes_sell + if reference_price is not None: + reference_price = max(0.0, min(1.0, float(reference_price))) + bucket["market_price"] = reference_price + bucket["probability"] = reference_price if yes_prices.get("quote_source"): bucket["quote_source"] = yes_prices.get("quote_source") if yes_prices.get("quote_age_ms") is not None: @@ -819,7 +825,6 @@ class PolymarketReadOnlyLayer: ended_at = parsed break - now_utc = datetime.now(timezone.utc) tradable = True reason = None if closed: @@ -831,9 +836,6 @@ class PolymarketReadOnlyLayer: elif accepting_orders is False: tradable = False reason = "not_accepting_orders" - elif ended_at is not None and ended_at <= now_utc: - tradable = False - reason = "past_end_time" return { "active": active, @@ -1397,6 +1399,8 @@ class PolymarketReadOnlyLayer: "sell": sell, "midpoint": midpoint, "last_trade_price": last_trade, + "quote_source": "polymarket_clob_client", + "quote_age_ms": 0, "book": book, "book_liquidity": book_liquidity, } @@ -1420,6 +1424,8 @@ class PolymarketReadOnlyLayer: "sell": sell, "midpoint": midpoint, "last_trade_price": last_trade, + "quote_source": "polymarket_clob_rest", + "quote_age_ms": 0, "book": book, "book_liquidity": book_liquidity, } @@ -1635,6 +1641,8 @@ class PolymarketReadOnlyLayer: no_sell = max(0.0, min(1.0, 1.0 - yes_sell)) market_slug = str(market.get("slug") or "").strip() + row_yes_token_id = str(yes_token.get("token_id") or "").strip() + row_no_token_id = str(no_token.get("token_id") or "").strip() top_rows.append( { "label": self._extract_market_bucket_label(market, bucket_temp), @@ -1646,8 +1654,8 @@ class PolymarketReadOnlyLayer: "yes_sell": yes_sell, "no_buy": no_buy, "no_sell": no_sell, - "yes_token_id": yes_token_id or None, - "no_token_id": no_token_id or None, + "yes_token_id": row_yes_token_id or None, + "no_token_id": row_no_token_id or None, "quote_source": yes_prices.get("quote_source"), "quote_age_ms": _safe_int(yes_prices.get("quote_age_ms"), 0), "slug": market_slug or None, diff --git a/tests/test_polymarket_readonly.py b/tests/test_polymarket_readonly.py index 78e4c4dc..1f4f8002 100644 --- a/tests/test_polymarket_readonly.py +++ b/tests/test_polymarket_readonly.py @@ -59,6 +59,7 @@ def test_fetch_token_market_data_prefers_orderbook_executable_prices(): assert data["sell"] == 0.24 assert data["midpoint"] == 0.5 assert data["last_trade_price"] == 0.49 + assert data["quote_source"] == "polymarket_clob_client" def test_get_token_market_data_prefers_fresh_ws_cache(): @@ -117,6 +118,56 @@ def test_price_analysis_computes_edge_kelly_and_lock(): assert analysis["best_side"] == "yes" +def test_trade_state_keeps_open_markets_tradable_after_gamma_end_date(): + layer = PolymarketReadOnlyLayer() + + state = layer._market_trade_state( + { + "active": True, + "closed": False, + "acceptingOrders": True, + "endDate": "2020-01-01T00:00:00Z", + } + ) + + assert state["tradable"] is True + assert state["reason"] is None + assert state["ended_at_utc"] == "2020-01-01T00:00:00+00:00" + + +def test_hydrate_bucket_prices_uses_executable_quotes_without_midpoint(): + layer = PolymarketReadOnlyLayer() + buckets = [ + { + "temp": 14.0, + "yes_token_id": "yes-token", + "no_token_id": "no-token", + } + ] + + def _fake_get_token_market_data(token_id): + if token_id == "yes-token": + return { + "buy": 0.66, + "sell": 0.70, + "quote_source": "polymarket_clob_rest", + "quote_age_ms": 0, + } + return {"buy": 0.30, "sell": 0.36} + + layer._get_token_market_data = _fake_get_token_market_data + + layer._hydrate_bucket_prices(buckets) + + assert buckets[0]["yes_buy"] == 0.66 + assert buckets[0]["yes_sell"] == 0.70 + assert buckets[0]["no_buy"] == 0.30 + assert buckets[0]["no_sell"] == 0.36 + assert round(buckets[0]["market_price"], 6) == 0.68 + assert round(buckets[0]["probability"], 6) == 0.68 + assert buckets[0]["quote_source"] == "polymarket_clob_rest" + + def test_build_top_temperature_buckets_dedupes_same_temperature(): layer = PolymarketReadOnlyLayer() @@ -195,6 +246,11 @@ def test_build_top_temperature_buckets_dedupes_same_temperature(): ) values = [row.get("value") for row in rows] + token_ids = [row.get("yes_token_id") for row in rows] assert len(values) == len(set(values)) + assert len(token_ids) == len(set(token_ids)) assert rows[0]["value"] == 14.0 + assert rows[0]["yes_token_id"] == ( + "highest-temperature-in-ankara-on-march-12-2026-14c-or-higher|yes" + ) assert all(not str(row.get("label") or "").startswith("<=") for row in rows)