我用1分钟给股票找出了3000个交易因子,用过的人都发财了
炒股的人,做量化的人,甚至是不小心刷到财经视频的你……多多少少都听过“交易因子”这几个字。
听起来就很高级对吧? 什么动量因子、反转因子、大小盘因子、情绪因子…… 每次别人提起都眉飞色舞,好像掌握了什么财富密码, 你问一句“啥是因子?” 他们:你不懂 你再问一句“怎么做因子?” 他们:你不配
得了,我忍了好几年了。
今天,花姐我就带着键盘——不,带着良心, 来给你扒开交易因子的真面目,顺便还手把手教你:
怎么用Python,一分钟干出3000个因子, 不是“瞎编的因子”,是真·能喂给模型、能配对策略、能拿去回测的那种!
要是你能跟着我整完, 下次就轮到你对别人说:“这不是因子的问题,是你不会提。”
啥是交易因子? 简单点说,它就像股票市场里的“隐藏秘籍”。别人看K线、看均线、看MACD,你看交易因子——直接抢跑三步。
那么问题来了:怎么才能快速、批量地生成交易因子?
我试过很多招,最后发现了一个宝藏库,堪称“因子工厂”,它的名字是——tsfresh。
tsfresh:一台会炼金的因子生成器
tsfresh 是一个专注于时间序列特征提取的 Python 库,全称是 Time Series Feature extraction based on scalable hypothesis tests。它的核心功能,就是从一段或多段时间序列中,自动提取出大量可用于机器学习建模的统计特征。
这些特征,也就是我们常说的因子(在金融量化语境下)。
更具体一点,tsfresh 内置了上百种特征计算方法,涵盖常见的统计量(如最大值、最小值、平均值、方差、偏度、峰度)、频域特征、小波变换、傅里叶变换、复杂度指标、变化率指标等,甚至还有一些你连听都没听过的“奇葩特征”——什么change_quantiles、agg_autocorrelation、linear_trend_timewise,只要能用数学刻画时序,它都能榨出来。
你甚至都不用自己去定义特征函数,它会把能提的都给你提一遍,一口气喂你几千个。
它的设计初衷其实是面向工业 IoT 领域,比如设备预测维护之类的,但它天生适合金融时间序列。尤其是我们搞量化、做选股、因子挖掘这类场景,数据形态几乎一模一样。
说白了,tsfresh 就是把“写因子”这件原本需要金融知识、经验积累、血泪教训的事,变成了“数据一扔,特征全来”的自动化流程。
当然,它的输出并不是终点,你还需要后续做筛选、降维、建模、回测。但就“因子发掘”这个环节而言,它极大地降低了门槛,是初学者和实验驱动型开发者的一大利器。
不过话说回来,tsfresh 也不是没有毛病——
它有时候提的因子重复性强、冗余度高,而且默认配置下会很吃内存和计算资源。
所以用得好是一把宝剑,用不好就是因子垃圾场。怎么用,咱一会儿慢慢讲。
思路全景
今天我们以300ETF为例,教大家如何从头开始提取交易因子。
我们要干的事可以分 3 步走:
- 获取 300ETF 历史行情 + 衍生技术指标
- 用
tsfresh从行情+指标中自动提取“时序特征因子” - 筛选出有用的因子
Step 1:获取 300ETF 数据 + 计算技术指标
我们选用 沪深300ETF(510300.SH),通过 AKShare 获取过去两年的日线数据,并计算常见的技术指标(MACD、RSI、KDJ)。
# 01_data_prep.py
import akshare as ak
import pandas as pd
import talib
from datetime import datetime, timedelta
# 获取过去两年日线数据
symbol = "510300"
start_date = (datetime.today() - timedelta(days=365 * 2)).strftime('%Y%m%d')
df = ak.fund_etf_hist_em(symbol=symbol, period="daily", start_date=start_date, end_date='20250520')
# 重命名字段
df = df.rename(columns={
"日期": "date",
"开盘": "open",
"收盘": "close",
"最高": "high",
"最低": "low",
"成交量": "volume",
})
df['date'] = pd.to_datetime(df['date'])
df = df.sort_values('date').reset_index(drop=True)
# 使用 TA-Lib 计算技术指标
df['rsi'] = talib.RSI(df['close'], timeperiod=14)
macd, macdsignal, macdhist = talib.MACD(df['close'], fastperiod=12, slowperiod=26, signalperiod=9)
df['macd'] = macd
df['macds'] = macdsignal
df['macdh'] = macdhist
slowk, slowd = talib.STOCH(
df['high'], df['low'], df['close'],
fastk_period=9, slowk_period=3, slowk_matype=0,
slowd_period=3, slowd_matype=0
)
df['kdjk'] = slowk
df['kdjd'] = slowd
df['kdjj'] = 3 * slowk - 2 * slowd # 手动构造 J 值(模拟 KDJ 的 J 值)
# 清洗 NaN
df = df.dropna().reset_index(drop=True)
df.to_csv("01data.csv",index=False)
Step 2:用 tsfresh 自动提取因子特征
接下来,我们将数据转换为 tsfresh 需要的格式,并从中提取“时间窗口内的统计特征”。
库安装命令
pip install tsfresh
# 02——feature_extract.py
from tsfresh import extract_features
from tsfresh.utilities.dataframe_functions import impute
from tsfresh.feature_extraction import EfficientFCParameters
from tqdm import tqdm # 用于进度条展示(可选)
import warnings
import pandas as pd
df = pd.read_csv("01data.csv")
df['date'] = pd.to_datetime(df['date'])
warnings.filterwarnings("ignore", category=RuntimeWarning, module="tsfresh.utilities.dataframe_functions")
# === 滑动窗口提取 tsfresh 特征 ===
WINDOW_SIZE = 20
features_list = []
print(f"开始提取滑动窗口特征,每个窗口长度为 {WINDOW_SIZE}...")
for i in tqdm(range(WINDOW_SIZE, len(df))):
df_window = df.iloc[i - WINDOW_SIZE:i].copy()
# 只提取 close、volume、rsi、macd 四个作为例子
tsfresh_df = df_window[['date', 'close', 'volume', 'rsi', 'macd']].copy()
tsfresh_df['id'] = 0 # 单一时间序列
# 转为 long-format
df_long = tsfresh_df.melt(id_vars=["date", "id"], var_name="kind", value_name="value")
df_long.rename(columns={"date": "time"}, inplace=True)
# 提取特征
features = extract_features(
df_long,
column_id="id",
column_sort="time",
column_kind="kind",
column_value="value",
default_fc_parameters=EfficientFCParameters(),
n_jobs=0,
disable_progressbar=True
)
impute(features) # 缺失值(NaN)处理函数,处理缺失值
features.columns = [f'feature_{col}' for col in range(features.shape[1])]
features['date'] = df.iloc[i]['date'] # 当前行时间作为结果时间
features_list.append(features)
# 合并所有窗口结果
df_all_features = pd.concat(features_list, axis=0).reset_index(drop=True)
df_all_features.to_csv("02data.csv", index=False)
# 展示结果
print(df_all_features.head())
print("******************特征提取完毕**********************")
运行完以后我们就得到了feature_0 feature_1 ... feature_3106 feature_3107 date
总计3107个因子了
Step 3:因子这么多,我该用哪几个?
因子提出来了,下一步当然就是——挑出“有用的因子”,也就是特征选择(feature selection) 这一步。咱不能全都上,不然模型就容易陷入高维灾难、过拟合,甚至运行慢还没提升效果。
常见的因子筛选方法主要有以下几种: 1. 过滤法(Filter Method) —— 快速初筛 典型方法:
| 方法 | 原理 | Python 工具 |
|---|---|---|
| 方差过滤 | 删除方差小的列(波动小,无信息) | VarianceThreshold |
| 相关系数 | 与目标的 Pearson/Spearman 相关性 | df.corr() |
| 单变量评分 | f-value, chi2, mutual_info | sklearn.feature_selection |
from sklearn.feature_selection import VarianceThreshold
# 例子:删除方差小于阈值的因子
selector = VarianceThreshold(threshold=1e-5)
X_filtered = selector.fit_transform(X)
2. 包裹法(Wrapper Method) —— 模型亲测,有点“炼丹”味 常用方法:
| 方法 | 原理 | 优点 |
|---|---|---|
| 递归特征消除(RFE) | 每次删掉不重要的特征,用模型重复训练 | 比较稳 |
| forward/backward selection | 从少到多/多到少加减特征 | 直观 |
from sklearn.feature_selection import RFE
from sklearn.ensemble import RandomForestClassifier
rfe = RFE(estimator=RandomForestClassifier(), n_features_to_select=20)
X_selected = rfe.fit_transform(X, y)
3. 嵌入法(Embedded) —— 直接利用模型自身的特征重要性 常用方法:
| 模型 | 方法 |
|---|---|
| Lasso | L1 正则会把不重要特征权重压成 0 |
| 决策树 / GBDT / XGBoost | 自带 feature_importances_ |
| LightGBM | 超强推荐,速度快,重要性可解释 |
import lightgbm as lgb
model = lgb.LGBMClassifier()
model.fit(X, y)
importance = pd.Series(model.feature_importances_, index=X.columns)
top_features = importance.sort_values(ascending=False).head(30)
4. 基于稳定性的因子选择(金融量化专属)
-
计算每个因子对未来收益的 IC(信息系数)
-
选取长期 IC 稳定、显著为正的因子
-
更贴近实战!
# 举个例子(IC)
from scipy.stats import spearmanr
ic_list = []
for col in X.columns:
ic, _ = spearmanr(X[col], y)
ic_list.append((col, ic))
ic_df = pd.DataFrame(ic_list, columns=["feature", "IC"]).sort_values("IC", ascending=False)
实战建议:
| 情况 | 推荐操作 |
|---|---|
| 先快点跑一遍看哪些特征是废的 | 过滤法 |
| 做建模数据集,搞模型准确率 | 嵌入法(LGB/XGBoost) |
| 做因子有效性分析(金融) | 计算 IC、IR |
| 自动化多因子回测/研究 | 用因子池,每月选表现好的 |
tsfresh 自带特征选择!
这里我们用tsfresh 自带特征选择来实现因子筛选。 假如我们想通过因子来筛选未来5天会上涨的股票,这里花姐给出了一个简单的示例:
# 03_feature_select.py
from tsfresh import select_features
import pandas as pd
from tsfresh.utilities.dataframe_functions import impute
def main():
df = pd.read_csv("01data.csv")
df['date'] = pd.to_datetime(df['date'])
df_all_features = pd.read_csv("02data.csv")
df_all_features['date'] = pd.to_datetime(df_all_features['date'])
# 设定你的特征 DataFrame(去掉 date)
X = df_all_features.drop(columns=["date"])
# 构造一个简单的目标变量(比如未来5天是否上涨)
df_label = df[['date', 'close']].copy()
df_label['target'] = df_label['close'].shift(-5) > df_label['close']
df_label['target'] = df_label['target'].astype(int)
# 只保留与 X 对齐的时间(注意时间是滞后的)
y = df_label[df_label['date'].isin(df_all_features['date'])]['target'].reset_index(drop=True)
# Step 1: 填补缺失值
impute(X)
print("******************使用 tsfresh 进行特征选择**********************")
# Step 2: 使用 tsfresh 进行特征选择
X_selected = select_features(X, y)
print("******************输出选择后的因子**********************")
# Step 3: 输出选择后的因子
print(f"选出来的有效因子数量:{X_selected.shape[1]}")
print("部分有效因子名:", X_selected.columns[:10].tolist())
df_selected_features = df_all_features[['date']+X_selected.columns.tolist()]
df_selected_features.to_csv("selected_features_with_data.csv", index=False)
if __name__=="__main__":
main()
看到这里你可能好奇 为啥一定要加 if name == 'main':
你用的是 tsfresh.select_features(X, y),它在内部使用了 multiprocessing.Pool() 来加速计算。但是在 Windows 系统下Python 启动多进程时,会重新「导入整个主模块」,如果你没有加 if __name__ == '__main__':,Python 会直接执行整个文件——就形成了无限递归「导入-执行-导入-执行」,然后炸锅!
所以不同于 Linux/macOS,需要你用:
if __name__ == '__main__':
来保护你的主逻辑。
画图看看因子是不是在扯淡
你没看错,就是画图。眼睛一看就知道因子灵不灵。
import pandas as pd
import matplotlib.pyplot as plt
# ===== 设置中文支持 =====
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# ===== 读取数据 =====
df_price = pd.read_csv("01data.csv", parse_dates=["date"])
df_features = pd.read_csv("selected_features_with_data.csv", parse_dates=["date"])
# ===== 合并两个数据集(按日期对齐)=====
df_merged = pd.merge(df_price[['date', 'close']], df_features, on='date', how='inner')
# ===== 选取前10个特征列 =====
feature_cols = [col for col in df_features.columns if col != 'date'][:10]
# ===== 绘图开始 =====
fig, ax1 = plt.subplots(figsize=(16, 8))
# --- 主轴:收盘价 ---
color = 'tab:blue'
ax1.set_xlabel("日期")
ax1.set_ylabel("收盘价", color=color)
ax1.plot(df_merged['date'], df_merged['close'], color=color, label='收盘价', linewidth=2)
ax1.tick_params(axis='y', labelcolor=color)
# --- 副轴:特征曲线 ---
ax2 = ax1.twinx()
ax2.set_ylabel("特征值")
# 颜色循环(可以自定义更漂亮的)
colors = plt.cm.tab10.colors # 10个颜色
# 画10个特征线
for i, col in enumerate(feature_cols):
ax2.plot(df_merged['date'], df_merged[col], label=col, linestyle='--', color=colors[i % 10])
ax2.tick_params(axis='y')
fig.tight_layout()
# 图例合并收盘价和特征
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc='lower right')
plt.title("收盘价 + 10个特征走势" ,fontsize=16, pad=20)
plt.grid(True)
fig.tight_layout()
plt.subplots_adjust(top=0.92) # 0.92 表示顶部保留 8% 空间(1 是满)
plt.show()
下一步预告:这些因子到底能不能赚钱?
别急,下一篇文章我们就来做个简单回测——看看这些因子能不能当上“预言家”。 记住一句话:
因子不是万能的,但没有因子是万万不能的。
要是你觉得写得还行,就留个在看 收藏 点赞 呗~ 我不是为了流量,我是怕你以后找不到(认真脸)。
—— 花姐 ❤️