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:
+72
-1
@@ -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)")
|
||||
|
||||
Reference in New Issue
Block a user