feat: Add Polymarket read-only data layer, Telegram push utility, and market alert engine.

This commit is contained in:
2569718930@qq.com
2026-03-12 10:06:09 +08:00
parent c156809231
commit 20d3e940e0
3 changed files with 220 additions and 28 deletions
+110 -21
View File
@@ -16,7 +16,7 @@ import re
import threading
import time
import unicodedata
from datetime import datetime
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple
import requests
@@ -53,6 +53,22 @@ def _safe_int(value: Any, default: int) -> int:
return default
def _safe_bool(value: Any) -> Optional[bool]:
if value is None:
return None
if isinstance(value, bool):
return value
if isinstance(value, (int, float)):
return bool(value)
if isinstance(value, str):
normalized = value.strip().lower()
if normalized in {"1", "true", "yes", "on"}:
return True
if normalized in {"0", "false", "no", "off"}:
return False
return bool(value)
def _normalize_text(value: Any) -> str:
text = str(value or "").strip().lower()
if not text:
@@ -180,6 +196,24 @@ def _extract_iso_date(value: Any) -> Optional[str]:
return None
def _parse_iso_datetime_utc(value: Any) -> Optional[datetime]:
if not value:
return None
text = str(value).strip()
if not text:
return None
# Prefer timestamps that include a time component; plain dates are ambiguous.
if "T" not in text:
return None
try:
dt = datetime.fromisoformat(text.replace("Z", "+00:00"))
except Exception:
return None
if dt.tzinfo is None:
return dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
def _build_city_token_index() -> Dict[str, List[str]]:
result: Dict[str, List[str]] = {}
for key, info in CITY_REGISTRY.items():
@@ -448,20 +482,43 @@ class PolymarketReadOnlyLayer:
or market.get("volume")
or market.get("volume24hr")
)
trade_state = self._market_trade_state(market)
primary_market_payload = {
"id": market.get("id"),
"question": market.get("question") or market.get("title"),
"slug": market_slug,
"condition_id": condition_id,
"end_date": market_date,
"active": trade_state.get("active"),
"closed": trade_state.get("closed"),
"accepting_orders": trade_state.get("accepting_orders"),
"ended_at_utc": trade_state.get("ended_at_utc"),
"tradable": trade_state.get("tradable"),
"tradable_reason": trade_state.get("reason"),
"liquidity": liquidity,
"volume": volume,
}
if not trade_state.get("tradable"):
scan["reason"] = (
"Matched market is not tradable."
+ (
f" reason={trade_state.get('reason')}"
if trade_state.get("reason")
else ""
)
)
scan["primary_market"] = primary_market_payload
scan["selected_condition_id"] = condition_id
scan["selected_slug"] = market_slug
scan["liquidity"] = liquidity
scan["volume"] = volume
return scan
tokens = self._extract_market_tokens(market)
yes_token, no_token = self._resolve_yes_no_tokens(tokens)
if not yes_token or not no_token:
scan["reason"] = "Matched market has no resolvable YES/NO token pair."
scan["primary_market"] = {
"condition_id": condition_id,
"end_date": market_date,
"id": market.get("id"),
"liquidity": liquidity,
"question": market.get("question") or market.get("title"),
"slug": market_slug,
"volume": volume,
}
scan["primary_market"] = primary_market_payload
scan["selected_condition_id"] = condition_id
scan["selected_slug"] = market_slug
scan["liquidity"] = liquidity
@@ -541,17 +598,7 @@ class PolymarketReadOnlyLayer:
{
"available": True,
"reason": None,
"primary_market": {
"id": market.get("id"),
"question": market.get("question") or market.get("title"),
"slug": market_slug,
"condition_id": condition_id,
"end_date": market_date,
"active": bool(market.get("active", False)),
"closed": bool(market.get("closed", False)),
"liquidity": liquidity,
"volume": volume,
},
"primary_market": primary_market_payload,
"selected_condition_id": condition_id,
"selected_slug": market_slug,
"market_price": market_price,
@@ -586,6 +633,46 @@ class PolymarketReadOnlyLayer:
)
return scan
def _market_trade_state(self, market: Dict[str, Any]) -> Dict[str, Any]:
active = _safe_bool(market.get("active"))
closed_raw = _safe_bool(market.get("closed"))
closed = bool(closed_raw) if closed_raw is not None else False
accepting_orders = _safe_bool(
market.get("acceptingOrders", market.get("accepting_orders"))
)
ended_at = None
for key in ("endDate", "resolutionDate", "closedTime", "gameStartTime"):
parsed = _parse_iso_datetime_utc(market.get(key))
if parsed is not None:
ended_at = parsed
break
now_utc = datetime.now(timezone.utc)
tradable = True
reason = None
if closed:
tradable = False
reason = "closed"
elif active is False:
tradable = False
reason = "inactive"
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,
"closed": closed,
"accepting_orders": accepting_orders,
"ended_at_utc": ended_at.isoformat() if ended_at is not None else None,
"tradable": tradable,
"reason": reason,
}
def _derive_signal(
self,
edge_percent: Optional[float],
@@ -1274,6 +1361,8 @@ class PolymarketReadOnlyLayer:
]
] = []
for market in candidate_markets:
if not self._market_trade_state(market).get("tradable"):
continue
bucket_temp = self._extract_market_bucket_temp(market)
if bucket_temp is None:
continue