Add ML dashboard panel, paper trader, time decay model

- /api/ml endpoint: per-model raw vs calibrated, typhoon, spatial features
- ML Predictions card in web dashboard (color-coded, sorted by confidence)
- Typhoon Probabilities card (T1/T3/T8 now/72h/120h)
- PaperTrader: simulated trading with portfolio Kelly, P&L tracking,
  trade history persistence, auto-resolution after 24h
- TimeDecayModel: theta decay for binary options
  sigma(t) = sigma_0 * (T-t)^beta (beta=0.4 for weather)
  Fair price convergence from 50%→model_prob as expiry approaches
- Paper trader CLI: --track (monitor), --report, --simulate-days

Run dashboard: python web_dashboard.py  # see ML panel
Run paper:  python ml/paper_trader.py --simulate-days 30
This commit is contained in:
ramseshk
2026-08-11 11:15:43 +08:00
parent f8dde42007
commit aad4ee8cf6
4 changed files with 732 additions and 4 deletions
+141 -4
View File
@@ -26,6 +26,15 @@ 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'''
@@ -193,6 +202,19 @@ HTML_TEMPLATE = r'''
<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>
@@ -240,10 +262,25 @@ function renderAll(data) {
renderTomorrow(data.tomorrow);
renderCharts(data.forecast);
renderComparison(data.forecast);
renderSignals(data.signals);
renderKelly(data.kelly);
renderRaw(data.forecast);
renderTyphoon(data.typhoon);
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) {
@@ -467,6 +504,59 @@ function renderRaw(fc) {
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>
@@ -629,6 +719,53 @@ def api_dashboard():
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("/")
def index():
return render_template_string(HTML_TEMPLATE)