kronos / webui /output /feature_engineering.py
sgfw's picture
Upload 68 files
459d1c8 verified
Raw History Blame Contribute Delete
27.3 kB
# -*- 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] 特征工程模块测试通过")