Add market creation engine + ML dashboard panel

Market creator (ml/create_markets.py):
- Generates Polymarket-compliant BinaryOption question specs
- Temperature threshold markets (30°C, 33°C, 35°C) per day
- Rain probability and heavy rain (10mm/25mm) markets per day
- Typhoon T8 weekly + monthly markets
- Wind gust threshold markets (>50 km/h at Chek Lap Kok)
- Calendar spread markets (rainy days per week)
- Resolution criteria tied to verifiable HKO public API sources
- Submission-ready JSON output with dispute windows + metadata
- Human-readable summary with probability bars + edge estimates

Web dashboard:
- /api/ml endpoint: per-model raw/calibrated probabilities
- ML Predictions card: color-coded, sorted by confidence
- Typhoon Probabilities card: T1/T3/T8 now + 72h/120h
- Auto-refreshes independently (avoids blocking chart refresh)

Usage:
  python ml/create_markets.py               # View proposals
  python ml/create_markets.py --submit      # JSON for Polymarket
This commit is contained in:
ramseshk
2026-08-11 11:26:46 +08:00
parent 7c2239a572
commit 0f44b91350
+436
View File
@@ -0,0 +1,436 @@
#!/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()