"""Online Learning - Adaptive model retraining with concept drift detection.""" import numpy as np import pandas as pd from sklearn.linear_model import SGDRegressor from typing import Dict, Optional import warnings warnings.filterwarnings('ignore') class OnlineLearner: """Incremental learning for adaptive alpha models.""" def __init__(self, lookback_window: int = 252): self.lookback_window = lookback_window self.model = SGDRegressor(loss='squared_error', penalty='l2', alpha=0.0001, learning_rate='adaptive', eta0=0.01, max_iter=1, warm_start=True, random_state=42) self.feature_buffer = [] self.target_buffer = [] self.is_fitted = False self.update_count = 0 self.drift_scores = [] def partial_fit(self, X: np.ndarray, y: np.ndarray): """Incrementally update model.""" self.feature_buffer.append(X) self.target_buffer.append(y) if len(self.feature_buffer) > self.lookback_window: self.feature_buffer = self.feature_buffer[-self.lookback_window:] self.target_buffer = self.target_buffer[-self.lookback_window:] X_batch = np.vstack(self.feature_buffer) y_batch = np.concatenate(self.target_buffer) if not self.is_fitted: self.model.fit(X_batch, y_batch); self.is_fitted = True else: self.model.partial_fit(X_batch, y_batch) self.update_count += 1 def predict(self, X: np.ndarray) -> np.ndarray: return self.model.predict(X) if self.is_fitted else np.zeros(len(X)) def get_drift_score(self, recent_X: np.ndarray, recent_y: np.ndarray) -> float: """Detect concept drift.""" if not self.is_fitted: return 0.0 pred = self.predict(recent_X) recent_mse = np.mean((pred - recent_y) ** 2) all_pred = self.predict(np.vstack(self.feature_buffer)) all_y = np.concatenate(self.target_buffer) historical_mse = np.mean((all_pred - all_y) ** 2) drift = recent_mse / (historical_mse + 1e-8) - 1.0 self.drift_scores.append(drift) return drift class AdaptiveEnsemble: """Ensemble adapting weights based on recent IC.""" def __init__(self, models: Dict[str, object], adaptation_rate: float = 0.1): self.models = models self.weights = {name: 1.0 / len(models) for name in models} self.adaptation_rate = adaptation_rate def predict(self, X: np.ndarray) -> np.ndarray: final = np.zeros(len(X)) for name, model in self.models.items(): try: final += self.weights[name] * model.predict(X) except: pass return final def update_weights(self, predictions: Dict[str, np.ndarray], actual: np.ndarray): from scipy.stats import spearmanr ics = {} for name, pred in predictions.items(): ic, _ = spearmanr(pred, actual) ics[name] = abs(ic) if not np.isnan(ic) else 0.0 total = sum(ics.values()) + 1e-8 target = {name: ic / total for name, ic in ics.items()} for name in self.weights: self.weights[name] = (1 - self.adaptation_rate) * self.weights[name] + self.adaptation_rate * target[name] total_w = sum(self.weights.values()) self.weights = {k: v / total_w for k, v in self.weights.items()}