Files
hk-weather-mkt/web_dashboard.py
ramseshk 40f26cf232 Add /simple dashboard (zero JS deps) + Chart.js fallback error handler
- /simple: lightweight HTML dashboard, no external JS, works instantly
  Shows current conditions, tomorrow forecast, ML predictions,
  5-day forecast table, Kelly sizing, typhoon signal
- /: added Chart.js CDN onerror fallback — shows clear message + /simple link
  if CDN is unreachable (common cause of 'everything says loading')
2026-08-11 12:10:39 +08:00

894 lines
34 KiB
Python

#!/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
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
try:
from ml import MLPredictor
_ml_available = True
except Exception:
_ml_available = False
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"
onerror="document.body.innerHTML='<h1 style=color:#58a6ff;text-align:center;padding-top:100px>Chart.js CDN failed to load</h1><p style=color:#8b949e;text-align:center>Try <a href=/simple style=color:#58a6ff>/simple</a> instead</p><p style=color:#8b949e;text-align:center>Or open http://localhost:5000/simple</p>'"></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>
<!-- ML PREDICTIONS -->
<div class="card">
<h3>ML Model Predictions <span class="model-tag">Logistic Regression</span></h3>
<div id="ml-loading" class="loading">Loading...</div>
<div id="ml-content" style="display:none"></div>
</div>
<!-- TYPHOON -->
<div class="card">
<h3>Typhoon Probabilities <span class="model-tag">Climatological</span></h3>
<div id="typhoon-content" class="stat-sub">Loading...</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);
}
// ML data refreshes independently (slower)
fetchML();
}
async function fetchML() {
try {
const resp = await fetch('/api/ml');
const data = await resp.json();
renderML(data);
renderTyphoonML(data.typhoon);
} catch(e) {
document.getElementById('ml-loading').style.display = 'block';
document.getElementById('ml-content').style.display = 'none';
}
}
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>';
}
function renderML(data) {
document.getElementById('ml-loading').style.display = 'none';
const div = document.getElementById('ml-content');
div.style.display = '';
if (data.error) {
div.innerHTML = '<div class="stat-sub">ML models not available (train first: python ml/train.py)</div>';
return;
}
const models = data.models || {};
if (Object.keys(models).length === 0) {
div.innerHTML = '<div class="stat-sub">No model predictions yet</div>';
return;
}
let html = '';
const sorted = Object.entries(models).sort((a, b) => {
const x = Math.abs(a[1].calibrated - 50);
const y = Math.abs(b[1].calibrated - 50);
return y - x;
});
for (const [name, m] of sorted) {
const cls = m.calibrated > 75 ? 'badge-green' : (m.calibrated < 25 ? 'badge-danger' : 'badge-info');
html += `<div class="signal-row">
<div>${m.description} <span class="badge ${cls}">${m.calibrated.toFixed(1)}%</span></div>
<div style="font-size:10px;color:#484f58">raw=${m.raw.toFixed(1)}% · ${m.method}</div>
</div>`;
}
div.innerHTML = html;
}
function renderTyphoonML(typhoon) {
const div = document.getElementById('typhoon-content');
if (!typhoon || Object.keys(typhoon).length === 0) {
div.innerHTML = '<span class="stat-sub">No data</span>';
return;
}
let html = '';
const levels = ['typhoon_T1', 'typhoon_T3', 'typhoon_T8'];
for (const k of levels) {
if (typhoon[k] !== undefined) {
const cls = typhoon[k] > 30 ? 'badge-warn' : (typhoon[k] > 10 ? 'badge-info' : '');
html += `<span class="badge ${cls || ''}" style="margin:2px">${k.replace('typhoon_','')}: ${typhoon[k].toFixed(1)}%</span> `;
}
}
if (typhoon['typhoon_T8_72h'] !== undefined) {
html += `<div style="margin-top:4px;font-size:11px">T8/72h: ${typhoon['typhoon_T8_72h'].toFixed(1)}% · T8/120h: ${(typhoon['typhoon_T8_120h']||0).toFixed(1)}%</div>`;
}
div.innerHTML = html;
}
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("/api/ml")
def api_ml():
"""Return ML model predictions."""
if not _ml_available:
return jsonify({"error": "ML not available"}), 503
try:
predictor = MLPredictor()
predictor.fetch_and_predict()
predictions = predictor._last_predictions or {}
# Per-model raw vs calibrated
models = {}
for target, model in predictor.ensemble.models.items():
if predictor._last_features is not None:
raw = float(model.predict_raw(predictor._last_features[1:2])[0]) if len(predictor._last_features) > 1 else 0.5
cal = float(model.predict_proba(predictor._last_features[1:2])[0]) if len(predictor._last_features) > 1 else 50.0
else:
raw, cal = 0.5, 50.0
models[target] = {
"description": model.target_def["description"],
"raw": round(raw * 100, 1),
"calibrated": round(cal, 1),
"method": model.calibrator.method,
}
# Typhoon
typhoon = {}
if predictor._last_typhoon:
for k in ["typhoon_T1", "typhoon_T3", "typhoon_T8",
"typhoon_T8_72h", "typhoon_T8_120h"]:
if k in predictor._last_typhoon:
typhoon[k] = round(predictor._last_typhoon[k], 1)
# Spatial
spatial = predictor._last_spatial or {}
return jsonify({
"fetch_time": datetime.now().strftime("%H:%M:%S"),
"models": models,
"typhoon": typhoon,
"spatial": spatial,
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/simple")
def simple():
"""Simple unstyled dashboard that always works."""
return render_template_string("""
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>HK Weather — Simple</title>
<style>
body{background:#0d1117;color:#c9d1d9;font:14px monospace;max-width:900px;margin:20px auto;padding:16px}
h1{color:#58a6ff;font-size:18px}
h2{color:#8b949e;font-size:13px;margin-top:16px}
.card{background:#161b22;border:1px solid#30363d;border-radius:6px;padding:12px;margin:8px 0}
.row{display:flex;justify-content:space-between;margin:2px 0}
.label{color:#8b949e}
.val{color:#f0f6fc;font-weight:700}
.green{color:#3fb950}.red{color:#f85149}.yellow{color:#d29922}.gray{color:#8b949e}
.bar{display:inline-block;height:14px;background:#3fb950;border-radius:2px;vertical-align:middle;margin-left:4px}
.err{color:#f85149}
</style></head><body>
<h1>HK Weather Prediction Market</h1>
<div id="ts" class="gray"></div>
<h2>Current Conditions (HKO)</h2>
<div class="card" id="current">Loading...</div>
<h2>Tomorrow Forecast</h2>
<div class="card" id="tomorrow">Loading...</div>
<h2>ML Model Predictions</h2>
<div class="card" id="ml">Loading...</div>
<h2>5-Day Forecast</h2>
<div class="card" id="forecast">Loading...</div>
<h2>Typhoon</h2>
<div class="card" id="typhoon">Loading...</div>
<h2>Kelly Sizing</h2>
<div class="card" id="kelly">Loading...</div>
<script>
async function load(){
try{
let r=await fetch('/api/dashboard');let d=await r.json();
document.getElementById('ts').textContent=d.fetch_time;
let c=d.current||{};
document.getElementById('current').innerHTML=
'<div class=row><span class=label>Temperature</span><span class=val>'+c.temperature+'°C</span></div>'+
'<div class=row><span class=label>Feels Like</span><span class=val>'+c.feels_like+'°C</span></div>'+
'<div class=row><span class=label>Humidity</span><span class=val>'+c.humidity+'%</span></div>'+
'<div class=row><span class=label>Wind</span><span class=val>'+c.wind_speed+' km/h</span></div>'+
'<div class=row><span class=label>Pressure</span><span class=val>'+c.pressure+' hPa</span></div>'+
(c.warning?'<div class=row><span class=label>Warning</span><span class=yellow>'+c.warning.substring(0,80)+'</span></div>':'');
let t=d.tomorrow||{};
document.getElementById('tomorrow').innerHTML=
'<div class=row><span class=label>'+t.date+'</span></div>'+
'<div class=row><span class=label>Tmax</span><span class=val>'+t.temp_max+'°C</span></div>'+
'<div class=row><span class=label>Tmin</span><span class=val>'+t.temp_min+'°C</span></div>'+
'<div class=row><span class=label>Rain Prob</span><span class=val>'+t.rain_prob+'%</span></div>'+
'<div class=row><span class=label>Rain Total</span><span class=val>'+t.rain_sum+'mm</span></div>'+
'<div class=row><span class=label>Wind Max</span><span class=val>'+t.wind_max+' km/h</span></div>'+
'<div class=row><span class=label>Gusts</span><span class=val>'+t.wind_gust+' km/h</span></div>';
let f=d.forecast||{};
let fc='<div style=overflow-x:auto><table style=font-size:12px;width:100%>'+
'<tr style=color:#8b949e><td>Date</td><td>Tmax</td><td>Tmin</td><td>Rain%</td><td>Rain mm</td><td>Wind</td><td>Gust</td></tr>';
for(let i=0;i<(f.dates||[]).length;i++){
fc+='<tr><td>'+f.dates[i]+'</td><td>'+f.tmax[i]+'</td><td>'+f.tmin[i]+'</td>'+
'<td>'+f.rain_prob[i]+'%</td><td>'+f.rain_sum[i]+'</td>'+
'<td>'+f.wind_max[i]+'</td><td>'+f.wind_gust[i]+'</td></tr>';
}
fc+='</table></div>';
document.getElementById('forecast').innerHTML=fc;
let k=d.kelly||[];
let kh='';
for(let i=0;i<k.length;i++){
let cl=k[i].active?(k[i].side=='buy_yes'?'green':'red'):'gray';
kh+='<div class=row><span>'+k[i].name+'</span><span class='+cl+'>'+k[i].side+' $'+k[i].size.toFixed(2)+'</span></div>';
}
document.getElementById('kelly').innerHTML=kh||'No signals';
let ty=d.typhoon||{};
document.getElementById('typhoon').textContent='Signal: T'+ty.signal;
}catch(e){
document.getElementById('current').innerHTML='<span class=err>Error: '+e.message+'</span>';
}
}
load();
// Load ML predictions separately
fetch('/api/ml').then(r=>r.json()).then(d=>{
let models=d.models||{};
let html='';
for(let[name,m] of Object.entries(models)){
let cl=m.calibrated>75?'green':(m.calibrated<25?'red':'yellow');
let w=m.calibrated>50?m.calibrated:100-m.calibrated;
html+='<div class=row style=margin:4px 0><span>'+m.description+'</span>'+
'<span><span class='+cl+'>'+m.calibrated.toFixed(1)+'%</span>'+
'<span class=bar style=width:'+(w*2)+'px></span>'+
'</span></div>';
}
document.getElementById('ml').innerHTML=html||'No ML models loaded';
}).catch(e=>{
document.getElementById('ml').innerHTML='<span class=err>ML unavailable (train first)</span>';
});
</script></body></html>""")
@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)