""" 特征工程:因子 → 特征矩阵 + 目标标签。 严禁使用未来数据。所有变换基于 expanding window 或训练集统计。 """ import numpy as np import pandas as pd from sklearn.preprocessing import RobustScaler class FeatureEngine: """ 特征工程引擎。 参数: lookahead: 预测未来 N 个交易日 label_type: 'regression' | 'classification' winsorize_pct: 去极值的分位数边界 (0.01, 0.99) nan_threshold: NaN 占比超过此值的因子直接剔除 """ def __init__( self, lookahead: int = 5, label_type: str = "regression", winsorize_pct: tuple[float, float] = (0.01, 0.99), nan_threshold: float = 0.3, ): self.lookahead = lookahead self.label_type = label_type self.winsorize_pct = winsorize_pct self.nan_threshold = nan_threshold self._scaler = RobustScaler() self._scaler_fitted = False self._valid_features: list[str] = [] # ── 标签构建 ────────────────────────────────────────── def build_labels(self, price_df: pd.DataFrame) -> pd.Series: """ 构建目标标签。 regression: (close_{t+N} - close_t) / close_t * 100 classification: 1 if return > 0 else 0 """ close = price_df["close"] future = close.shift(-self.lookahead) ret = (future - close) / close * 100 if self.label_type == "classification": return (ret > 0).astype(int) return ret.rename(f"y_fwd_{self.lookahead}") # ── 特征构建 ────────────────────────────────────────── def build( self, factor_df: pd.DataFrame, price_df: pd.DataFrame, fit: bool = True, ) -> tuple[pd.DataFrame, pd.Series]: """ 构建特征矩阵 X 和标签 y。 参数: factor_df: 因子 DataFrame, index=trade_date, columns=因子名 price_df: 价格 DataFrame, 需有 'close' fit: True=训练模式(fit scaler + 记录有效特征),False=预测模式 返回: X, y(y 在 predict 模式下为 None) """ X = factor_df.copy() # 1. 剔除 NaN 率过高的列 if fit: nan_ratio = X.isna().mean() self._valid_features = list(nan_ratio[nan_ratio <= self.nan_threshold].index) # 排除非因子列 self._valid_features = [c for c in self._valid_features if c not in ("close", "open", "high", "low", "volume")] X = X[self._valid_features].copy() if self._valid_features else X # 2. 缺失值填充:前值填充 → 截面中位数 X = X.ffill().fillna(X.median()) # 3. 去极值(Winsorize) if fit: lo, hi = self.winsorize_pct self._winsor_lower = X.quantile(lo) self._winsor_upper = X.quantile(hi) for col in X.columns: if col in getattr(self, "_winsor_lower", pd.Series()): X[col] = X[col].clip(self._winsor_lower[col], self._winsor_upper[col]) # 4. 标准化(训练时 fit,预测时 transform) if fit: X_scaled = self._scaler.fit_transform(X) self._scaler_fitted = True else: X_scaled = self._scaler.transform(X) X = pd.DataFrame(X_scaled, index=X.index, columns=X.columns) # 5. 构建标签 y = self.build_labels(price_df) if fit else None # 6. 对齐(删掉无法构建标签的行) if fit: valid_idx = X.index.intersection(y.dropna().index) X = X.loc[valid_idx] y = y.loc[valid_idx] return X, y # ── 多股票构建 ──────────────────────────────────────── def build_universe( self, factor_universe: dict[str, pd.DataFrame], price_universe: dict[str, pd.DataFrame], ) -> tuple[pd.DataFrame, pd.Series]: """多股票拼接特征矩阵(每只股票独立处理再拼接)。""" X_parts, y_parts = [], [] for ts_code in factor_universe: f_df = factor_universe[ts_code] p_df = price_universe.get(ts_code) if p_df is None or f_df.empty or p_df.empty: continue X, y = self.build(f_df, p_df, fit=True) if X.empty: continue X["_ts_code"] = ts_code X_parts.append(X) y_parts.append(y) if not X_parts: return pd.DataFrame(), pd.Series() X_all = pd.concat(X_parts) y_all = pd.concat(y_parts) return X_all.drop(columns=["_ts_code"]), y_all