1.1 背景
这几天踩了不少数据的坑,趁热打铁总结一下,也希望能帮大家少走点弯路。数据清洗这块,很多人觉得是琐事,其实它对最终策略效果的影响非常大。模型的好坏,很多时候不是算法决定的,而是你喂进去的数据质量决定的。下面我举几个例子,大家就懂了:
- 数据不清洗,就像你要做个火爆肥肠结果菜都没洗,味道能对吗?哈哈哈。
- 第一次拿到因子数据,乍一看数值有点大,就想着直接 log 一下压缩,结果模型训练完发现还是在学风格因子,整段预测方向跑偏。
- 有些字段比如 ROE、净利润增长率,值是 0 或者极端异常,模型居然把这些噪音也学进去了。明明是亏损逃命的地方,它却看成了机会,一顿买买买。
- 有些因子分布偏态特别严重,没处理直接拿去训练。结果模型遇到一只妖股连续暴涨几天,它就高潮了,以为发现了黄金因子,其实纯属过拟合。
所以说,数据清洗不是走个流程,而是生死线。有的文章把模型讲得天花乱坠,对数据只字不提,这是不负责任。喂进去的数据不对,模型怎么调都白搭。
1.2 数据集构建流程
本篇文章就是来讲数据集怎么构建的,特别是面向端到端的截面打分模型。我们希望把原始 K 线和因子数据加工成能直接用于训练和回测的格式。最后我会贴出数据下载地址,方便大家实操。
我们整个数据构建流程分成几大步:
- 基础数据准备(K线、财报、市值、行业等);
- 清洗(去极值、填缺失、剔除异常);
- 标准化 & 中性化(量纲统一、市值行业剥离);
- 特征工程(滚动窗口、衍生变量、标签构造等);
不过别被这个流程骗了,实际做的时候是乱序的,比如 K 线先清一遍,然后做完特征后还要再清一次,标准化和中性化也不是一步到位,而是来回迭代的,组合拳操作。
我们先搞清楚目标:模型不是用来预测收益本身,而是训练它学会一个排序分数,知道哪些股票“可能更强”。换句话说,是让它学会强弱关系,而不是具体涨幅。
数据集结构大概是:
因子值1 | 因子值2 | 因子值n | 标签值 |
---|---|---|---|
因子值1 | 因子值2 | … | 5% |
因子值1 | 因子值2 | … | 2% |
因子值1 | 因子值2 | … | -3% |
1.2.1 数据集准备与清洗
不建议自己手搓。PandaAI 有成型的数据服务,小助理微信问下就行,也可以用 akshare 这种开源项目,别造轮子。
这次项目我删了北交所、创业板、科创板的股票,然后剔除名称中带 ST、退市等的,接着干掉了全是 0 或 NaN 的字段和行。
1.2.2 数据集准备与清洗
私募有个经典因子库机制:1号库存所有因子,2号库只留那些表现合格的,每个月更新。
咱们训练用的因子一定得有逻辑支持,不能过时,别用 IC 已经负值的因子搞训练,纯浪费资源。
场景的评估指标可以用rankic画图看看例如:
这种勉强能用
这种基本盖了帽了,所以大家一定要做好因子有效性的判断,如果因子无效完全别用。模型训练讲究的就是垃圾进、垃圾出,别搞一堆垃圾进去。
我这次用的因子有:
[‘换手率_5’, ‘成交额Std_5’, ‘ROE_单季’, ‘EP_单季’,‘波动率_5’, ‘成交额Mean_5’, ‘市值’, ‘归母净利润同比增速_60’]
持仓周期一周,用过去 5 天的特征预测未来 5 天的表现,BTW:别问我为什么只有这么几个,o(╥﹏╥)o因为我一共就这么几个,机构都得几百个因子的来。
1.2.3 标签构造与再清洗
不能用收盘价算标签,错过了真实值最看最后一步,比如受拉盘的影响导致等等,会失真。我们用 VWAP,更接近真实成交成本,然后再做 Z-score 得到 label-z 作为监督信号。
推荐一个好用工具:YData,可以初步检查数据的缺失、异常值、偏态程度等,效率很高。
四、log 处理 & 分布优化
对所有特征做 log 转换能压缩尺度、降低极值影响、改善分布形态,特别是那些偏态严重的。做完以后画图一看,明显舒服多了。
1.2.4 统一量纲与中性化
举个生活中的例子,打拳击比赛,要分重量级和轻量级,你轻量级王者把你丢到重量级去你是个渣渣,所以要做中性化,大家把量级同意了再说技术问题。
不同因子的量纲不一致,分布也乱,直接训练会导致梯度不稳定,模型训练像心电图一样乱跳。
统一前还得先中性化,为什么?
- 换手率高的一般是小盘;
- 动量强的一般是行业轮动;
- ROE 高的多是白马;
你不做中性化,模型就会把这些结构“偷学”走,根本学不到真正 alpha。
记住,不是对市值中性化,是对其他因子中性化,利用市值来做剥离。
1.2.5 我理解的顺序:log 还是中性化先?
理论上,先中性化更符合逻辑。但实证发现先 log 再做剥离更容易接近正态分布,模型稳定性更好。两种方案各有利弊,别太教条。
最后这套数据集就可以用于后续训练了。别迷信模型复杂度,数据才是王道。
1.3 代码部分
建议大家做交易员不要做程序员,代码大家用大模型逐行写注释挨个看,没必要深究,如果要从感知机讲到MLP估计又是一篇文章,大家先跑起来吧,最后是代码+数据,直接运行,跑起来看看效果。
1.3.1 数据清洗
df = pd.read_pickle('/Users/rockontrol/Documents/github/lianghua/select-stock/data/运行缓存/factor_blue.pkl')
# 1. 计算未来5日VWAP收益作为label
df = df.sort_values(['股票代码', '交易日期'])
df['vwap_future_5'] = df.groupby('股票代码')['vwap'].shift(-5)
df['label'] = df['vwap_future_5'] / df['vwap'] - 1
# 2. 横截面 zscore 归一化标签
df['label_z'] = df.groupby('交易日期')['label'].transform(lambda x: zscore(x, nan_policy='omit'))
# 3. 保留所需字段
columns_to_keep = [
'交易日期','股票代码',
'换手率_5', '成交额Std_5', 'ROE_单季', 'EP_单季',
'波动率_5', '成交额Mean_5', '市值', '归母净利润同比增速_60',
'vwap', 'label_z'
]
# 4. 过滤数据交易日期大于2015-01-01
# 转换为日期类型(建议只做一次)
filtered_df = df[columns_to_keep]
filtered_df['交易日期'] = pd.to_datetime(filtered_df['交易日期'])
# 过滤大于2020年的数据(不带 copy)
filtered_df = filtered_df[filtered_df['交易日期'] > '2010-01-01']
# 指定你希望清洗的因子列
factor_cols = [
'换手率_5',
'成交额Std_5',
'ROE_单季',
'EP_单季',
'波动率_5',
'成交额Mean_5',
'市值',
'归母净利润同比增速_60'
]
# 打印初始样本数量
print(f"清洗前数据量:{filtered_df.shape[0]:,} 行")
# 过滤掉任意一个因子为 0 的行
filtered_df = filtered_df[(filtered_df[factor_cols] != 0).all(axis=1)]
# 过滤掉 label_z 为 NaN 的行(zscore异常导致)
filtered_df = filtered_df[filtered_df['label_z'].notna()]
# 对市值、成交额做 log 缩放
import pandas as pd
import pandas as pd
import numpy as np
import statsmodels.api as sm
# 1. 对偏态字段 log(1+x)
log_cols = ['市值', '成交额Mean_5', '成交额Std_5', '换手率_5', '波动率_5']
for col in log_cols:
filtered_df[col] = np.log1p(filtered_df[col])
filtered_df['市值_log'] = filtered_df['市值'] # 做协变量使用
# 2. 异常值裁剪
for col in ['ROE_单季', '归母净利润同比增速_60']:
q_low = filtered_df[col].quantile(0.01)
q_high = filtered_df[col].quantile(0.99)
filtered_df = filtered_df[(filtered_df[col] >= q_low) & (filtered_df[col] <= q_high)]
# 3. MAD 函数
def mad_normalize(series):
median = series.median()
mad = np.median(np.abs(series - median))
return (series - median) / (mad if mad != 0 else 1)
# 4. 中性化 + 针对性处理
target_cols = ['成交额Mean_5', '成交额Std_5', '换手率_5', '波动率_5']
extra_std_cols = ['换手率_5', '波动率_5']
for col in target_cols:
X = sm.add_constant(filtered_df['市值_log'])
y = filtered_df[col]
model = sm.OLS(y, X, missing='drop').fit()
resid = model.resid
resid_col = f"{col}_中性"
filtered_df[resid_col] = resid
# 如果是换手率/波动率,加 Z-score 和 MAD
if col in extra_std_cols:
filtered_df[f"{col}_z"] = (resid - resid.mean()) / (resid.std() if resid.std() != 0 else 1)
filtered_df[f"{col}_mad"] = mad_normalize(resid)
# 删除原始列,但 **不删除中性化后的残差**
filtered_df.drop(columns=[col], inplace=True)
# 5. 删除协变量和原始市值
filtered_df.drop(columns=['市值_log','换手率_5_中性','波动率_5_中性'], inplace=True)
# ✅ 输出最终保留列
print("最终保留字段:", filtered_df.columns.tolist())
from ydata_profiling import ProfileReport
# 生成数据报告(filtered_df 是你的训练数据)
# profile = ProfileReport(filtered_df, title="SilentAlpha 训练数据报告", explorative=True)
# # 保存成 HTML
# profile.to_file("alpha_data_report.html")
# exit()
#打印清洗后数据量
print(f"清洗后数据量:{filtered_df.shape[0]:,} 行")
filtered_df.to_parquet("/Users/rockontrol/Documents/github/lianghua/alpha_train_data.parquet")
filtered_df.to_csv('/Users/rockontrol/Documents/github/lianghua/训练数据.csv', index=False, encoding='utf-8-sig')
print(filtered_df)
exit()
1.3.2 模型训练部分
import pandas as pd
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout
from tensorflow.keras import backend as K
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
import matplotlib.pyplot as plt
import time
from tensorflow.keras.regularizers import l2
if tf.config.list_physical_devices('GPU'):
print("✅ 正在使用 GPU 训练")
else:
print("⚠️ 没有检测到 GPU,使用 CPU")
# Step 1: 加载数据
df = pd.read_parquet("./alpha_train_data.parquet")
df['归母净利润同比增速_60'] = df['归母净利润同比增速_60'].fillna(0)
df = df.replace([np.inf, -np.inf], np.nan).dropna()
# Step 2: 分训练/测试集
df_train = df[df['交易日期'] < '2023-01-01'].copy()
df_test = df[df['交易日期'] >= '2023-01-01'].copy()
# Step 3: 特征准备
label_col = 'label_z'
non_feature_cols = ['交易日期', '市值', '股票代码', 'vwap', '股票名称', label_col]
feature_cols = [col for col in df.columns if col not in non_feature_cols]
X_train = df_train[feature_cols].values
y_train = df_train[label_col].values
X_test = df_test[feature_cols].values
y_test = df_test[label_col].values
# ✅ 已标准化,无需重复标准化
# Step 4: IC Loss 函数
@tf.function
def ic_loss(y_true, y_pred):
y_true_centered = y_true - K.mean(y_true)
y_pred_centered = y_pred - K.mean(y_pred)
numerator = K.sum(y_true_centered * y_pred_centered)
denominator = K.sqrt(K.sum(K.square(y_true_centered)) * K.sum(K.square(y_pred_centered)) + K.epsilon())
return -numerator / denominator
# Step 5: 构建模型
model = Sequential([
Dense(128, activation='relu', input_shape=(X_train.shape[1],), kernel_regularizer=l2(1e-4)),
Dropout(0.2),
Dense(64, activation='relu', kernel_regularizer=l2(1e-4)),
Dense(32, activation='relu'),
Dense(1)
])
model.compile(optimizer='adam', loss=ic_loss)
# Step 6: 数据管道
batch_size = 1024
train_dataset = tf.data.Dataset.from_tensor_slices((X_train, y_train))
train_dataset = train_dataset.shuffle(5000).batch(batch_size).prefetch(tf.data.AUTOTUNE)
# Step 7: 训练模型
early_stop = EarlyStopping(monitor='loss', patience=20, min_delta=1e-4, restore_best_weights=True)
checkpoint = ModelCheckpoint(
filepath='model_epoch_{epoch:02d}.h5',
save_freq='epoch',
save_weights_only=False,
verbose=1
)
start_time = time.time()
history = model.fit(train_dataset, epochs=1000, callbacks=[ early_stop,checkpoint], verbose=1)
duration = time.time() - start_time
# Step 8: 保存模型
model.save("silentalpha_mlp_ic_model.h5")
print("✅ 模型已保存为 silentalpha_mlp_ic_model.h5")
# Step 9: 预测 + 可视化
y_pred = model.predict(X_test).flatten()
plt.figure(figsize=(6, 4))
plt.scatter(y_test, y_pred, alpha=0.3)
plt.xlabel("True label_z")
plt.ylabel("Predicted label_z")
plt.title("MLP预测结果(IC Loss)")
plt.grid(True)
plt.tight_layout()
plt.savefig("mlp_ic_loss_prediction.png")
plt.show()
print(f"\n✅ 训练耗时:{duration:.2f} 秒,预测图已保存为 mlp_ic_loss_prediction.png")
# Step 10: 保存预测
df_test['pred'] = y_pred
df_test.to_parquet("alpha_test_pred.parquet")
print("✅ 测试集预测结果保存为 alpha_test_pred.parquet")
# Step 11: 训练损失可视化
plt.figure(figsize=(6, 4))
plt.plot(history.history['loss'], label='Train Loss', color='blue')
plt.xlabel("Epoch")
plt.ylabel("IC Loss")
plt.title("训练过程中的 IC Loss")
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.savefig("train_loss_curve.png")
plt.show()
print("✅ 训练损失曲线已保存为 train_loss_curve.png")
整理好后的数据地址,太大了,我放百度网盘了,大家可以直接下载然后跑训练脚本。
链接: https://pan.baidu.com/s/1iX3wPHaoG9sTqwXRu-AUAQ 提取码: 3cmt