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:
+141
-4
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user