This version of Antigravity is no longer supported. Please upgrade to receive the latest features.
This commit is contained in:
+3
-1
@@ -209,9 +209,11 @@ POLYMARKET_MARKET_SCAN_ENABLED=true
|
|||||||
POLYMARKET_GAMMA_URL=https://gamma-api.polymarket.com
|
POLYMARKET_GAMMA_URL=https://gamma-api.polymarket.com
|
||||||
POLYMARKET_CLOB_URL=https://clob.polymarket.com
|
POLYMARKET_CLOB_URL=https://clob.polymarket.com
|
||||||
POLYMARKET_CHAIN_ID=137
|
POLYMARKET_CHAIN_ID=137
|
||||||
POLYMARKET_HTTP_TIMEOUT_SEC=8
|
POLYMARKET_HTTP_TIMEOUT_SEC=20
|
||||||
POLYMARKET_MARKET_CACHE_TTL_SEC=60
|
POLYMARKET_MARKET_CACHE_TTL_SEC=60
|
||||||
POLYMARKET_PRICE_CACHE_TTL_SEC=30
|
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
|
POLYWEATHER_MARKET_SCAN_PAYLOAD_TTL_SEC=30
|
||||||
POLYMARKET_WS_PRICE_ENABLED=false
|
POLYMARKET_WS_PRICE_ENABLED=false
|
||||||
POLYMARKET_WS_MARKET_URL=wss://ws-subscriptions-clob.polymarket.com/ws/market
|
POLYMARKET_WS_MARKET_URL=wss://ws-subscriptions-clob.polymarket.com/ws/market
|
||||||
|
|||||||
@@ -87,6 +87,15 @@ MARKET_CITY_ALIASES: Dict[str, str] = {
|
|||||||
"lau fau shan": "shenzhen",
|
"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:
|
def _resolve_market_city_key(city_key: str) -> str:
|
||||||
return MARKET_CITY_ALIASES.get(city_key, city_key)
|
return MARKET_CITY_ALIASES.get(city_key, city_key)
|
||||||
@@ -397,7 +406,7 @@ class PolymarketReadOnlyLayer:
|
|||||||
.strip()
|
.strip()
|
||||||
.rstrip("/")
|
.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(
|
self.market_cache_ttl = _safe_int(
|
||||||
os.getenv("POLYMARKET_MARKET_CACHE_TTL_SEC", "60"),
|
os.getenv("POLYMARKET_MARKET_CACHE_TTL_SEC", "60"),
|
||||||
60,
|
60,
|
||||||
@@ -418,7 +427,7 @@ class PolymarketReadOnlyLayer:
|
|||||||
_safe_float(os.getenv("POLYMARKET_SIGNAL_MIN_LIQUIDITY")) or 500.0
|
_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
|
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.fast_price_only = True if fast_price_only is None else bool(fast_price_only)
|
||||||
|
|
||||||
self._session = httpx.Client(
|
self._session = httpx.Client(
|
||||||
@@ -653,8 +662,16 @@ class PolymarketReadOnlyLayer:
|
|||||||
)
|
)
|
||||||
return scan
|
return scan
|
||||||
|
|
||||||
yes_prices = self._get_token_market_data(str(yes_token.get("token_id")))
|
yes_prices = self._merge_market_quote_fallback(
|
||||||
no_prices = self._get_token_market_data(str(no_token.get("token_id")))
|
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:
|
if liquidity is None:
|
||||||
liquidity = _extract_price(yes_prices.get("book_liquidity"))
|
liquidity = _extract_price(yes_prices.get("book_liquidity"))
|
||||||
@@ -1510,7 +1527,8 @@ class PolymarketReadOnlyLayer:
|
|||||||
dt = datetime.fromisoformat(str(target_date))
|
dt = datetime.fromisoformat(str(target_date))
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
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:
|
if not city_slug:
|
||||||
return None
|
return None
|
||||||
month_name = dt.strftime("%B").lower()
|
month_name = dt.strftime("%B").lower()
|
||||||
@@ -1804,6 +1822,115 @@ class PolymarketReadOnlyLayer:
|
|||||||
"book_liquidity": book_liquidity,
|
"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:
|
def _clob_get(self, path: str, params: Dict[str, Any]) -> Any:
|
||||||
url = f"{self.clob_url}{path}"
|
url = f"{self.clob_url}{path}"
|
||||||
try:
|
try:
|
||||||
@@ -1940,8 +2067,24 @@ class PolymarketReadOnlyLayer:
|
|||||||
|
|
||||||
yes_token_id = str(yes_token.get("token_id") or "").strip()
|
yes_token_id = str(yes_token.get("token_id") or "").strip()
|
||||||
no_token_id = str(no_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 {}
|
yes_prices = (
|
||||||
no_prices = self._get_token_market_data(no_token_id) if no_token_id else {}
|
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_midpoint = _extract_price(yes_prices.get("midpoint"))
|
||||||
yes_implied = _extract_price(yes_token.get("implied_probability"))
|
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)
|
broad_quotes = self._batch_get_token_market_data(token_ids, include_books=False)
|
||||||
bias_inputs: List[Tuple[float, float]] = []
|
bias_inputs: List[Tuple[float, float]] = []
|
||||||
for entry in market_entries:
|
for entry in market_entries:
|
||||||
yes_quote = broad_quotes.get(entry["yes_token_id"], {})
|
yes_quote = self._merge_market_quote_fallback(
|
||||||
no_quote = broad_quotes.get(entry["no_token_id"], {})
|
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 = (
|
market_event_probability = (
|
||||||
_extract_price(yes_quote.get("midpoint"))
|
_extract_price(yes_quote.get("midpoint"))
|
||||||
or _extract_price(yes_quote.get("buy"))
|
or _extract_price(yes_quote.get("buy"))
|
||||||
@@ -3333,8 +3484,16 @@ class PolymarketReadOnlyLayer:
|
|||||||
market_slug = str(entry["market"].get("slug") or "").strip()
|
market_slug = str(entry["market"].get("slug") or "").strip()
|
||||||
if market_slug not in seen_slugs:
|
if market_slug not in seen_slugs:
|
||||||
continue
|
continue
|
||||||
yes_quote = precise_quotes.get(entry["yes_token_id"], {})
|
yes_quote = self._merge_market_quote_fallback(
|
||||||
no_quote = precise_quotes.get(entry["no_token_id"], {})
|
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:
|
if yes_quote:
|
||||||
entry["yes_ask"] = _extract_price(yes_quote.get("buy")) or entry.get("yes_ask")
|
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")
|
entry["yes_bid"] = _extract_price(yes_quote.get("sell")) or entry.get("yes_bid")
|
||||||
|
|||||||
@@ -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():
|
def test_fetch_token_market_data_fast_price_only_skips_heavy_endpoints():
|
||||||
layer = PolymarketReadOnlyLayer()
|
layer = PolymarketReadOnlyLayer()
|
||||||
|
layer.fast_price_only = True
|
||||||
calls = []
|
calls = []
|
||||||
payloads = {
|
payloads = {
|
||||||
("/price", "BUY"): {"price": "0.23"},
|
("/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
|
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():
|
def test_get_token_market_data_uses_price_cache_within_ttl():
|
||||||
layer = PolymarketReadOnlyLayer()
|
layer = PolymarketReadOnlyLayer()
|
||||||
calls = []
|
calls = []
|
||||||
|
|||||||
Reference in New Issue
Block a user