#!/usr/bin/env python3 """ Market Creation Engine for HK Weather Prediction Markets. Generates Polymarket-compliant market proposals with: - Clear binary questions (verifiable via HKO public API) - Resolution criteria with specific data sources - Appropriate end dates + resolution delays - Category tags and metadata Output: - JSON market specs (for Polymarket curation submission) - Human-readable summary (for review before submission) Polymarket curation contact: partnerships@polymarket.com Usage: python ml/create_markets.py # Generate proposals python ml/create_markets.py --submit # Output submission-ready JSON python ml/create_markets.py --next 7 # Proposals for next 7 days """ import sys import json import argparse from pathlib import Path from datetime import datetime, timedelta from typing import Dict, List, Optional import numpy as np sys.path.insert(0, str(Path(__file__).parent.parent)) from ml.predictor import MLPredictor from ml.model import TARGET_DEFINITIONS from weather.openmeteo_client import OpenMeteoClient from weather.hko_client import HKOClient # Resolution sources — must be publicly verifiable RESOLUTION_SOURCES = { "hko_temperature": { "name": "Hong Kong Observatory Daily Temperature", "url": "https://data.weather.gov.hk/weatherAPI/opendata/weather.php?dataType=rhrread&lang=en", "description": "Maximum temperature recorded at HKO Headquarters (King's Park) on the target date, as published on the HKO Open Data API. Value in degrees Celsius.", "verification_field": "temperature.data[place='Hong Kong Observatory'].value", "verification_window": "00:00-23:59 HKT on target date", "dispute_window_hours": 24, }, "hko_rainfall": { "name": "Hong Kong Observatory Daily Rainfall", "url": "https://data.weather.gov.hk/weatherAPI/opendata/weather.php?dataType=rhrread&lang=en", "description": "Total rainfall (mm) recorded at HKO Headquarters on the target date. If any rainfall > 0mm is recorded, the market resolves YES.", "verification_field": "rainfall.data.max", "verification_window": "00:00-23:59 HKT on target date", "dispute_window_hours": 24, }, "hko_signal": { "name": "Hong Kong Observatory Tropical Cyclone Warning Signals", "url": "https://www.hko.gov.hk/en/wxinfo/currwx/tc_gis.htm", "description": "Official HKO tropical cyclone warning signal level. If signal T8 or above is hoisted at any time during the window, resolves YES.", "verification_field": "signal_level >= 8", "verification_window": "target_date 00:00 to end_date 23:59 HKT", "dispute_window_hours": 24, }, } MARKET_CATEGORIES = { "temperature": {"category": "Weather", "tags": ["weather", "temperature", "hong-kong"]}, "rainfall": {"category": "Weather", "tags": ["weather", "rain", "hong-kong"]}, "typhoon": {"category": "Weather", "tags": ["weather", "typhoon", "hong-kong"]}, "wind": {"category": "Weather", "tags": ["weather", "wind", "hong-kong"]}, } class MarketCreator: """Generate Polymarket-ready market proposals from ML forecasts.""" def __init__(self): self.predictor = MLPredictor() self.openmeteo = OpenMeteoClient() self.hko = HKOClient() def generate_proposals(self, days_ahead: int = 7) -> List[Dict]: """Generate market proposals for the next N days.""" self.predictor.fetch_and_predict() forecasts = self.predictor._last_predictions or {} fc = self.openmeteo.get_forecast(lead_days=days_ahead) proposals = [] # Temperature markets proposals.extend(self._temperature_markets(fc, forecasts, days_ahead)) # Rain markets proposals.extend(self._rain_markets(fc, forecasts, days_ahead)) # Typhoon markets proposals.extend(self._typhoon_markets(forecasts)) # Wind markets proposals.extend(self._wind_markets(fc, forecasts, days_ahead)) # Calendar spread / multi-day markets proposals.extend(self._calendar_spread_markets(fc, forecasts)) return proposals def _temperature_markets(self, fc, forecasts, days): """Generate temperature threshold markets.""" proposals = [] for day_offset in range(1, min(days + 1, len(fc) if fc is not None else 1)): if fc is None or day_offset >= len(fc): continue row = fc.iloc[day_offset] target_date = row.name.strftime("%Y-%m-%d") if hasattr(row.name, 'strftime') else str(row.name) tmax = float(row.get("temperature_2m_max", 0)) display_date = (datetime.strptime(target_date, "%Y-%m-%d") if "-" in target_date else datetime.now()).strftime("%b %d") for threshold in [30, 33, 35]: target = f"temp_gt_{threshold}c_24h" model_prob = forecasts.get(f"{target}_{target_date}", forecasts.get(target, 50.0)) # Only propose if model has a meaningful signal if 5 < model_prob < 95: proposals.append({ "question": f"Will the maximum temperature in Hong Kong exceed {threshold}°C on {display_date}?", "outcomes": ["Yes", "No"], "description": ( f"Market resolves YES if the maximum temperature recorded at the " f"Hong Kong Observatory (King's Park) exceeds {threshold}°C on " f"{datetime.strptime(target_date, '%Y-%m-%d').strftime('%B %d, %Y')}.\n\n" f"Resolution source: HKO Open Data API (temperature.data)." ), "end_date": (datetime.strptime(target_date, "%Y-%m-%d") + timedelta(days=1)).strftime("%Y-%m-%dT12:00:00"), "resolution_source": "hko_temperature", "category": "temperature", "model_probability": round(model_prob, 1), "target_date": target_date, "threshold": threshold, "forecast_value": round(tmax, 1), "tag": f"Hong Kong temperature {threshold}C {display_date}", }) return proposals def _rain_markets(self, fc, forecasts, days): """Generate rain probability/amount markets.""" proposals = [] for day_offset in range(1, min(days + 1, len(fc) if fc is not None else 1)): if fc is None or day_offset >= len(fc): continue row = fc.iloc[day_offset] target_date = row.name.strftime("%Y-%m-%d") if hasattr(row.name, 'strftime') else str(row.name) precip = float(row.get("precipitation_sum", 0)) prob = float(row.get("precipitation_probability_max", 0)) display_date = (datetime.strptime(target_date, "%Y-%m-%d") if "-" in target_date else datetime.now()).strftime("%b %d") # Rain yes/no model_p = forecasts.get("rain_gt_0mm_24h", 50.0) if 5 < model_p < 95: proposals.append({ "question": f"Will measurable rain (≥0.5mm) fall in Hong Kong on {display_date}?", "outcomes": ["Yes", "No"], "description": ( f"Market resolves YES if the HKO records ≥0.5mm of rainfall " f"at the Observatory station on {datetime.strptime(target_date, '%Y-%m-%d').strftime('%B %d, %Y')}.\n\n" f"Forecast: {prob:.0f}% probability, {precip:.1f}mm expected.\n\n" f"Resolution: HKO Open Data API (rainfall.data)." ), "end_date": (datetime.strptime(target_date, "%Y-%m-%d") + timedelta(days=1)).strftime("%Y-%m-%dT12:00:00"), "resolution_source": "hko_rainfall", "category": "rainfall", "model_probability": round(model_p, 1), "target_date": target_date, "forecast_value": round(precip, 1), "tag": f"Hong Kong rain {display_date}", }) # Heavy rain threshold for mm in [10, 25]: target = f"rain_gt_{mm}mm_24h" model_p = forecasts.get(target, 50.0) if 1 < model_p < 50: proposals.append({ "question": f"Will Hong Kong receive more than {mm}mm of rain on {display_date}?", "outcomes": ["Yes", "No"], "description": ( f"Market resolves YES if total rainfall at HKO Observatory exceeds {mm}mm " f"on {datetime.strptime(target_date, '%Y-%m-%d').strftime('%B %d, %Y')}.\n\n" f"Forecast: {precip:.1f}mm expected.\n\n" f"Resolution: HKO Open Data API (rainfall.data)." ), "end_date": (datetime.strptime(target_date, "%Y-%m-%d") + timedelta(days=1)).strftime("%Y-%m-%dT12:00:00"), "resolution_source": "hko_rainfall", "category": "rainfall", "model_probability": round(model_p, 1), "target_date": target_date, "threshold": mm, "forecast_value": round(precip, 1), "tag": f"Hong Kong rain {mm}mm {display_date}", }) return proposals def _typhoon_markets(self, forecasts): """Generate typhoon signal markets.""" proposals = [] typhoon = self.predictor._last_typhoon or {} today = datetime.now() # T8 in 7 days (weekly market) t8_7d = typhoon.get("typhoon_T8_120h", 0.5) end_date = (today + timedelta(days=7)).strftime("%Y-%m-%dT12:00:00") display_end = (today + timedelta(days=7)).strftime("%b %d") if t8_7d > 0.5: # Only propose when there's any signal proposals.append({ "question": f"Will the T8 typhoon signal be hoisted in Hong Kong by {display_end}?", "outcomes": ["Yes", "No"], "description": ( f"Market resolves YES if the Hong Kong Observatory hoists the " f"Tropical Cyclone Warning Signal No. 8 (or higher) at any time " f"between now and {display_end} 23:59 HKT.\n\n" f"Climatological probability for this period: {t8_7d:.1f}%.\n\n" f"Resolution: HKO Tropical Cyclone Warning System." ), "end_date": end_date, "resolution_source": "hko_signal", "category": "typhoon", "model_probability": round(t8_7d, 1), "target_date": display_end, "tag": "Hong Kong typhoon T8 weekly", }) # Monthly: any T8 this month month_end = today.replace(day=1) + timedelta(days=32) month_end = month_end.replace(day=1) remaining_days = (month_end - today).days month_name = today.strftime("%B") t8_month = typhoon.get("typhoon_T8_120h", 0.5) if t8_month > 0.5: proposals.append({ "question": f"Will a T8 or higher typhoon signal be hoisted in Hong Kong in {month_name} {today.year}?", "outcomes": ["Yes", "No"], "description": ( f"Market resolves YES if HKO hoists Tropical Cyclone Warning Signal " f"No. 8 or higher at any time during {month_name} {today.year}.\n\n" f"Historical frequency: ~{3/12*100:.0f}% of months have T8+ events.\n\n" f"Resolution: HKO Tropical Cyclone Warning System." ), "end_date": month_end.strftime("%Y-%m-%dT12:00:00"), "resolution_source": "hko_signal", "category": "typhoon", "model_probability": round(t8_month, 1), "target_date": month_end.strftime("%Y-%m-%d"), "tag": f"Hong Kong typhoon T8 {month_name} {today.year}", }) return proposals def _wind_markets(self, fc, forecasts, days): """Generate wind speed threshold markets.""" proposals = [] for day_offset in range(1, min(days + 1, len(fc) if fc is not None else 1)): if fc is None or day_offset >= len(fc): continue row = fc.iloc[day_offset] target_date = row.name.strftime("%Y-%m-%d") if hasattr(row.name, 'strftime') else str(row.name) wind = float(row.get("wind_speed_10m_max", 0)) gust = float(row.get("wind_gusts_10m_max", 0)) display_date = (datetime.strptime(target_date, "%Y-%m-%d") if "-" in target_date else datetime.now()).strftime("%b %d") if gust > 35: # Only propose when windy proposals.append({ "question": f"Will wind gusts exceed 50 km/h in Hong Kong on {display_date}?", "outcomes": ["Yes", "No"], "description": ( f"Market resolves YES if wind gusts ≥50 km/h are recorded at " f"Chek Lap Kok (airport) on {datetime.strptime(target_date, '%Y-%m-%d').strftime('%B %d, %Y')}.\n\n" f"Forecast wind: {wind:.0f} km/h, gusts: {gust:.0f} km/h.\n\n" f"Resolution: HKO wind gust data from Chek Lap Kok station." ), "end_date": (datetime.strptime(target_date, "%Y-%m-%d") + timedelta(days=1)).strftime("%Y-%m-%dT12:00:00"), "resolution_source": "hko_temperature", "category": "wind", "model_probability": round(forecasts.get("wind_gt_30kmh_24h", 50), 1), "target_date": target_date, "threshold": 50, "forecast_value": round(gust, 1), "tag": f"Hong Kong wind gusts {display_date}", }) return proposals def _calendar_spread_markets(self, fc, forecasts): """Generate calendar spread / multi-day aggregate markets.""" proposals = [] if fc is None or len(fc) < 4: return proposals today = datetime.now() next_7_days = [(today + timedelta(days=i)) for i in range(1, 8)] days_with_rain = 0 for i in range(min(7, len(fc) - 1)): row = fc.iloc[i + 1] prob = float(row.get("precipitation_probability_max", 0)) if prob > 35: days_with_rain += 1 if days_with_rain > 0: proposals.append({ "question": f"Will it rain on at least 3 days in Hong Kong from {next_7_days[0].strftime('%b %d')} to {next_7_days[-1].strftime('%b %d')}?", "outcomes": ["Yes", "No"], "description": ( f"Counts the number of days with measurable rain (≥0.5mm) at HKO " f"during the 7-day period. Resolves YES if ≥3 days have rain.\n\n" f"Forecast suggests ~{days_with_rain} days with rain probability >35%." ), "end_date": next_7_days[-1].strftime("%Y-%m-%dT12:00:00"), "resolution_source": "hko_rainfall", "category": "rainfall", "model_probability": round(min(95, max(5, days_with_rain * 20)), 1), "target_date": next_7_days[-1].strftime("%Y-%m-%d"), "threshold": 3, "tag": "Hong Kong rainy days weekly", }) return proposals def format_proposals(self, proposals: List[Dict]) -> str: """Format proposals as a human-readable summary.""" if not proposals: return "No market proposals generated." lines = [ f"=== Polymarket HK Weather Market Proposals ===", f"Generated: {datetime.now():%Y-%m-%d %H:%M HKT}", f"Total proposals: {len(proposals)}", f"", ] by_category = {} for p in proposals: cat = p.get("category", "other") by_category.setdefault(cat, []).append(p) for cat, ps in by_category.items(): lines.append(f"─── {cat.title()} ({len(ps)}) ───") for p in ps: model_p = p.get("model_probability", 50) fv = p.get("forecast_value", "-") threshold = p.get("threshold", "-") prob_bar = "█" * int(model_p / 5) + "░" * (20 - int(model_p / 5)) lines.append(f" [{model_p:>5.1f}% {prob_bar}] {p['question']}") if threshold != "-" and fv != "-": lines.append(f" threshold={threshold}, forecast={fv}") lines.append("") lines.append(f"═══ Submission Instructions ═══") lines.append(f"1. Review proposals above") lines.append(f"2. Submit via: python ml/create_markets.py --submit > proposals.json") lines.append(f"3. Contact: partnerships@polymarket.com with proposals.json") lines.append(f"4. Reference resolution sources are all HKO public APIs") lines.append(f"") lines.append(f"Rough edge estimate (ML model vs naive 50% prior):") for p in proposals[:5]: model_p = p.get("model_probability", 50) edge = abs(model_p - 50) if edge > 15: lines.append(f" {p['tag']:<45s} edge={edge:.1f}pp") return "\n".join(lines) def to_submission_json(self, proposals: List[Dict]) -> List[Dict]: """Convert proposals to Polymarket submission format.""" submission = [] for p in proposals: source = RESOLUTION_SOURCES.get( p.get("resolution_source", ""), RESOLUTION_SOURCES["hko_temperature"], ) cats = MARKET_CATEGORIES.get(p.get("category", ""), MARKET_CATEGORIES["temperature"]) submission.append({ "question": p["question"], "outcomes": p["outcomes"], "description": p["description"], "endDateIso": p["end_date"], "resolutionSource": { "name": source["name"], "url": source["url"], "verificationDescription": source["description"], "disputeWindowHours": source["dispute_window_hours"], }, "category": cats["category"], "tags": cats["tags"], "metadata": { "target_date": p.get("target_date", ""), "threshold": p.get("threshold", ""), "forecast_value": p.get("forecast_value", ""), "model_probability": p.get("model_probability", ""), }, }) return submission def main(): parser = argparse.ArgumentParser(description="Generate Polymarket weather market proposals") parser.add_argument("--next", type=int, default=5, help="Days ahead for daily markets") parser.add_argument("--submit", action="store_true", help="Output submission-ready JSON") parser.add_argument("--min-edge", type=float, default=10, help="Minimum edge (pp) to include") args = parser.parse_args() creator = MarketCreator() proposals = creator.generate_proposals(days_ahead=args.next) # Filter by edge proposals = [p for p in proposals if abs(p.get("model_probability", 50) - 50) >= args.min_edge] if args.submit: submission = creator.to_submission_json(proposals) print(json.dumps(submission, indent=2)) else: print(creator.format_proposals(proposals)) if __name__ == "__main__": main()