# -*- coding: utf-8 -*- """ Kronos-Enhanced FT-Transformer - 特征工程模块 ============================================= 从原始K线数据中提取45维连续特征 + 5维类别特征 融合量价理论、波浪理论、倍量理论三大理论的特征表达 """ import numpy as np import pandas as pd from collections import OrderedDict import warnings warnings.filterwarnings('ignore') class FeatureEngineer: """ 特征工程引擎 ============= 将原始OHLCV数据转化为FT-Transformer输入的特征张量 输出特征清单(共50维): 连续特征(45维): [0-6] 价格特征: open, high, low, close, change%, amplitude, midpoint [7-11] 均线系统: EMA5/10/20/60 偏离率 + EMA排列状态值 [12-17] 技术指标: RSI6, RSI14, KDJ_K, KDJ_D, KDJ_J, CCI20 [18-21] MACD系列: DIF, DEA, MACD柱, 信号方向值 [22-24] 布林带: 上轨偏离, 下轨偏离, 带宽 [25-28] 成交量特征: 量比, 5日均量比, 10日均量比, 量能状态值 [29-31] 量价特征: 量价相关系数, 量价背离值, 量价状态值 [32-34] 波浪特征: 浪型位置编码, 浪型强度, 斐波那契位置 [35-38] DMI系列: +DI, -DI, ADX, SAR偏离 [39-40] OBV/MFI: OBV变化率, MFI值 [41-44] 时间/统计特征: 日收益率, 5日收益率, 20日波动率, 换手率 类别特征(5维): [0] 市场状态: 牛市/熊市/震荡/反弹(8类) [1] 趋势方向: 上升/下降/横盘/转折(4类) [2] 倍量状态: 无倍量/倍量突破/倍量确认/缩量(4类,但实际编码为3) [3] 波浪阶段: 驱动浪1-5/调整浪ABC(6类) [4] 月份编码: 1-12月周期特征(12类) """ CONTINUOUS_DIM = 45 CATEGORICAL_DIM = 5 CAT_CARDINALITIES = [8, 4, 3, 6, 12] def __init__(self, sequence_length=60): self.seq_len = sequence_length self._feature_names = self._build_feature_names() def _build_feature_names(self): """构建特征名称列表(用于可解释性)""" names = OrderedDict() # 价格特征 for i, n in enumerate(['open', 'high', 'low', 'close', 'change_pct', 'amplitude', 'midpoint']): names['price_{}'.format(n)] = i # 均线偏离 for i, n in enumerate(['ema5', 'ema10', 'ema20', 'ema60', 'ema_alignment']): names['ma_{}'.format(n)] = 7 + i # 技术指标 for i, n in enumerate(['rsi6', 'rsi14', 'kdj_k', 'kdj_d', 'kdj_j', 'cci']): names['indicator_{}'.format(n)] = 12 + i # MACD for i, n in enumerate(['dif', 'dea', 'histogram', 'signal']): names['macd_{}'.format(n)] = 18 + i # 布林带 for i, n in enumerate(['upper_dev', 'lower_dev', 'bandwidth']): names['boll_{}'.format(n)] = 22 + i # 成交量 for i, n in enumerate(['vol_ratio', 'vol_ma5_ratio', 'vol_ma10_ratio', 'vol_state']): names['volume_{}'.format(n)] = 25 + i # 量价特征 for i, n in enumerate(['price_vol_corr', 'divergence', 'vp_state']): names['vp_{}'.format(n)] = 29 + i # 波浪特征 for i, n in enumerate(['wave_pos', 'wave_strength', 'fib_pos']): names['wave_{}'.format(n)] = 32 + i # DMI/SAR for i, n in enumerate(['plus_di', 'minus_di', 'adx', 'sar_dev']): names['dmi_{}'.format(n)] = 35 + i # OBV/MFI for i, n in enumerate(['obv_change', 'mfi']): names['flow_{}'.format(n)] = 39 + i # 时间统计 for i, n in enumerate(['daily_ret', 'ret_5d', 'volatility_20d', 'turnover']): names['stat_{}'.format(n)] = 41 + i return names def extract_features(self, df): """ 从DataFrame提取全部特征 参数: df: DataFrame, 必须包含列 date,open,high,low,close,volume,turnover 返回: continuous: ndarray (T, 45) 连续特征 categorical: ndarray (T, 5) 类别特征(整数编码) labels: ndarray (T, 3) 标签 [趋势方向, 价格变化率, 波动率] """ df = df.copy().reset_index(drop=True) df = df.sort_values('date').reset_index(drop=True) # ===== 预计算基础指标 ===== self._compute_indicators(df) T = len(df) continuous = np.zeros((T, self.CONTINUOUS_DIM), dtype=np.float32) categorical = np.zeros((T, self.CATEGORICAL_DIM), dtype=np.int32) for t in range(T): if t < 20: # 前20个交易日数据不足,跳过 continue continuous[t] = self._extract_continuous(df, t) categorical[t] = self._extract_categorical(df, t) # 归一化连续特征(Z-Score) continuous = self._normalize(continuous) # 生成标签 labels = self._generate_labels(df) return continuous, categorical, labels def _compute_indicators(self, df): """预计算所有技术指标""" close = df['close'] high = df['high'] low = df['low'] volume = df['volume'] # 基础价格 df['change_pct'] = close.pct_change() df['amplitude'] = (high - low) / close.shift(1) df['midpoint'] = (high + low) / 2 # EMA均线系统 for period in [5, 10, 20, 60]: df['ema_{}'.format(period)] = close.ewm(span=period, adjust=False).mean() df['ema5_dev'] = (close - df['ema_5']) / df['ema_5'] df['ema10_dev'] = (close - df['ema_10']) / df['ema_10'] df['ema20_dev'] = (close - df['ema_20']) / df['ema_20'] df['ema60_dev'] = (close - df['ema_60']) / df['ema_60'] # EMA排列状态: 多头排列=2, 空头排列=0, 交叉=1 df['ema_alignment'] = np.where( (df['ema_5'] > df['ema_10']) & (df['ema_10'] > df['ema_20']) & (df['ema_20'] > df['ema_60']), 2.0, np.where( (df['ema_5'] < df['ema_10']) & (df['ema_10'] < df['ema_20']) & (df['ema_20'] < df['ema_60']), 0.0, 1.0)) # RSI delta = close.diff() gain = delta.where(delta > 0, 0).rolling(14).mean() loss = (-delta.where(delta < 0, 0)).rolling(14).mean() rs = gain / (loss + 1e-10) df['rsi14'] = 100 - 100 / (1 + rs) gain6 = delta.where(delta > 0, 0).rolling(6).mean() loss6 = (-delta.where(delta < 0, 0)).rolling(6).mean() rs6 = gain6 / (loss6 + 1e-10) df['rsi6'] = 100 - 100 / (1 + rs6) # KDJ(3,3,3) low_list = low.rolling(9, min_periods=1).min() high_list = high.rolling(9, min_periods=1).max() rsv = (close - low_list) / (high_list - low_list + 1e-10) * 100 df['kdj_k'] = rsv.ewm(com=2, adjust=False).mean() df['kdj_d'] = df['kdj_k'].ewm(com=2, adjust=False).mean() df['kdj_j'] = 3 * df['kdj_k'] - 2 * df['kdj_d'] # CCI(20) tp = (high + low + close) / 3 ma_tp = tp.rolling(20).mean() md_tp = tp.rolling(20).apply(lambda x: np.abs(x - x.mean()).mean(), raw=True) df['cci'] = (tp - ma_tp) / (0.015 * md_tp + 1e-10) # MACD(5,10,3) ema5 = close.ewm(span=5, adjust=False).mean() ema10 = close.ewm(span=10, adjust=False).mean() df['dif'] = ema5 - ema10 df['dea'] = df['dif'].ewm(span=3, adjust=False).mean() df['macd_hist'] = 2 * (df['dif'] - df['dea']) df['macd_signal'] = np.where(df['dif'] > df['dea'], 1.0, np.where(df['dif'] < df['dea'], -1.0, 0.0)) # 布林带(20,2) ma20 = close.rolling(20).mean() std20 = close.rolling(20).std() df['boll_upper'] = ma20 + 2 * std20 df['boll_lower'] = ma20 - 2 * std20 df['boll_upper_dev'] = (close - df['boll_upper']) / (std20 + 1e-10) df['boll_lower_dev'] = (close - df['boll_lower']) / (std20 + 1e-10) df['boll_bandwidth'] = (4 * std20) / (ma20 + 1e-10) # 成交量特征 vol_ma5 = volume.rolling(5).mean() vol_ma10 = volume.rolling(10).mean() df['vol_ratio'] = volume / (vol_ma5 + 1e-10) df['vol_ma5_ratio'] = volume / (vol_ma5 + 1e-10) df['vol_ma10_ratio'] = volume / (vol_ma10 + 1e-10) # 量能状态: 放量>1.5=2, 正常0.8-1.5=1, 缩量<0.8=0 df['vol_state'] = np.where(df['vol_ratio'] > 1.5, 2.0, np.where(df['vol_ratio'] < 0.8, 0.0, 1.0)) # 量价相关系数 df['vp_corr'] = close.pct_change().rolling(10).corr(volume.pct_change()) # 量价背离: 价格涨量缩=正背离(看空), 价格涨量增=同步(看多) price_up = (df['change_pct'] > 0).astype(float) vol_down = (df['vol_ratio'] < 1.0).astype(float) df['divergence'] = price_up * vol_down - (~price_up.astype(bool)).astype(float) * (~vol_down.astype(bool)).astype(float) # 量价状态 df['vp_state'] = np.where( (df['change_pct'] > 0) & (df['vol_ratio'] > 1.0), 2.0, # 量价齐升 np.where((df['change_pct'] > 0) & (df['vol_ratio'] < 0.8), -1.0, # 顶背离 np.where((df['change_pct'] < 0) & (df['vol_ratio'] > 1.5), -2.0, # 放量下跌 np.where((df['change_pct'] < 0) & (df['vol_ratio'] < 0.8), 1.0, 0.0)))) # 缩量下跌(看多) # 波浪特征(简化版:基于EMA斜率推断) ema20_slope = df['ema_20'].diff(5) df['wave_pos'] = np.clip(ema20_slope / (close.rolling(20).std() + 1e-10), -3, 3) df['wave_strength'] = np.abs(df['wave_pos']) # 斐波那契位置(相对60日高低点) high60 = high.rolling(60).max() low60 = low.rolling(60).min() df['fib_pos'] = (close - low60) / (high60 - low60 + 1e-10) # DMI(14,6) plus_dm = high.diff() minus_dm = -low.diff() plus_dm = np.where((plus_dm > minus_dm) & (plus_dm > 0), plus_dm, 0) minus_dm = np.where((minus_dm > plus_dm) & (minus_dm > 0), minus_dm, 0) tr = pd.concat([high - low, np.abs(high - close.shift(1)), np.abs(low - close.shift(1))], axis=1).max(axis=1) atr14 = tr.rolling(14).mean() df['plus_di'] = 100 * pd.Series(plus_dm, index=df.index).rolling(14).mean() / (atr14 + 1e-10) df['minus_di'] = 100 * pd.Series(minus_dm, index=df.index).rolling(14).mean() / (atr14 + 1e-10) dx = 100 * np.abs(df['plus_di'] - df['minus_di']) / (df['plus_di'] + df['minus_di'] + 1e-10) df['adx'] = dx.rolling(6).mean() # SAR(0.02, 0.2) - 简化 df['sar'] = self._compute_sar(high, low, close) df['sar_dev'] = (close - df['sar']) / (close + 1e-10) # OBV obv = (np.sign(df['change_pct']) * volume).cumsum() df['obv_change'] = obv.diff(5) / (obv.rolling(20).std() + 1e-10) # MFI(14) typical_price = (high + low + close) / 3 raw_money_flow = typical_price * volume pos_flow = np.where(typical_price > typical_price.shift(1), raw_money_flow, 0) neg_flow = np.where(typical_price < typical_price.shift(1), raw_money_flow, 0) pos_mf = pd.Series(pos_flow).rolling(14).sum() neg_mf = pd.Series(neg_flow).rolling(14).sum() df['mfi'] = 100 - 100 / (1 + pos_mf / (neg_mf + 1e-10)) # 统计特征 df['daily_ret'] = df['change_pct'] df['ret_5d'] = close.pct_change(5) df['volatility_20d'] = close.pct_change().rolling(20).std() * np.sqrt(252) df['turnover'] = df.get('turnover', pd.Series(0, index=df.index)) def _compute_sar(self, high, low, close, af_start=0.02, af_max=0.2): """简化SAR计算""" n = len(close) sar = np.zeros(n) af = af_start trend = 1 # 1=多, -1=空 ep = low.iloc[0] sar[0] = high.iloc[0] for i in range(1, n): if trend == 1: sar[i] = sar[i-1] + af * (ep - sar[i-1]) if low.iloc[i] < sar[i]: trend = -1 sar[i] = ep ep = low.iloc[i] af = af_start else: if high.iloc[i] > ep: ep = high.iloc[i] af = min(af + af_start, af_max) else: sar[i] = sar[i-1] + af * (ep - sar[i-1]) if high.iloc[i] > sar[i]: trend = 1 sar[i] = ep ep = high.iloc[i] af = af_start else: if low.iloc[i] < ep: ep = low.iloc[i] af = min(af + af_start, af_max) return pd.Series(sar, index=close.index) def _extract_continuous(self, df, t): """提取第t行的45维连续特征""" features = np.zeros(self.CONTINUOUS_DIM, dtype=np.float32) row = df.iloc[t] # [0-6] 价格特征 features[0] = row['open'] features[1] = row['high'] features[2] = row['low'] features[3] = row['close'] features[4] = row.get('change_pct', 0) or 0 features[5] = row.get('amplitude', 0) or 0 features[6] = row.get('midpoint', row['close']) # [7-11] 均线偏离 features[7] = row.get('ema5_dev', 0) or 0 features[8] = row.get('ema10_dev', 0) or 0 features[9] = row.get('ema20_dev', 0) or 0 features[10] = row.get('ema60_dev', 0) or 0 features[11] = row.get('ema_alignment', 1) or 1 # [12-17] 技术指标 features[12] = row.get('rsi6', 50) or 50 features[13] = row.get('rsi14', 50) or 50 features[14] = row.get('kdj_k', 50) or 50 features[15] = row.get('kdj_d', 50) or 50 features[16] = row.get('kdj_j', 50) or 50 features[17] = row.get('cci', 0) or 0 # [18-21] MACD features[18] = row.get('dif', 0) or 0 features[19] = row.get('dea', 0) or 0 features[20] = row.get('macd_hist', 0) or 0 features[21] = row.get('macd_signal', 0) or 0 # [22-24] 布林带 features[22] = row.get('boll_upper_dev', 0) or 0 features[23] = row.get('boll_lower_dev', 0) or 0 features[24] = row.get('boll_bandwidth', 0) or 0 # [25-28] 成交量 features[25] = row.get('vol_ratio', 1) or 1 features[26] = row.get('vol_ma5_ratio', 1) or 1 features[27] = row.get('vol_ma10_ratio', 1) or 1 features[28] = row.get('vol_state', 1) or 1 # [29-31] 量价特征 features[29] = row.get('vp_corr', 0) or 0 features[30] = row.get('divergence', 0) or 0 features[31] = row.get('vp_state', 0) or 0 # [32-34] 波浪特征 features[32] = row.get('wave_pos', 0) or 0 features[33] = row.get('wave_strength', 0) or 0 features[34] = row.get('fib_pos', 0.5) or 0.5 # [35-38] DMI/SAR features[35] = row.get('plus_di', 20) or 20 features[36] = row.get('minus_di', 20) or 20 features[37] = row.get('adx', 20) or 20 features[38] = row.get('sar_dev', 0) or 0 # [39-40] OBV/MFI features[39] = row.get('obv_change', 0) or 0 features[40] = row.get('mfi', 50) or 50 # [41-44] 时间统计 features[41] = row.get('daily_ret', 0) or 0 features[42] = row.get('ret_5d', 0) or 0 features[43] = row.get('volatility_20d', 0) or 0 features[44] = row.get('turnover', 0) or 0 return features def _extract_categorical(self, df, t): """提取第t行的5维类别特征""" cats = np.zeros(self.CATEGORICAL_DIM, dtype=np.int32) row = df.iloc[t] # [0] 市场状态(8类): 基于EMA60趋势+波动率 ema60_ret = row.get('ema60_dev', 0) or 0 vol = row.get('volatility_20d', 0.3) or 0.3 if ema60_ret > 0.1 and vol < 0.3: cats[0] = 0 # 强牛市 elif ema60_ret > 0.05 and vol < 0.4: cats[0] = 1 # 牛市 elif ema60_ret > 0 and vol < 0.5: cats[0] = 2 # 弱牛市 elif ema60_ret > -0.05 and ema60_ret <= 0 and vol < 0.3: cats[0] = 3 # 震荡偏多 elif ema60_ret >= -0.05 and vol < 0.3: cats[0] = 4 # 震荡 elif ema60_ret < 0 and vol < 0.3: cats[0] = 5 # 震荡偏空 elif ema60_ret < -0.05: cats[0] = 6 # 熊市 elif ema60_ret < -0.1 and vol > 0.4: cats[0] = 7 # 强熊市/崩盘 else: cats[0] = 4 # [1] 趋势方向(4类) macd_signal = row.get('macd_signal', 0) or 0 adx = row.get('adx', 20) or 20 if adx > 25 and macd_signal > 0: cats[1] = 0 # 明确上升 elif adx > 25 and macd_signal < 0: cats[1] = 1 # 明确下降 elif adx <= 25 and macd_signal >= 0: cats[1] = 2 # 横盘偏多 else: cats[1] = 3 # 横盘偏空 # [2] 倍量状态(3类) vol_ratio = row.get('vol_ratio', 1) or 1 if vol_ratio >= 1.8: cats[2] = 0 # 倍量突破 elif vol_ratio >= 1.3: cats[2] = 1 # 温和放量 else: cats[2] = 2 # 正常/缩量 # [3] 波浪阶段(6类): 1浪/2浪/3浪/4浪/5浪/调整 wave_pos = row.get('wave_pos', 0) or 0 fib_pos = row.get('fib_pos', 0.5) or 0.5 if wave_pos > 0 and fib_pos < 0.38: cats[3] = 0 # 1浪起始 elif wave_pos < 0 and fib_pos > 0.2: cats[3] = 1 # 2浪回调 elif wave_pos > 1 and fib_pos < 0.62: cats[3] = 2 # 3浪主升 elif wave_pos < 0 and fib_pos > 0.5: cats[3] = 3 # 4浪调整 elif wave_pos > 0.5 and fib_pos > 0.62: cats[3] = 4 # 5浪尾升 else: cats[3] = 5 # ABC调整 # [4] 月份编码(12类) if 'date' in df.columns: try: cats[4] = pd.to_datetime(row['date']).month - 1 except: cats[4] = 0 else: cats[4] = 0 return cats def _normalize(self, features): """Z-Score归一化(逐特征)""" mean = np.nanmean(features, axis=0, keepdims=True) std = np.nanstd(features, axis=0, keepdims=True) std = np.where(std < 1e-8, 1.0, std) normalized = (features - mean) / std # 处理NaN normalized = np.nan_to_num(normalized, nan=0.0, posinf=3.0, neginf=-3.0) return normalized def _generate_labels(self, df, pred_days=60): """ 生成训练标签 [0] 趋势方向: +1(涨) / 0(平) / -1(跌) [1] 未来N天价格变化率 [2] 未来N天波动率 """ T = len(df) labels = np.zeros((T, 3), dtype=np.float32) close = df['close'].values for t in range(T - pred_days): future_close = close[t+1:t+1+pred_days] future_return = (future_close[-1] - close[t]) / close[t] future_vol = np.std(np.diff(future_close) / future_close[:-1]) * np.sqrt(252) if len(future_close) > 1 else 0 labels[t, 0] = 1.0 if future_return > 0.05 else (-1.0 if future_return < -0.05 else 0.0) labels[t, 1] = future_return labels[t, 2] = future_vol return labels def create_sequences(self, continuous, categorical, labels, seq_len=None): """ 创建滑动窗口序列 返回: X_cont: (N, seq_len, 45) X_cat: (N, seq_len, 5) y: (N, 3) """ if seq_len is None: seq_len = self.seq_len n_samples = len(continuous) - seq_len - 60 # 减去预测期 X_cont = np.zeros((n_samples, seq_len, self.CONTINUOUS_DIM), dtype=np.float32) X_cat = np.zeros((n_samples, seq_len, self.CATEGORICAL_DIM), dtype=np.int32) y = np.zeros((n_samples, 3), dtype=np.float32) for i in range(n_samples): X_cont[i] = continuous[i:i+seq_len] X_cat[i] = categorical[i:i+seq_len] y[i] = labels[i+seq_len-1] # 过滤无效样本 valid = ~np.any(np.isnan(y), axis=1) return X_cont[valid], X_cat[valid], y[valid] def get_feature_info(self): """返回特征信息字典(用于报告)""" return { 'continuous_dim': self.CONTINUOUS_DIM, 'categorical_dim': self.CATEGORICAL_DIM, 'cat_cardinalities': self.CAT_CARDINALITIES, 'feature_names': list(self._feature_names.keys()), 'total_dim': self.CONTINUOUS_DIM + self.CATEGORICAL_DIM, } class DataFetcher: """数据获取器 - 使用腾讯财经HTTP API获取A股历史数据(无状态/无连接/快速)""" @staticmethod def _code_to_tencent(stock_code): """将股票代码格式(sh.600036)转换为腾讯财经格式(sh600036)""" return stock_code.replace('.', '') @staticmethod def fetch_stock_data(stock_code, start_date='2010-01-01', end_date='2025-12-31'): """ 从腾讯财经HTTP API获取股票历史日K数据 参数: stock_code: str, 如 'sh.600036' 或 'sz.000001' start_date: str, 起始日期 end_date: str, 截止日期 返回: DataFrame with columns: date, open, high, low, close, volume, turnover """ import urllib.request import json tencent_code = DataFetcher._code_to_tencent(stock_code) # 腾讯财经日K线API - 前复权 # 腾讯API不支持日期范围参数,获取全部后程序端过滤 url = f"http://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param={tencent_code},day,,,1000,qfq" try: req = urllib.request.Request(url, headers={ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }) with urllib.request.urlopen(req, timeout=10) as resp: result = json.loads(resp.read().decode('utf-8')) except Exception as e: print(f"[ERROR] 腾讯财经API请求失败: {e}") return None # 解析返回数据 try: data = result.get('data', {}) stock_data = data.get(tencent_code, {}) day_data = stock_data.get('qfqday') or stock_data.get('day', []) qt_data = data.get(tencent_code, {}).get('qt', {}) if not day_data: print(f"[WARN] 腾讯财经未返回K线数据: {stock_code}") return None # 腾讯qfqday格式: [date, open, close, high, low, volume(手)] data_rows = [] for item in day_data: if item[0] and float(item[2]) > 0: vol = float(item[5]) if len(item) > 5 and item[5] else 0 data_rows.append({ 'date': item[0], 'open': float(item[1]) if item[1] else 0, 'high': float(item[3]) if len(item) > 3 and item[3] else 0, 'low': float(item[4]) if len(item) > 4 and item[4] else 0, 'close': float(item[2]) if item[2] else 0, 'volume': vol * 100, # 手转股 'turnover': 0, }) if not data_rows: print(f"[WARN] 过滤后无有效数据: {stock_code}") return None df = pd.DataFrame(data_rows) df = df[df['close'] > 0].reset_index(drop=True) # 程序端日期过滤 df['date'] = pd.to_datetime(df['date']) if start_date: df = df[df['date'] >= pd.Timestamp(start_date)] if end_date: df = df[df['date'] <= pd.Timestamp(end_date)] df = df.reset_index(drop=True) df['turnover'] = df['turnover'].fillna(0) print(f"[INFO] 腾讯财经获取 {stock_code} 数据: {len(df)} 条 ({df['date'].iloc[0]} ~ {df['date'].iloc[-1]})") return df except Exception as e: print(f"[ERROR] 解析腾讯财经数据失败: {e}") return None if __name__ == '__main__': # 快速测试 print("=" * 60) print("特征工程模块测试") print("=" * 60) fe = FeatureEngineer(sequence_length=30) info = fe.get_feature_info() print("\n特征维度: 连续={}, 类别={}, 总计={}".format( info['continuous_dim'], info['categorical_dim'], info['total_dim'])) print("类别基数: {}".format(info['cat_cardinalities'])) print("特征名称数: {}".format(len(info['feature_names']))) # 生成模拟数据测试 np.random.seed(42) T = 300 dates = pd.date_range('2020-01-01', periods=T, freq='B') close = 10 + np.cumsum(np.random.randn(T) * 0.1) mock_df = pd.DataFrame({ 'date': dates.strftime('%Y-%m-%d'), 'open': close + np.random.randn(T) * 0.05, 'high': close + np.abs(np.random.randn(T) * 0.1), 'low': close - np.abs(np.random.randn(T) * 0.1), 'close': close, 'volume': np.random.randint(1000000, 5000000, T).astype(float), 'turnover': np.random.uniform(0.5, 5.0, T), }) cont, cat, labels = fe.extract_features(mock_df) print("\n连续特征shape: {}".format(cont.shape)) print("类别特征shape: {}".format(cat.shape)) print("标签shape: {}".format(labels.shape)) X_c, X_cat, y = fe.create_sequences(cont, cat, labels) print("\n序列化后:") print(" X_continuous: {}".format(X_c.shape)) print(" X_categorical: {}".format(X_cat.shape)) print(" y: {}".format(y.shape)) print("\n[OK] 特征工程模块测试通过")