This version of Antigravity is no longer supported. Please upgrade to receive the latest features.

This commit is contained in:
2569718930@qq.com
2026-04-30 09:50:17 +08:00
parent a984c22b62
commit 96dfb4c5b3
3 changed files with 230 additions and 12 deletions
+3 -1
View File
@@ -209,9 +209,11 @@ POLYMARKET_MARKET_SCAN_ENABLED=true
POLYMARKET_GAMMA_URL=https://gamma-api.polymarket.com
POLYMARKET_CLOB_URL=https://clob.polymarket.com
POLYMARKET_CHAIN_ID=137
POLYMARKET_HTTP_TIMEOUT_SEC=8
POLYMARKET_HTTP_TIMEOUT_SEC=20
POLYMARKET_MARKET_CACHE_TTL_SEC=60
POLYMARKET_PRICE_CACHE_TTL_SEC=30
# false = fetch CLOB book/depth for orderbook analysis; true = price-only, lighter but no book levels
POLYMARKET_FAST_PRICE_ONLY=false
POLYWEATHER_MARKET_SCAN_PAYLOAD_TTL_SEC=30
POLYMARKET_WS_PRICE_ENABLED=false
POLYMARKET_WS_MARKET_URL=wss://ws-subscriptions-clob.polymarket.com/ws/market
+170 -11
View File
@@ -87,6 +87,15 @@ MARKET_CITY_ALIASES: Dict[str, str] = {
"lau fau shan": "shenzhen",
}
MARKET_CITY_SLUG_ALIASES: Dict[str, str] = {
# Polymarket's weather event URL uses the colloquial NYC slug, while
# PolyWeather keeps the canonical registry key as "new york".
"new york": "nyc",
# The tracked station is Buckley/Aurora, but Polymarket lists this market
# under the user-facing Denver city name.
"aurora": "denver",
}
def _resolve_market_city_key(city_key: str) -> str:
return MARKET_CITY_ALIASES.get(city_key, city_key)
@@ -397,7 +406,7 @@ class PolymarketReadOnlyLayer:
.strip()
.rstrip("/")
)
self.http_timeout = _safe_float(os.getenv("POLYMARKET_HTTP_TIMEOUT_SEC")) or 8.0
self.http_timeout = _safe_float(os.getenv("POLYMARKET_HTTP_TIMEOUT_SEC")) or 20.0
self.market_cache_ttl = _safe_int(
os.getenv("POLYMARKET_MARKET_CACHE_TTL_SEC", "60"),
60,
@@ -418,7 +427,7 @@ class PolymarketReadOnlyLayer:
_safe_float(os.getenv("POLYMARKET_SIGNAL_MIN_LIQUIDITY")) or 500.0
)
self.edge_threshold = _safe_float(os.getenv("POLYMARKET_SIGNAL_EDGE_PCT")) or 2.0
fast_price_only = _safe_bool(os.getenv("POLYMARKET_FAST_PRICE_ONLY", "true"))
fast_price_only = _safe_bool(os.getenv("POLYMARKET_FAST_PRICE_ONLY", "false"))
self.fast_price_only = True if fast_price_only is None else bool(fast_price_only)
self._session = httpx.Client(
@@ -653,8 +662,16 @@ class PolymarketReadOnlyLayer:
)
return scan
yes_prices = self._get_token_market_data(str(yes_token.get("token_id")))
no_prices = self._get_token_market_data(str(no_token.get("token_id")))
yes_prices = self._merge_market_quote_fallback(
self._get_token_market_data(str(yes_token.get("token_id"))),
market,
"yes",
)
no_prices = self._merge_market_quote_fallback(
self._get_token_market_data(str(no_token.get("token_id"))),
market,
"no",
)
if liquidity is None:
liquidity = _extract_price(yes_prices.get("book_liquidity"))
@@ -1510,7 +1527,8 @@ class PolymarketReadOnlyLayer:
dt = datetime.fromisoformat(str(target_date))
except Exception:
return None
city_slug = str(city_key or "").strip().lower().replace(" ", "-")
market_slug_key = MARKET_CITY_SLUG_ALIASES.get(city_key, city_key)
city_slug = str(market_slug_key or "").strip().lower().replace(" ", "-")
if not city_slug:
return None
month_name = dt.strftime("%B").lower()
@@ -1804,6 +1822,115 @@ class PolymarketReadOnlyLayer:
"book_liquidity": book_liquidity,
}
def _has_quote_prices(self, quote: Optional[Dict[str, Any]]) -> bool:
if not isinstance(quote, dict) or not quote:
return False
return any(
_extract_price(quote.get(key)) is not None
for key in ("buy", "sell", "midpoint", "last_trade_price")
)
def _build_market_quote_fallback(
self,
market: Dict[str, Any],
outcome_side: str,
) -> Dict[str, Any]:
"""Build a price fallback from Gamma market-level quote fields.
CLOB `/price` and `/book` remain the preferred source. Gamma's market
payload still carries public `bestBid` / `bestAsk` / `outcomePrices`;
using it prevents a total "price unavailable" state when the CLOB
endpoint, batch payload, or token lookup is temporarily unavailable.
"""
if not isinstance(market, dict) or not market:
return {}
side = str(outcome_side or "").strip().lower()
outcome_prices = _json_or_list(market.get("outcomePrices"))
yes_probability = _extract_price(outcome_prices[0]) if len(outcome_prices) >= 1 else None
no_probability = _extract_price(outcome_prices[1]) if len(outcome_prices) >= 2 else None
best_bid = _extract_price(
market.get("bestBid")
or market.get("best_bid")
or market.get("bid")
)
best_ask = _extract_price(
market.get("bestAsk")
or market.get("best_ask")
or market.get("ask")
)
spread = _extract_price(market.get("spread"))
if spread is None and best_bid is not None and best_ask is not None:
spread = max(0.0, float(best_ask) - float(best_bid))
midpoint = (
(best_bid + best_ask) / 2.0
if best_bid is not None and best_ask is not None
else yes_probability
)
last_trade = _extract_price(market.get("lastTradePrice") or market.get("last_trade_price"))
if side == "no":
buy = _clamp_probability(1.0 - best_bid) if best_bid is not None else None
sell = _clamp_probability(1.0 - best_ask) if best_ask is not None else None
resolved_midpoint = (
_clamp_probability(1.0 - midpoint)
if midpoint is not None
else _clamp_probability(no_probability)
)
resolved_last_trade = (
_clamp_probability(1.0 - last_trade)
if last_trade is not None
else None
)
else:
buy = _clamp_probability(best_ask)
sell = _clamp_probability(best_bid)
resolved_midpoint = _clamp_probability(midpoint)
resolved_last_trade = _clamp_probability(last_trade)
if not any(value is not None for value in (buy, sell, resolved_midpoint, resolved_last_trade)):
return {}
return {
"buy": buy,
"sell": sell,
"midpoint": resolved_midpoint,
"spread": spread,
"last_trade_price": resolved_last_trade,
"quote_source": "polymarket_gamma_market_fallback",
"quote_age_ms": 0,
"book": None,
"book_liquidity": _extract_price(
market.get("liquidityClob")
or market.get("liquidityNum")
or market.get("liquidity")
),
}
def _merge_market_quote_fallback(
self,
quote: Optional[Dict[str, Any]],
market: Dict[str, Any],
outcome_side: str,
) -> Dict[str, Any]:
fallback = self._build_market_quote_fallback(market, outcome_side)
if not fallback:
return dict(quote or {})
if not isinstance(quote, dict) or not quote:
return fallback
merged = dict(fallback)
for key, value in quote.items():
if value is None:
continue
if isinstance(value, str) and not value.strip():
continue
merged[key] = value
if self._has_quote_prices(quote):
merged["quote_source"] = quote.get("quote_source") or merged.get("quote_source")
return merged
def _clob_get(self, path: str, params: Dict[str, Any]) -> Any:
url = f"{self.clob_url}{path}"
try:
@@ -1940,8 +2067,24 @@ class PolymarketReadOnlyLayer:
yes_token_id = str(yes_token.get("token_id") or "").strip()
no_token_id = str(no_token.get("token_id") or "").strip()
yes_prices = self._get_token_market_data(yes_token_id) if yes_token_id else {}
no_prices = self._get_token_market_data(no_token_id) if no_token_id else {}
yes_prices = (
self._merge_market_quote_fallback(
self._get_token_market_data(yes_token_id),
market,
"yes",
)
if yes_token_id
else {}
)
no_prices = (
self._merge_market_quote_fallback(
self._get_token_market_data(no_token_id),
market,
"no",
)
if no_token_id
else {}
)
yes_midpoint = _extract_price(yes_prices.get("midpoint"))
yes_implied = _extract_price(yes_token.get("implied_probability"))
@@ -2803,8 +2946,16 @@ class PolymarketReadOnlyLayer:
broad_quotes = self._batch_get_token_market_data(token_ids, include_books=False)
bias_inputs: List[Tuple[float, float]] = []
for entry in market_entries:
yes_quote = broad_quotes.get(entry["yes_token_id"], {})
no_quote = broad_quotes.get(entry["no_token_id"], {})
yes_quote = self._merge_market_quote_fallback(
broad_quotes.get(entry["yes_token_id"], {}),
entry["market"],
"yes",
)
no_quote = self._merge_market_quote_fallback(
broad_quotes.get(entry["no_token_id"], {}),
entry["market"],
"no",
)
market_event_probability = (
_extract_price(yes_quote.get("midpoint"))
or _extract_price(yes_quote.get("buy"))
@@ -3333,8 +3484,16 @@ class PolymarketReadOnlyLayer:
market_slug = str(entry["market"].get("slug") or "").strip()
if market_slug not in seen_slugs:
continue
yes_quote = precise_quotes.get(entry["yes_token_id"], {})
no_quote = precise_quotes.get(entry["no_token_id"], {})
yes_quote = self._merge_market_quote_fallback(
precise_quotes.get(entry["yes_token_id"], {}),
entry["market"],
"yes",
)
no_quote = self._merge_market_quote_fallback(
precise_quotes.get(entry["no_token_id"], {}),
entry["market"],
"no",
)
if yes_quote:
entry["yes_ask"] = _extract_price(yes_quote.get("buy")) or entry.get("yes_ask")
entry["yes_bid"] = _extract_price(yes_quote.get("sell")) or entry.get("yes_bid")
+57
View File
@@ -71,6 +71,7 @@ def test_fetch_token_market_data_uses_rest_orderbook_executable_prices():
def test_fetch_token_market_data_fast_price_only_skips_heavy_endpoints():
layer = PolymarketReadOnlyLayer()
layer.fast_price_only = True
calls = []
payloads = {
("/price", "BUY"): {"price": "0.23"},
@@ -122,6 +123,62 @@ def test_fetch_token_market_data_keeps_buy_sell_semantics_without_orderbook():
assert data["midpoint"] == 0.25
def test_weather_event_slug_uses_polymarket_city_aliases():
layer = PolymarketReadOnlyLayer()
assert (
layer._build_weather_event_slug("new york", "2026-04-30")
== "highest-temperature-in-nyc-on-april-30-2026"
)
assert (
layer._build_weather_event_slug("aurora", "2026-04-30")
== "highest-temperature-in-denver-on-april-30-2026"
)
def test_market_quote_fallback_uses_gamma_best_bid_ask_when_clob_missing():
layer = PolymarketReadOnlyLayer()
market = {
"bestBid": "0.53",
"bestAsk": "0.54",
"lastTradePrice": "0.54",
"spread": "0.01",
"outcomePrices": '["0.535", "0.465"]',
"liquidityClob": "57040.5",
}
yes = layer._merge_market_quote_fallback({}, market, "yes")
no = layer._merge_market_quote_fallback({}, market, "no")
assert yes["buy"] == 0.54
assert yes["sell"] == 0.53
assert yes["midpoint"] == 0.535
assert yes["quote_source"] == "polymarket_gamma_market_fallback"
assert no["buy"] == 0.47
assert round(no["sell"], 6) == 0.46
assert round(no["midpoint"], 6) == 0.465
assert no["book_liquidity"] == 57040.5
def test_market_quote_fallback_preserves_clob_prices_when_available():
layer = PolymarketReadOnlyLayer()
market = {
"bestBid": "0.53",
"bestAsk": "0.54",
"outcomePrices": '["0.535", "0.465"]',
}
merged = layer._merge_market_quote_fallback(
{"buy": 0.55, "sell": 0.52, "midpoint": 0.535, "quote_source": "polymarket_clob_rest"},
market,
"yes",
)
assert merged["buy"] == 0.55
assert merged["sell"] == 0.52
assert merged["quote_source"] == "polymarket_clob_rest"
def test_get_token_market_data_uses_price_cache_within_ttl():
layer = PolymarketReadOnlyLayer()
calls = []