diff --git a/weather/openmeteo_client.py b/weather/openmeteo_client.py index 219e6ff..9e3b3c4 100644 --- a/weather/openmeteo_client.py +++ b/weather/openmeteo_client.py @@ -149,9 +149,11 @@ class OpenMeteoClient: daily_df.index = daily_df.index.tz_convert("Asia/Hong_Kong") daily_df.attrs["model"] = self.model - daily_df.attrs["hourly"] = hourly_df daily_df.attrs["fetch_time"] = datetime.now() + # Store hourly separately to avoid pandas attrs bug with DataFrames + self._last_hourly = hourly_df + return daily_df def get_current_conditions(self) -> dict: @@ -196,12 +198,13 @@ class OpenMeteoClient: def get_precipitation_probability(self, hours_ahead: int = 24) -> float: """Get precipitation probability for next N hours.""" df = self.get_forecast(lead_days=2) - if df is not None and hasattr(df, 'attrs') and 'hourly' in df.attrs: - hourly = df.attrs['hourly'] - now = pd.Timestamp.now(tz="Asia/Hong_Kong") - future = hourly[hourly.index <= now + pd.Timedelta(hours=hours_ahead)] - if 'precipitation_probability' in future.columns: - return float(future['precipitation_probability'].max()) + if df is not None: + hourly = getattr(self, '_last_hourly', pd.DataFrame()) + if not hourly.empty: + now = pd.Timestamp.now(tz="Asia/Hong_Kong") + future = hourly[hourly.index <= now + pd.Timedelta(hours=hours_ahead)] + if 'precipitation_probability' in future.columns: + return float(future['precipitation_probability'].max()) return 0.0 def get_scoring_window_summary(self, target_date: str) -> dict: @@ -220,7 +223,7 @@ class OpenMeteoClient: return {} row = df.loc[target] - hourly = df.attrs.get("hourly", pd.DataFrame()) + hourly = getattr(self, '_last_hourly', pd.DataFrame()) if not hourly.empty: day_hourly = hourly[ diff --git a/web_dashboard.py b/web_dashboard.py new file mode 100644 index 0000000..3c4f901 --- /dev/null +++ b/web_dashboard.py @@ -0,0 +1,644 @@ +#!/usr/bin/env python3 +""" +HK Weather Prediction Market — Dev Dashboard + +Flask web server with interactive charts: + - Real-time HKO observations + WeatherNext forecasts + - Temperature, rain probability, wind speed charts + - HKO vs WeatherNext model comparison + - Trading signals + Kelly sizing panel + +Usage: + python web_dashboard.py + # Open http://localhost:5000 +""" + +import json +import math +from datetime import datetime, timedelta +from pathlib import Path + +from flask import Flask, jsonify, render_template_string + +from weather.hk_extractor import HKExtractor +from weather.hko_client import HKOClient +from weather.openmeteo_client import OpenMeteoClient +from strategy.kelly import KellyCriterion +from config import HK_COORDS + +app = Flask(__name__) + +HTML_TEMPLATE = r''' + + + + + +HK Weather Prediction Market — Dev Dashboard + + + + + +
+
+

HK Weather Prediction Market

+

WeatherNext + HKO + Polymarket Strategy

+
+
+ ● Live + HKO + Open-Meteo + +
+
+ +
+ + +
+

Current Conditions HKO

+
Loading...
+ +
+ + +
+

Tomorrow

+
+ Temp Max + -- +
+
+ Temp Min + -- +
+
+ Rain Probability + -- +
+
+ Rain Total + -- +
+
+ Wind Max + -- +
+
+ Gusts Max + -- +
+
+
+ + +
+

5-Day Temperature Forecast

+
+
Open-Meteo + HKO comparison
+
+ + +
+

Rain Probability

+
+
+ + +
+

Wind Speed Forecast

+
+
+ + +
+

HKO vs WeatherNext Model Comparison

+
+
+ + +
+

Trading Signals

+
Computing...
+ +
+ + +
+

Kelly Sizing Simulator fraction=0.25

+
Computing...
+ +
+ + +
+

WeatherNext Raw Output (Open-Meteo)

