HK Weather Prediction Market Pipeline: WeatherNext + HKO + Polymarket

- Open-Meteo WeatherNext API client for HK forecasts
- HKO public data client (current conditions, 9-day forecast, typhoon warnings)
- HK-specific weather extraction and calibration
- Polymarket market scanning, price discovery, and market creation proposals
- Trading strategy engine: edge detection, Kelly criterion sizing, probability calibration
- End-to-end pipeline with dry-run mode and scheduled runner
- Interactive dashboard with live HK weather + forecasts + trading signals

Dependencies: Python 3.10+, openmeteo-requests, pandas
No API keys needed for dry-run mode.
Polymarket trading requires private key in .env.
This commit is contained in:
ramseshk
2026-08-10 12:48:05 +08:00
commit c93af97059
21 changed files with 2497 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
"""Polymarket integration for HK weather prediction markets."""
from .polymarket_client import PolymarketClient
from .trader import Trader
__all__ = ["PolymarketClient", "Trader"]
+214
View File
@@ -0,0 +1,214 @@
"""Polymarket API client for market data and trading.
Uses the Gamma Markets API for market discovery and CLOB for order execution.
"""
import json
import time
import hashlib
from datetime import datetime
from typing import Optional, Dict, List
from urllib.parse import urlencode
import requests
from config import (
POLYMARKET_GAMMA_API,
POLYMARKET_CLOB_API,
MIN_LIQUIDITY_USDC,
)
class PolymarketClient:
"""Client for Polymarket prediction markets."""
def __init__(self):
self.gamma_url = POLYMARKET_GAMMA_API
self.clob_url = POLYMARKET_CLOB_API
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "HK-Weather-Market/1.0",
"Accept": "application/json",
})
def search_markets(
self,
query: str = "hong kong weather",
active: bool = True,
limit: int = 20,
) -> List[Dict]:
"""Search for weather-related markets on Polymarket."""
params = {
"query": query,
"active": str(active).lower(),
"closed": "false",
"limit": limit,
"archived": "false",
"order": "liquidity",
}
try:
url = f"{self.gamma_url}/markets?{urlencode(params)}"
resp = self.session.get(url, timeout=15)
resp.raise_for_status()
markets = resp.json()
return [
{
"id": m.get("id"),
"question": m.get("question"),
"condition_id": m.get("conditionId"),
"slug": m.get("slug"),
"volume": float(m.get("volume", 0)),
"liquidity": float(m.get("liquidity", 0)),
"volume_24hr": float(m.get("volume24hr", 0)),
"end_date": m.get("endDateIso"),
"start_date": m.get("startDateIso"),
"active": m.get("active"),
"closed": m.get("closed"),
"outcome_prices": json.loads(m.get("outcomePrices", "[]")),
"outcomes": json.loads(m.get("outcomes", "[]")),
"description": m.get("description", ""),
"category": m.get("category", ""),
"tags": m.get("tags", []),
}
for m in markets
]
except Exception as e:
print(f"Polymarket search error: {e}")
return []
def get_market(self, condition_id: str) -> Optional[Dict]:
"""Get a single market by condition ID."""
try:
url = f"{self.gamma_url}/markets/{condition_id}"
resp = self.session.get(url, timeout=15)
resp.raise_for_status()
m = resp.json()
return {
"id": m.get("id"),
"question": m.get("question"),
"condition_id": m.get("conditionId"),
"slug": m.get("slug"),
"volume": float(m.get("volume", 0)),
"liquidity": float(m.get("liquidity", 0)),
"end_date": m.get("endDateIso"),
"active": m.get("active"),
"closed": m.get("closed"),
"outcome_prices": json.loads(m.get("outcomePrices", "[]")),
"outcomes": json.loads(m.get("outcomes", "[]")),
"description": m.get("description", ""),
}
except Exception as e:
print(f"Polymarket get_market error: {e}")
return None
def get_market_price(self, token_id: str) -> Optional[float]:
"""Get current price for a specific outcome token."""
try:
url = f"{self.clob_url}/price?token_id={token_id}&side=buy"
resp = self.session.get(url, timeout=10)
if resp.status_code == 200:
data = resp.json()
return float(data.get("price", 0))
except Exception as e:
print(f"Polymarket price error: {e}")
return None
def get_order_book(self, token_id: str) -> Dict:
"""Get order book for a token."""
try:
url = f"{self.clob_url}/book?token_id={token_id}"
resp = self.session.get(url, timeout=10)
resp.raise_for_status()
return resp.json()
except Exception as e:
print(f"Polymarket orderbook error: {e}")
return {}
def find_relevant_weather_markets(self) -> List[Dict]:
"""Find all weather-related markets relevant to Hong Kong."""
queries = [
"hong kong weather",
"hong kong temperature",
"hong kong typhoon",
"hong kong rain",
"asia typhoon",
"south china sea",
]
all_markets = []
seen_ids = set()
for query in queries:
markets = self.search_markets(query=query)
for m in markets:
if m["id"] not in seen_ids and m.get("active") and not m.get("closed"):
seen_ids.add(m["id"])
all_markets.append(m)
all_markets.sort(key=lambda m: m.get("liquidity", 0), reverse=True)
return all_markets
def get_market_implied_probability(
self, condition_id: str, outcome_index: int = 0
) -> Optional[float]:
"""Get market-implied probability for a specific outcome.
Uses midpoint of best bid/ask when available, otherwise last price.
"""
market = self.get_market(condition_id)
if not market or "outcome_prices" not in market:
return None
if outcome_index < len(market["outcome_prices"]):
return float(market["outcome_prices"][outcome_index])
return None
def get_clob_token_id(self, condition_id: str, outcome_index: int = 0) -> Optional[str]:
"""Get CLOB token ID for a market outcome.
Token IDs are derived deterministically from condition ID + outcome index.
"""
try:
url = f"{self.clob_url}/markets/{condition_id}"
resp = self.session.get(url, timeout=10)
resp.raise_for_status()
data = resp.json()
tokens = data.get("tokens", [])
if outcome_index < len(tokens):
return tokens[outcome_index].get("token_id")
except Exception as e:
print(f"CLOB token ID error: {e}")
return None
def create_market(
self,
question: str,
outcomes: List[str],
end_date: str,
description: str = "",
) -> Optional[Dict]:
"""
Create a new market on Polymarket.
NOTE: Requires whitelisted API key and Polygonscan approval.
Markets go through curation before going live.
"""
print("Market creation requires curation approval from Polymarket.")
print(f"Would create: {question}")
print(f"Outcomes: {outcomes}")
print(f"End date: {end_date}")
return {
"status": "proposed",
"question": question,
"outcomes": outcomes,
"end_date": end_date,
"note": "Submit via Polymarket UI or contact partnerships@polymarket.com",
}
+211
View File
@@ -0,0 +1,211 @@
"""Trading execution engine for Polymarket weather markets.
Handles order placement, position sizing, and risk management
for the HK weather prediction market strategy.
"""
from datetime import datetime
from typing import Optional, Dict, List, Tuple
from dataclasses import dataclass, field
from .polymarket_client import PolymarketClient
from config import MIN_EDGE_BPS, MAX_POSITION_USDC, MIN_LIQUIDITY_USDC
@dataclass
class TradeSignal:
"""A trading signal from the strategy engine."""
market_id: str
condition_id: str
question: str
outcome_index: int
outcome_label: str
model_probability: float # Our model-implied probability (0-100)
market_probability: float # Market-implied probability (0-100)
edge_bps: float # Edge in basis points
recommended_size_usdc: float # Kelly-recommended bet size
max_size_usdc: float # Maximum allowed position
signal_type: str # "buy_yes", "buy_no", "pass"
@dataclass
class ExecutionResult:
"""Result of a trade execution."""
signal: TradeSignal
success: bool
order_id: Optional[str] = None
filled_amount: float = 0.0
avg_price: float = 0.0
error: Optional[str] = None
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
class Trader:
"""Execute trades based on strategy signals."""
def __init__(
self,
client: PolymarketClient,
private_key: str = "",
funder_address: str = "",
dry_run: bool = True,
):
self.client = client
self.private_key = private_key
self.funder_address = funder_address
self.dry_run = dry_run
self.clob = None
self.positions: Dict[str, float] = {}
self.trade_history: List[ExecutionResult] = []
if not dry_run and private_key:
self._init_clob()
def _init_clob(self):
"""Initialize CLOB client for live trading."""
try:
from py_clob_client.client import ClobClient
from py_clob_client.clob_types import OrderArgs
host = "https://clob.polymarket.com"
chain_id = 137 # Polygon mainnet
self.clob = ClobClient(
host=host,
key=self.private_key,
chain_id=chain_id,
funder=self.funder_address,
signature_type=2,
)
print("CLOB client initialized for live trading")
except Exception as e:
print(f"CLOB init failed: {e}. Running in dry-run mode.")
self.dry_run = True
def execute_signal(self, signal: TradeSignal) -> ExecutionResult:
"""Execute a single trade signal."""
if signal.signal_type == "pass":
return ExecutionResult(
signal=signal,
success=True,
note="No trade: edge below threshold",
)
# Get token ID
token_id = self.client.get_clob_token_id(
signal.condition_id, signal.outcome_index
)
if not token_id:
return ExecutionResult(
signal=signal,
success=False,
error="Could not get token ID",
)
# Calculate number of shares at size (each share = $1 if correct)
price = signal.market_probability / 100.0
size = min(signal.recommended_size_usdc, signal.max_size_usdc)
if size < 1.0:
return ExecutionResult(
signal=signal,
success=False,
error=f"Size too small: ${size:.2f}",
)
if self.dry_run:
return self._execute_dry_run(signal, token_id, size, price)
else:
return self._execute_live(signal, token_id, size, price)
def _execute_dry_run(
self, signal: TradeSignal, token_id: str, size: float, price: float
) -> ExecutionResult:
"""Simulate trade execution for testing."""
result = ExecutionResult(
signal=signal,
success=True,
order_id=f"DRY_RUN_{datetime.now().timestamp()}",
filled_amount=size,
avg_price=price,
)
self.trade_history.append(result)
position_key = f"{signal.condition_id}_{signal.outcome_index}"
self.positions[position_key] = self.positions.get(position_key, 0) + size
print(f" [DRY RUN] {signal.signal_type}: ${size:.2f} on '{signal.question}'"
f" @ {price:.4f} (edge: {signal.edge_bps:.0f}bps)")
return result
def _execute_live(
self, signal: TradeSignal, token_id: str, size: float, price: float
) -> ExecutionResult:
"""Execute real trade on Polymarket CLOB."""
if not self.clob:
return ExecutionResult(
signal=signal,
success=False,
error="CLOB not initialized",
)
try:
# Create a limit order (IOC to avoid partial fills on stale prices)
order_args = {
"token_id": token_id,
"price": price,
"size": size,
"side": "BUY" if signal.signal_type == "buy_yes" else "SELL",
}
response = self.clob.create_and_post_order(
order_args, orderType="GTC"
)
result = ExecutionResult(
signal=signal,
success=True,
order_id=response.get("orderID", ""),
filled_amount=float(response.get("filled_size", 0)),
avg_price=float(response.get("avg_price", price)),
)
self.trade_history.append(result)
print(f" [LIVE] {signal.signal_type}: ${size:.2f} on '{signal.question}'"
f" @ {price:.4f} (edge: {signal.edge_bps:.0f}bps)")
return result
except Exception as e:
return ExecutionResult(
signal=signal,
success=False,
error=str(e),
)
def get_positions_summary(self) -> Dict:
"""Get summary of current positions and P&L."""
total_bet = sum(self.positions.values())
open_trades = len([t for t in self.trade_history if t.success])
return {
"total_positions_value_usdc": total_bet,
"num_open_trades": open_trades,
"num_markets": len(self.positions),
"positions": self.positions,
"dry_run": self.dry_run,
}
def cancel_all_orders(self):
"""Cancel all open orders. Only works in live mode."""
if self.dry_run or not self.clob:
print("Cannot cancel orders in dry-run mode")
return
try:
self.clob.cancel_all()
print("All orders cancelled")
except Exception as e:
print(f"Cancellation error: {e}")