Add web dev dashboard with real-time HK weather + model comparison

- Flask web server with interactive charts (Chart.js)
- Temperature, rain probability, wind speed charts with HKO vs Open-Meteo comparison
- Current conditions card with typhoon signal indicator
- Kelly criterion sizing simulator panel
- Trading signals panel with model-implied probabilities
- Raw forecast data JSON viewer
- Github-dark theme with responsive card layout
- Fixed Open-Meteo SDK compatibility and numpy bool serialization

Run: python web_dashboard.py  # http://localhost:5000
This commit is contained in:
ramseshk
2026-08-10 17:11:49 +08:00
parent c93af97059
commit e59076ad6a
2 changed files with 655 additions and 8 deletions
+7 -4
View File
@@ -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,8 +198,9 @@ 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']
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:
@@ -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[
+644
View File
@@ -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'''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HK Weather Prediction Market — Dev Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace;
background: #0d1117; color: #c9d1d9; min-height: 100vh;
padding: 16px;
}
h1 { font-size: 18px; margin-bottom: 4px; color: #58a6ff; }
h2 { font-size: 14px; margin-bottom: 12px; color: #8b949e; font-weight: 400; }
h3 { font-size: 13px; margin-bottom: 8px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 12px; }
.card {
background: #161b22; border: 1px solid #30363d; border-radius: 8px;
padding: 16px; position: relative; overflow: hidden;
}
.card.full { grid-column: 1 / -1; }
.card.wide { grid-column: span 2; }
.stat-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
.stat-label { color: #8b949e; font-size: 12px; }
.stat-value { font-size: 24px; font-weight: 700; color: #f0f6fc; }
.stat-unit { font-size: 12px; color: #8b949e; }
.stat-sub { font-size: 11px; color: #8b949e; }
.badge {
display: inline-block; padding: 2px 8px; border-radius: 12px;
font-size: 11px; font-weight: 600;
}
.badge-warn { background: #d299221a; color: #d29922; border: 1px solid #d2992240; }
.badge-danger { background: #f851491a; color: #f85149; border: 1px solid #f8514940; }
.badge-info { background: #58a6ff1a; color: #58a6ff; border: 1px solid #58a6ff40; }
.badge-green { background: #3fb9501a; color: #3fb950; border: 1px solid #3fb95040; }
canvas { width: 100% !important; max-height: 220px; }
.chart-wrap { position: relative; margin-top: 8px; }
.loading { text-align: center; padding: 20px; color: #8b949e; font-size: 13px; }
.error { color: #f85149; font-size: 12px; }
.timestamp { font-size: 11px; color: #484f58; text-align: right; margin-top: 8px; }
.signal-row { padding: 6px 0; border-bottom: 1px solid #21262d; font-size: 12px; }
.signal-row:last-child { border-bottom: none; }
.edge-pos { color: #3fb950; }
.edge-neg { color: #f85149; }
.signal-side { font-weight: 600; }
.btn {
background: #21262d; border: 1px solid #30363d; color: #c9d1d9;
padding: 4px 12px; border-radius: 6px; cursor: pointer; font-size: 11px;
font-family: inherit;
}
.btn:hover { background: #30363d; }
.btn:active { background: #161b22; }
.header-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.model-tag { font-size: 10px; color: #8b949e; margin-left: 6px; background: #21262d; padding: 1px 6px; border-radius: 8px; }
.refresh-indicator { font-size: 10px; color: #3fb950; }
table { width: 100%; border-collapse: collapse; font-size: 12px; }
th { text-align: left; color: #8b949e; padding: 4px 8px; border-bottom: 1px solid #30363d; font-weight: 400; }
td { padding: 4px 8px; border-bottom: 1px solid #21262d; }
tr:hover td { background: #1c2128; }
</style>
</head>
<body>
<div class="header-row">
<div>
<h1>HK Weather Prediction Market</h1>
<h2>WeatherNext + HKO + Polymarket Strategy</h2>
</div>
<div>
<span class="refresh-indicator" id="status">● Live</span>
<span class="model-tag">HKO + Open-Meteo</span>
<button class="btn" onclick="refresh()" style="margin-left: 8px;">Refresh</button>
</div>
</div>
<div class="grid">
<!-- CURRENT CONDITIONS -->
<div class="card">
<h3>Current Conditions <span class="model-tag">HKO</span></h3>
<div id="current-loading" class="loading">Loading...</div>
<div id="current-content" style="display:none">
<div class="stat-row">
<span class="stat-label">Temperature</span>
<span class="stat-value" id="cur-temp">--</span>
</div>
<div class="stat-row">
<span class="stat-label">Feels Like</span>
<span class="stat-value" id="cur-feels">--</span>
</div>
<div class="stat-row">
<span class="stat-label">Humidity</span>
<span class="stat-value" id="cur-hum">--</span>
</div>
<div class="stat-row">
<span class="stat-label">Wind</span>
<span class="stat-value" id="cur-wind">--</span>
</div>
<div class="stat-row">
<span class="stat-label">Pressure</span>
<span class="stat-value" id="cur-pres">--</span>
</div>
<div id="warnings-container" style="margin-top:8px"></div>
<div id="typhoon-container" style="margin-top:8px"></div>
</div>
</div>
<!-- TOMORROW SUMMARY -->
<div class="card">
<h3>Tomorrow <span id="tomorrow-date" class="model-tag"></span></h3>
<div class="stat-row">
<span class="stat-label">Temp Max</span>
<span class="stat-value" id="tom-tmax">--</span>
</div>
<div class="stat-row">
<span class="stat-label">Temp Min</span>
<span class="stat-value" id="tom-tmin">--</span>
</div>
<div class="stat-row">
<span class="stat-label">Rain Probability</span>
<span class="stat-value" id="tom-rain">--</span>
</div>
<div class="stat-row">
<span class="stat-label">Rain Total</span>
<span class="stat-value" id="tom-rain-mm">--</span>
</div>
<div class="stat-row">
<span class="stat-label">Wind Max</span>
<span class="stat-value" id="tom-wind">--</span>
</div>
<div class="stat-row">
<span class="stat-label">Gusts Max</span>
<span class="stat-value" id="tom-gust">--</span>
</div>
<div class="timestamp" id="model-source"></div>
</div>
<!-- TEMPERATURE CHART -->
<div class="card wide">
<h3>5-Day Temperature Forecast</h3>
<div class="chart-wrap"><canvas id="tempChart"></canvas></div>
<div class="timestamp">Open-Meteo + HKO comparison</div>
</div>
<!-- RAIN PROBABILITY CHART -->
<div class="card">
<h3>Rain Probability</h3>
<div class="chart-wrap"><canvas id="rainChart"></canvas></div>
</div>
<!-- WIND SPEED CHART -->
<div class="card">
<h3>Wind Speed Forecast</h3>
<div class="chart-wrap"><canvas id="windChart"></canvas></div>
</div>
<!-- HKO vs WEATHERNEXT COMPARISON -->
<div class="card wide">
<h3>HKO vs WeatherNext Model Comparison</h3>
<div id="comparison-table"></div>
</div>
<!-- TRADING SIGNALS -->
<div class="card">
<h3>Trading Signals</h3>
<div id="signals-loading" class="loading">Computing...</div>
<div id="signals-content" style="display:none"></div>
</div>
<!-- KELLY SIZING -->
<div class="card">
<h3>Kelly Sizing Simulator <span class="model-tag">fraction=0.25</span></h3>
<div id="kelly-loading" class="loading">Computing...</div>
<div id="kelly-content" style="display:none"></div>
</div>
<!-- RAW DATA -->
<div class="card full">
<h3>WeatherNext Raw Output (Open-Meteo)</h3>
<div id="raw-data" class="loading">Loading...</div>
</div>
</div>
<script>
let charts = {};
function destroyChart(id) {
if (charts[id]) { charts[id].destroy(); charts[id] = null; }
}
async function refresh() {
document.getElementById('status').textContent = '⟳ Loading...';
try {
const resp = await fetch('/api/dashboard');
const data = await resp.json();
renderAll(data);
document.getElementById('status').textContent = `● Live · ${data.fetch_time}`;
} catch(e) {
document.getElementById('status').textContent = '● Error';
console.error(e);
}
}
function renderAll(data) {
renderCurrent(data.current, data.typhoon);
renderTomorrow(data.tomorrow);
renderCharts(data.forecast);
renderComparison(data.forecast);
renderSignals(data.signals);
renderKelly(data.kelly);
renderRaw(data.forecast);
renderTyphoon(data.typhoon);
}
function renderCurrent(current, typhoon) {
document.getElementById('current-loading').style.display = 'none';
document.getElementById('current-content').style.display = '';
if (!current) return;
document.getElementById('cur-temp').innerHTML = current.temperature + '°C';
document.getElementById('cur-feels').innerHTML = current.feels_like + '°C';
document.getElementById('cur-hum').innerHTML = current.humidity + '%';
document.getElementById('cur-wind').innerHTML = current.wind_speed + ' km/h';
document.getElementById('cur-pres').innerHTML = current.pressure + ' hPa';
const wc = document.getElementById('warnings-container');
wc.innerHTML = '';
if (current.warning) {
wc.innerHTML = '<span class="badge badge-warn">' + current.warning + '</span>';
}
}
function renderTyphoon(typhoon) {
const tc = document.getElementById('typhoon-container');
if (!typhoon || typhoon.signal === 0) {
tc.innerHTML = '';
return;
}
const cls = typhoon.signal >= 8 ? 'badge badge-danger' : 'badge badge-warn';
tc.innerHTML = '<span class="' + cls + '">T' + typhoon.signal + ' Signal Active</span>';
}
function renderTomorrow(tom) {
if (!tom) return;
document.getElementById('tomorrow-date').textContent = tom.date;
document.getElementById('tom-tmax').innerHTML = tom.temp_max + '<span class="stat-unit">°C</span>';
document.getElementById('tom-tmin').innerHTML = tom.temp_min + '<span class="stat-unit">°C</span>';
document.getElementById('tom-rain').innerHTML = tom.rain_prob + '<span class="stat-unit">%</span>';
document.getElementById('tom-rain-mm').innerHTML = tom.rain_sum + '<span class="stat-unit">mm</span>';
document.getElementById('tom-wind').innerHTML = tom.wind_max + '<span class="stat-unit">km/h</span>';
document.getElementById('tom-gust').innerHTML = tom.wind_gust + '<span class="stat-unit">km/h</span>';
document.getElementById('model-source').textContent = 'Source: Open-Meteo + HKO';
}
function renderCharts(fc) {
if (!fc || !fc.dates) return;
const dates = fc.dates;
const ctx = document.getElementById.bind(document);
// Temperature chart
destroyChart('temp');
charts.temp = new Chart(ctx('tempChart'), {
type: 'line',
data: {
labels: dates,
datasets: [
{
label: 'Tmax (Open-Meteo)',
data: fc.tmax, borderColor: '#f85149', backgroundColor: '#f8514910',
fill: false, tension: 0.3, pointRadius: 3
},
{
label: 'Tmin (Open-Meteo)',
data: fc.tmin, borderColor: '#58a6ff', backgroundColor: '#58a6ff10',
fill: false, tension: 0.3, pointRadius: 3
},
{
label: 'Tmax (HKO)',
data: fc.hko_tmax, borderColor: '#f85149', backgroundColor: 'transparent',
fill: false, tension: 0.3, pointRadius: 4, pointStyle: 'rect',
borderDash: [5,5], borderWidth: 2
},
{
label: 'Tmin (HKO)',
data: fc.hko_tmin, borderColor: '#58a6ff', backgroundColor: 'transparent',
fill: false, tension: 0.3, pointRadius: 4, pointStyle: 'rect',
borderDash: [5,5], borderWidth: 2
},
{
label: '35°C Threshold',
data: dates.map(() => 35), borderColor: '#d29922',
fill: false, pointRadius: 0, borderDash: [2,2], borderWidth: 1
}
]
},
options: {
responsive: true,
plugins: { legend: { labels: { color: '#8b949e', font: { size: 10 }, boxWidth: 12, padding: 8 } } },
scales: {
x: { ticks: { color: '#484f58', font: { size: 10 } }, grid: { color: '#21262d' } },
y: { ticks: { color: '#484f58', callback: v => v + '°C', font: { size: 10 } }, grid: { color: '#21262d' } }
}
}
});
// Rain probability chart
destroyChart('rain');
charts.rain = new Chart(ctx('rainChart'), {
type: 'bar',
data: {
labels: dates,
datasets: [{
label: 'Rain Prob (%)',
data: fc.rain_prob, backgroundColor: fc.rain_prob.map(p => {
if (p > 75) return '#f8514960';
if (p > 50) return '#d2992260';
return '#3fb95060';
}),
borderColor: fc.rain_prob.map(p => {
if (p > 75) return '#f85149';
if (p > 50) return '#d29922';
return '#3fb950';
}),
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
x: { ticks: { color: '#484f58', font: { size: 10 } }, grid: { color: '#21262d' } },
y: { ticks: { color: '#484f58', callback: v => v + '%', max: 100, font: { size: 10 } }, grid: { color: '#21262d' } }
}
}
});
// Wind chart
destroyChart('wind');
charts.wind = new Chart(ctx('windChart'), {
type: 'line',
data: {
labels: dates,
datasets: [
{ label: 'Wind Max', data: fc.wind_max, borderColor: '#7ee787', fill: false, tension: 0.3, pointRadius: 3 },
{ label: 'Gusts Max', data: fc.wind_gust, borderColor: '#d29922', fill: false, tension: 0.3, pointRadius: 3, borderDash: [3,3] }
]
},
options: {
responsive: true,
plugins: { legend: { labels: { color: '#8b949e', font: { size: 10 }, boxWidth: 12, padding: 8 } } },
scales: {
x: { ticks: { color: '#484f58', font: { size: 10 } }, grid: { color: '#21262d' } },
y: { ticks: { color: '#484f58', callback: v => v + ' km/h', font: { size: 10 } }, grid: { color: '#21262d' } }
}
}
});
}
function renderComparison(fc) {
if (!fc || !fc.dates) return;
const tb = document.getElementById('comparison-table');
let html = '<table><tr><th>Date</th><th>Tmax (Open-Meteo)</th><th>Tmax (HKO)</th><th>Δ</th><th>Rain Prob</th><th>Rain (mm)</th></tr>';
for (let i = 0; i < fc.dates.length; i++) {
const om = fc.tmax[i] || '-';
const hk = fc.hko_tmax[i] || '-';
let delta = (om !== '-' && hk !== '-') ? (om - hk).toFixed(1) : '-';
let dClass = '';
if (delta !== '-') {
dClass = Math.abs(parseFloat(delta)) > 2 ? (parseFloat(delta) > 0 ? 'error' : '') : '';
}
html += `<tr>
<td>${fc.dates[i]}</td>
<td>${om}°C</td>
<td>${hk}°C</td>
<td class="${dClass}">${delta !== '-' ? (parseFloat(delta) > 0 ? '+' : '') + delta + '°C' : '-'}</td>
<td>${fc.rain_prob[i] || '-'}%</td>
<td>${fc.rain_sum[i] || '-'}mm</td>
</tr>`;
}
html += '</table>';
tb.innerHTML = html;
}
function renderSignals(signals) {
document.getElementById('signals-loading').style.display = 'none';
const div = document.getElementById('signals-content');
div.style.display = '';
if (!signals || signals.length === 0) {
div.innerHTML = '<div class="stat-sub">No active markets on Polymarket. Showing model-implied probabilities:</div>';
return;
}
let html = '';
for (const s of signals) {
const edgeCls = s.edge_bps > 0 ? 'edge-pos' : 'edge-neg';
const side = s.signal || 'pass';
html += `<div class="signal-row">
<div style="font-weight:600">${s.question}</div>
<div>Model: <b>${s.model_prob}%</b> &nbsp; Market: <b>${s.market_prob}%</b> &nbsp; Edge: <span class="${edgeCls}">${s.edge_bps < 0 ? '' : '+'}${s.edge_bps} bps</span></div>
<div>Side: <span class="signal-side">${side}</span> &nbsp; Size: $${s.size}</div>
</div>`;
}
div.innerHTML = html;
}
function renderKelly(kelly) {
document.getElementById('kelly-loading').style.display = 'none';
const div = document.getElementById('kelly-content');
div.style.display = '';
if (!kelly || kelly.length === 0) {
div.innerHTML = '<div class="stat-sub">No trades recommended</div>';
return;
}
let html = '';
for (const k of kelly) {
const sideCls = k.side === 'buy_yes' ? 'badge-green' : 'badge-danger';
html += `<div class="signal-row">
<div style="font-weight:600">${k.name}</div>
<div>Our: <b>${k.our_prob}%</b> vs Mkt: <b>${k.mkt_prob}%</b></div>
<div><span class="badge ${sideCls}">${k.side}</span> ${k.active ? '$' + k.size.toFixed(2) : 'Pass'} ${k.active ? '<span class="stat-sub" style="margin-left:4px">edge: ' + (k.edge * 100).toFixed(0) + ' bps</span>' : ''}</div>
</div>`;
}
div.innerHTML = html;
}
function renderRaw(fc) {
if (!fc) return;
const div = document.getElementById('raw-data');
div.innerHTML = '<pre style="font-size:11px;color:#8b949e;overflow-x:auto">' + JSON.stringify(fc, null, 2) + '</pre>';
}
refresh();
setInterval(refresh, 300000); // Every 5 min
</script>
</body>
</html>
'''
@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)