Add spatial features, typhoon model, ERA5 pipeline, portfolio Kelly

Tier 2 enhancements:
- SpatialWeatherClient: multi-station Open-Meteo fetcher for all HK locations
  Extracts urban heat island delta, coastal-inland gradients, wind convergence,
  precipitation spatial heterogeneity, composite instability index
- TyphoonModel: data-driven signal probability for T1/T3/T8/T10
  Climatological base rates + conditional transition probabilities
  Currently active T1 signal → 25% T3/24h, 10% T8/72h, 22% T8/120h
  ENSO modulation, active storm proximity boost, month-specific seasonality
- ERA5 download/process pipeline via CDS API
  Downloads hourly reanalysis for HK region, processes to daily training format
  Output schema matches Open-Meteo for seamless feature compatibility
- PortfolioKelly: correlation-aware simultaneous Kelly sizing
  Covariance matrix from historical outcome correlations
  Prevents over-betting on correlated rain/temp/wind markets
  Σ⁻¹ μ vector formulation, regularized inversion, independent fallback
- MLPredictor updated: integrates spatial + typhoon + portfolio Kelly
  record_outcome feeds both calibration AND portfolio correlation matrix
This commit is contained in:
ramseshk
2026-08-10 17:56:02 +08:00
parent 7d7a67bd20
commit 03f9ea2129
5 changed files with 1219 additions and 1 deletions
+72 -1
View File
@@ -22,10 +22,13 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
from ml.features import FeatureEngine
from ml.model import ModelEnsemble, TARGET_DEFINITIONS
from ml.spatial import SpatialWeatherClient
from ml.typhoon import TyphoonModel
from weather.openmeteo_client import OpenMeteoClient
from weather.hko_client import HKOClient
from strategy.calibrator import ProbabilityCalibrator
from strategy.kelly import KellyCriterion
from strategy.portfolio_kelly import PortfolioKelly
from config import HK_COORDS, MIN_EDGE_BPS, KELLY_FRACTION, MAX_POSITION_USDC
@@ -51,8 +54,11 @@ class MLPredictor:
self.ensemble = ModelEnsemble()
self.calibrator = ProbabilityCalibrator()
self.kelly = KellyCriterion(bankroll_usdc=bankroll_usdc, fraction=kelly_fraction)
self.portfolio_kelly = PortfolioKelly(fraction=kelly_fraction)
self.openmeteo = OpenMeteoClient()
self.hko = HKOClient()
self.spatial = SpatialWeatherClient()
self.typhoon = TyphoonModel()
self.min_edge_bps = min_edge_bps
@@ -61,6 +67,8 @@ class MLPredictor:
self._last_hourly: Optional[pd.DataFrame] = None
self._last_features: Optional[np.ndarray] = None
self._last_predictions: Optional[Dict[str, float]] = None
self._last_spatial: Optional[Dict[str, float]] = None
self._last_typhoon: Optional[Dict[str, float]] = None
self._ensemble_spread: Optional[Dict[str, float]] = None
# Load trained models
@@ -136,11 +144,51 @@ class MLPredictor:
predictions[target] = prob
self._last_predictions = predictions
# Add typhoon predictions (not from LightGBM — separate model)
self._last_typhoon = self._predict_typhoon()
predictions.update(self._last_typhoon)
# Add spatial features
self._last_spatial = self._compute_spatial_features()
return predictions
# Fallback: use heuristic predictions
return self._fallback_predictions()
def _predict_typhoon(self) -> Dict[str, float]:
"""Generate typhoon signal-level probabilities."""
hko = self.hko
current_signal = hko.get_current_signal_level()
typhoon_info = hko.get_typhoon_info()
month = datetime.now().month
probs = {}
for signal_level in ["T1", "T3", "T8"]:
for lead_hours in [24, 48, 72, 120]:
target = f"typhoon_{signal_level}_{lead_hours}h"
prob = self.typhoon.signal_probability(
signal_level=signal_level,
lead_hours=lead_hours,
current_signal=current_signal,
current_conditions=typhoon_info,
month=month,
)
if lead_hours == 24: # Store short key too
probs[f"typhoon_{signal_level}"] = prob
probs[target] = prob
return probs
def _compute_spatial_features(self) -> Dict[str, float]:
"""Extract spatial features from multi-station forecasts."""
station_data = self.spatial.fetch_all_stations(lead_days=5)
if not station_data:
return {}
return self.spatial.extract_spatial_features(station_data, day_index=1)
def _fallback_predictions(self) -> Dict[str, float]:
"""Fallback heuristic predictions when no ML models loaded."""
if self._last_forecast is None or len(self._last_forecast) == 0:
@@ -321,13 +369,14 @@ class MLPredictor:
}
def record_outcome(self, target: str, predicted_prob: float, actual: bool):
"""Record resolved market outcome for calibration."""
"""Record resolved market outcome for calibration and correlation."""
self.calibrator.record_outcome(
date=datetime.now().strftime("%Y-%m-%d"),
variable=target,
predicted_probability=predicted_prob,
actual_outcome=actual,
)
self.portfolio_kelly.record_outcomes({target: actual})
def get_top_signals(
self, markets: List[Dict], default_market_prob: float = 50.0
@@ -376,6 +425,28 @@ class MLPredictor:
prob = predictions[target]
lines.append(f" {tdef['description']}: {prob:.1f}%")
if self._last_typhoon:
lines.append(f"\n Typhoon probabilities:")
for level in ["T1", "T3", "T8"]:
key = f"typhoon_{level}"
if key in self._last_typhoon:
lines.append(f" {level}: {self._last_typhoon[key]:.1f}%")
for h in [48, 72, 120]:
for level in ["T1", "T8"]:
key = f"typhoon_{level}_{h}h"
if key in self._last_typhoon:
lines.append(f" {level} in {h}h: {self._last_typhoon[key]:.1f}%")
if self._last_spatial:
uhi = self._last_spatial.get("uhi_tmax_delta", 0)
instability = self._last_spatial.get("spatial_instability", 0)
if abs(uhi) > 0.5 or instability > 0.2:
lines.append(f"\n Spatial features:")
if abs(uhi) > 0.5:
lines.append(f" UHI delta: {uhi:+.1f}°C")
if instability > 0.2:
lines.append(f" Instability: {instability:.2f}")
spread = self._ensemble_spread or {}
if spread.get("composite_spread", 0) > 0.1:
lines.append(f"\n Ensemble disagreement: {spread['composite_spread']:.2f} (amplified edge)")