+
Loading...
+
+ +
+ + + + +''' + + +@app.route("/api/dashboard") +def api_dashboard(): + """Return all dashboard data as JSON.""" + try: + hko = HKOClient() + om = OpenMeteoClient() + + forecast = om.get_forecast(lead_days=5) + + current_data = {} + try: + hko_cur = hko.get_current_weather() + om_cur = om.get_current_conditions() + if hko_cur and hko_cur.get("temperature"): + current_data["temperature"] = hko_cur["temperature"][0]["value"] + else: + current_data["temperature"] = round(float(om_cur.get("temperature", 0)), 1) + if hko_cur and hko_cur.get("humidity"): + current_data["humidity"] = hko_cur["humidity"][0]["value"] + else: + current_data["humidity"] = round(float(om_cur.get("humidity", 0)), 1) + current_data["feels_like"] = round(float(om_cur.get("apparent_temp", 0)), 1) + current_data["wind_speed"] = round(float(om_cur.get("wind_speed", 0)), 1) + current_data["wind_direction"] = round(float(om_cur.get("wind_direction", 0)), 0) + current_data["pressure"] = round(float(om_cur.get("surface_pressure", 0)), 1) + w = hko_cur.get("warning_message", "") if hko_cur else "" + current_data["warning"] = w[0] if isinstance(w, list) and w else (w if isinstance(w, str) else "") + except Exception: + current_data = {"temperature": "--", "humidity": "--"} + + typhoon_data = {"signal": hko.get_current_signal_level()} + + tomorrow_data = {} + if forecast is not None and len(forecast) > 0: + d0 = forecast.iloc[0] + d1 = forecast.iloc[1] if len(forecast) > 1 else d0 + tomorrow_data = { + "date": d1.name.strftime("%Y-%m-%d"), + "temp_max": round(float(d1.get("temperature_2m_max", 0)), 1), + "temp_min": round(float(d1.get("temperature_2m_min", 0)), 1), + "rain_prob": round(float(d1.get("precipitation_probability_max", 0)), 1), + "rain_sum": round(float(d1.get("precipitation_sum", 0)), 1), + "wind_max": round(float(d1.get("wind_speed_10m_max", 0)), 1), + "wind_gust": round(float(d1.get("wind_gusts_10m_max", 0)), 1), + } + + fc_series = {"dates": [], "tmax": [], "tmin": [], "rain_prob": [], + "rain_sum": [], "wind_max": [], "wind_gust": [], + "hko_tmax": [], "hko_tmin": []} + + if forecast is not None: + for idx, row in forecast.iterrows(): + fc_series["dates"].append(idx.strftime("%m/%d")) + fc_series["tmax"].append(round(float(row.get("temperature_2m_max", 0)), 1)) + fc_series["tmin"].append(round(float(row.get("temperature_2m_min", 0)), 1)) + fc_series["rain_prob"].append(round(float(row.get("precipitation_probability_max", 0)), 1)) + fc_series["rain_sum"].append(round(float(row.get("precipitation_sum", 0)), 1)) + fc_series["wind_max"].append(round(float(row.get("wind_speed_10m_max", 0)), 1)) + fc_series["wind_gust"].append(round(float(row.get("wind_gusts_10m_max", 0)), 1)) + + try: + hko_fc = hko.get_forecast() + if hko_fc: + for day in hko_fc[:5]: + fc_series["hko_tmax"].append(int(day.get("forecast_temp_max", 0)) if day.get("forecast_temp_max") else None) + fc_series["hko_tmin"].append(int(day.get("forecast_temp_min", 0)) if day.get("forecast_temp_min") else None) + while len(fc_series["hko_tmax"]) < len(fc_series["tmax"]): + fc_series["hko_tmax"].append(None) + fc_series["hko_tmin"].append(None) + except Exception: + pass + + signals = [] + if forecast is not None and len(forecast) > 1: + d1 = forecast.iloc[1] + tmrw = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d") + rain_p = round(float(d1.get("precipitation_probability_max", 50)), 1) + temp_p = round(float(d1.get("temperature_2m_max", 30)), 1) + signals = [ + { + "question": f"Rain in HK on {tmrw}?", + "model_prob": rain_p, + "market_prob": 50.0, + "edge_bps": round((rain_p - 50) * 100, 0), + "signal": "buy_no" if rain_p < 45 else ("buy_yes" if rain_p > 55 else "pass"), + "size": "—" + }, + { + "question": f"Temp > 35°C in HK on {tmrw}?", + "model_prob": min(95, max(5, 50 + (temp_p - 35) * 20)), + "market_prob": 40.0, + "edge_bps": round((min(95, max(5, 50 + (temp_p - 35) * 20)) - 40) * 100, 0), + "signal": "buy_yes" if temp_p >= 35 else "pass", + "size": "—" + }, + { + "question": f"Temp > 33°C in HK on {tmrw}?", + "model_prob": min(95, max(5, 50 + (temp_p - 33) * 20)), + "market_prob": 70.0, + "edge_bps": round((min(95, max(5, 50 + (temp_p - 33) * 20)) - 70) * 100, 0), + "signal": "buy_yes" if temp_p >= 33 else "pass", + "size": "—" + }, + ] + + kelly = KellyCriterion(bankroll_usdc=1000.0) + kelly_results = [] + if forecast is not None and len(forecast) > 1: + d1 = forecast.iloc[1] + rain_p = round(float(d1.get("precipitation_probability_max", 50)), 1) + temp_p = round(float(d1.get("temperature_2m_max", 30)), 1) + + scenarios = [ + ("Rain tomorrow", rain_p, 50), + ("Temp > 35°C", min(95, max(5, 50 + (temp_p - 35) * 20)), 35), + ("Temp > 33°C", min(95, max(5, 50 + (temp_p - 33) * 20)), 75), + ] + for name, our_p, mkt_p in scenarios: + r_yes = kelly.size_bet(our_p, mkt_p, "buy_yes") + r_no = kelly.size_bet(our_p, mkt_p, "buy_no") + if r_yes.kelly_active and r_yes.size_usdc > r_no.size_usdc: + r = r_yes + side = "buy_yes" + elif r_no.kelly_active: + r = r_no + side = "buy_no" + else: + r = r_yes + side = "pass" + + kelly_results.append({ + "name": name, + "our_prob": round(our_p, 1), + "mkt_prob": round(mkt_p, 1), + "edge": round(r.edge, 3), + "side": side, + "active": bool(r.kelly_active), + "size": round(r.size_usdc, 2) if r.kelly_active else 0, + }) + + return jsonify({ + "fetch_time": datetime.now().strftime("%H:%M:%S"), + "current": current_data, + "typhoon": typhoon_data, + "tomorrow": tomorrow_data, + "forecast": fc_series, + "signals": signals, + "kelly": kelly_results, + }) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/") +def index(): + return render_template_string(HTML_TEMPLATE) + + +if __name__ == "__main__": + import os + debug = os.environ.get("FLASK_DEBUG", "").lower() in ("1", "true", "yes") + print("\n" + "=" * 56) + print(" HK Weather Prediction Market — Dev Dashboard") + print(f" Starting at: http://localhost:5000") + print("=" * 56 + "\n") + app.run(host="0.0.0.0", port=5000, debug=debug)