Spaces:
Running
Running
Download deep_learning/training/direction_model.py from ifieryarrows/copper-mind: direct link, hf CLI and curl.
- Browser
- Download file 6.46 kB
-
https://huggingface.co/spaces/ifieryarrows/copper-mind/resolve/main/deep_learning/training/direction_model.py
- Command line
-
hf download hf://spaces/ifieryarrows/copper-mind/deep_learning/training/direction_model.py
-
curl -L -o direction_model.py https://huggingface.co/spaces/ifieryarrows/copper-mind/resolve/main/deep_learning/training/direction_model.py
6.46 kB
| """Leakage-safe auxiliary weekly direction calibration. | |
| The TFT quantile path remains responsible for return magnitude and intervals. | |
| This module provides a small, deterministic direction model fitted only on | |
| chronological training origins. It is enabled only when its untouched | |
| validation predictions improve on the TFT direction with a balanced sign rate. | |
| """ | |
| from __future__ import annotations | |
| from typing import Any | |
| import numpy as np | |
| import pandas as pd | |
| # Keep the auxiliary direction model deliberately small and causal. The TFT | |
| # remains responsible for magnitude and interval shape; this model only | |
| # resolves a stable weekly sign mismatch from regime/news context. Selecting | |
| # this fixed family avoids fitting a high-dimensional classifier to a short | |
| # validation window. | |
| WEEKLY_DIRECTION_FEATURE_NAMES = ( | |
| "after_close_news_count", | |
| "days_since_last_material_news", | |
| "event_shock_score", | |
| "futures_curve_slope", | |
| "futures_spread_long", | |
| "material_news_count", | |
| "news_count", | |
| "proxy_vol_price_interaction", | |
| "proxy_vol_spike", | |
| "proxy_vol_zscore", | |
| "regime_high_vol_chop", | |
| "regime_inventory_tightness", | |
| "regime_risk_off_macro", | |
| "regime_risk_on_demand", | |
| "regime_supply_shock", | |
| "regime_usd_pressure", | |
| "sentiment_index", | |
| "stale_sentiment_flag", | |
| ) | |
| def _select_direction_features(feature_names: list[str]) -> list[str]: | |
| """Use the fixed causal family when available; keep custom test inputs usable.""" | |
| selected = [name for name in WEEKLY_DIRECTION_FEATURE_NAMES if name in feature_names] | |
| return selected or list(feature_names) | |
| def _finite_feature_frame(frame: pd.DataFrame, feature_names: list[str]) -> np.ndarray: | |
| missing = [name for name in feature_names if name not in frame.columns] | |
| if missing: | |
| raise ValueError(f"Direction model features missing from frame: {missing}") | |
| values = frame[feature_names].apply(pd.to_numeric, errors="coerce").to_numpy(dtype=np.float64) | |
| values = np.nan_to_num(values, nan=0.0, posinf=0.0, neginf=0.0) | |
| return values | |
| def fit_weekly_direction_model( | |
| frame: pd.DataFrame, | |
| feature_names: list[str], | |
| *, | |
| train_cutoff: int, | |
| horizon: int = 5, | |
| max_encoder_length: int = 50, | |
| target_col: str = "target_5d_log_return", | |
| ) -> dict[str, Any]: | |
| """Fit a small balanced logistic direction model on training origins only.""" | |
| from sklearn.linear_model import LogisticRegression | |
| from sklearn.preprocessing import StandardScaler | |
| times = frame["time_idx"].to_numpy(dtype=np.int64) | |
| target = pd.to_numeric(frame[target_col], errors="coerce").to_numpy(dtype=np.float64) | |
| origin_mask = ( | |
| (times >= int(max_encoder_length) - 1) | |
| & (times <= int(train_cutoff) - int(horizon)) | |
| & np.isfinite(target) | |
| ) | |
| if int(origin_mask.sum()) < 60: | |
| return { | |
| "version": 1, | |
| "enabled": False, | |
| "reason": "insufficient_training_origins", | |
| "fit_split": "train", | |
| "feature_names": list(feature_names), | |
| "horizon": int(horizon), | |
| } | |
| feature_names = _select_direction_features(feature_names) | |
| x = _finite_feature_frame(frame.loc[origin_mask], feature_names) | |
| y = (target[origin_mask] > 0.0).astype(np.int64) | |
| if np.unique(y).size < 2: | |
| return { | |
| "version": 1, | |
| "enabled": False, | |
| "reason": "single_training_sign_class", | |
| "fit_split": "train", | |
| "feature_names": list(feature_names), | |
| "horizon": int(horizon), | |
| } | |
| scaler = StandardScaler() | |
| x_scaled = scaler.fit_transform(x) | |
| model = LogisticRegression( | |
| C=0.03, | |
| class_weight="balanced", | |
| max_iter=1000, | |
| random_state=42, | |
| solver="lbfgs", | |
| ) | |
| model.fit(x_scaled, y) | |
| return { | |
| "version": 1, | |
| "enabled": False, | |
| "reason": "awaiting_validation_selection", | |
| "fit_split": "train", | |
| "feature_names": list(feature_names), | |
| "horizon": int(horizon), | |
| "decision_threshold": 0.50, | |
| "train_origin_count": int(len(y)), | |
| "mean": scaler.mean_.tolist(), | |
| "scale": scaler.scale_.tolist(), | |
| "coef": model.coef_[0].tolist(), | |
| "intercept": float(model.intercept_[0]), | |
| } | |
| def predict_weekly_direction( | |
| calibrator: dict[str, Any], | |
| frame: pd.DataFrame, | |
| *, | |
| start_exclusive: int, | |
| end_inclusive: int, | |
| ) -> np.ndarray: | |
| """Predict validation/test-origin positive probabilities in time order.""" | |
| times = frame["time_idx"].to_numpy(dtype=np.int64) | |
| mask = (times > int(start_exclusive)) & (times <= int(end_inclusive)) | |
| selected = frame.loc[mask].sort_values("time_idx") | |
| if selected.empty: | |
| return np.empty(0, dtype=np.float64) | |
| x = _finite_feature_frame(selected, list(calibrator["feature_names"])) | |
| mean = np.asarray(calibrator["mean"], dtype=np.float64) | |
| scale = np.asarray(calibrator["scale"], dtype=np.float64) | |
| coef = np.asarray(calibrator["coef"], dtype=np.float64) | |
| scale = np.where(np.abs(scale) > 1e-12, scale, 1.0) | |
| logits = ((x - mean) / scale) @ coef + float(calibrator["intercept"]) | |
| return 1.0 / (1.0 + np.exp(-np.clip(logits, -40.0, 40.0))) | |
| def apply_weekly_direction_model( | |
| pred: np.ndarray, | |
| positive_probability: np.ndarray, | |
| *, | |
| threshold: float = 0.50, | |
| horizon: int = 5, | |
| ) -> np.ndarray: | |
| """Align each TFT weekly median sign to the causal direction model. | |
| The adjustment shifts every quantile by the median-path delta, preserving | |
| interval widths and ordering while changing only the forecast location. | |
| """ | |
| arr = np.asarray(pred, dtype=np.float64).copy() | |
| probs = np.asarray(positive_probability, dtype=np.float64).reshape(-1) | |
| if arr.ndim != 3 or arr.shape[1] < horizon: | |
| raise ValueError(f"Expected predictions [n,>={horizon},q], got {arr.shape}") | |
| if len(probs) != len(arr): | |
| raise ValueError(f"Direction probabilities {len(probs)} do not match predictions {len(arr)}") | |
| median_idx = arr.shape[2] // 2 | |
| median_path = arr[:, :horizon, median_idx] | |
| weekly_median = median_path.sum(axis=1) | |
| current_sign = np.where(weekly_median >= 0.0, 1.0, -1.0) | |
| desired_sign = np.where(probs >= float(threshold), 1.0, -1.0) | |
| new_median_path = median_path * (desired_sign * current_sign)[:, None] | |
| arr[:, :horizon, :] += (new_median_path - median_path)[:, :, None] | |
| return arr | |