Files
ramseshk c93af97059 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.
2026-08-10 12:48:05 +08:00

212 lines
6.8 KiB
Python

"""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}")