线性因子到端到端模型的尝试(二):提供数据集+训练代码
  ELVES 13天前 47 0

1.1 背景

这几天踩了不少数据的坑,趁热打铁总结一下,也希望能帮大家少走点弯路。数据清洗这块,很多人觉得是琐事,其实它对最终策略效果的影响非常大。模型的好坏,很多时候不是算法决定的,而是你喂进去的数据质量决定的。下面我举几个例子,大家就懂了:

  1. 数据不清洗,就像你要做个火爆肥肠结果菜都没洗,味道能对吗?哈哈哈。
  2. 第一次拿到因子数据,乍一看数值有点大,就想着直接 log 一下压缩,结果模型训练完发现还是在学风格因子,整段预测方向跑偏。
  3. 有些字段比如 ROE、净利润增长率,值是 0 或者极端异常,模型居然把这些噪音也学进去了。明明是亏损逃命的地方,它却看成了机会,一顿买买买。
  4. 有些因子分布偏态特别严重,没处理直接拿去训练。结果模型遇到一只妖股连续暴涨几天,它就高潮了,以为发现了黄金因子,其实纯属过拟合。
    所以说,数据清洗不是走个流程,而是生死线。有的文章把模型讲得天花乱坠,对数据只字不提,这是不负责任。喂进去的数据不对,模型怎么调都白搭。

1.2 数据集构建流程

本篇文章就是来讲数据集怎么构建的,特别是面向端到端的截面打分模型。我们希望把原始 K 线和因子数据加工成能直接用于训练和回测的格式。最后我会贴出数据下载地址,方便大家实操。
我们整个数据构建流程分成几大步:

  1. 基础数据准备(K线、财报、市值、行业等);
  2. 清洗(去极值、填缺失、剔除异常);
  3. 标准化 & 中性化(量纲统一、市值行业剥离);
  4. 特征工程(滚动窗口、衍生变量、标签构造等);
    不过别被这个流程骗了,实际做的时候是乱序的,比如 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画图看看例如:   
image.png
这种勉强能用
image.png
这种基本盖了帽了,所以大家一定要做好因子有效性的判断,如果因子无效完全别用。模型训练讲究的就是垃圾进、垃圾出,别搞一堆垃圾进去。
我这次用的因子有:
[‘换手率_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

最后一次编辑于 13天前 0

暂无评论

推荐阅读