欢迎加入专注于财经数据与量化投研的【数据科学实战】知识星球!在这里,您将获取持续更新的《财经数据宝典》和《量化投研宝典》,这两部宝典相辅相成,为您在量化投研道路上提供明确指引。 《量化投研宝典》精选了业内持续维护且实用性强的开源工具(Backtrader、Qlib、VeighNa等),配合详细教程与代码示例,帮助您快速构建量化策略;《财经数据宝典》则汇集了多年财经数据维护经验,全面介绍从 AKShare、Tushare 到 Wind、iFind 等国内外数据源,并附有丰富的使用技巧。 无论您是量化投资新手还是经验丰富的研究者,星球社区都能帮您少走弯路,事半功倍,共同探索数据驱动的投资世界!
引言
在量化金融领域,时间序列动量策略是一种经典的投资方法,它基于"强者恒强,弱者恒弱"的市场规律。今天,我们将通过 Python 实现一个完整的时间序列动量分析项目,从数据获取到模型评估,帮你掌握机器学习在金融领域的实际应用。
本文将带你构建线性回归模型来预测未来收益,并通过正则化技术解决过拟合问题,让你深入理解模型优化的核心技巧。
核心概念解析
时间序列动量
时间序列动量策略认为,过去表现良好的资产在未来一段时间内将继续表现良好。这种现象在金融市场中普遍存在,是量化投资的重要基础。
模型的过拟合与欠拟合
- 欠拟合:模型过于简单,无法捕捉数据中的规律,在训练集和测试集上表现都很差
- 过拟合:模型过度学习训练数据,包括噪声,在训练集上表现优秀但无法泛化到新数据
正则化技术
正则化通过对模型系数添加惩罚项来防止过拟合:
- Elastic Net:结合 Ridge 和 Lasso 的优点
实战步骤详解
步骤 1:导入库和获取数据
首先,我们需要导入必要的 Python 库并下载标普 500 指数 ETF(SPY)的历史数据:
import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
from sklearn import linear_model
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.model_selection import train_test_split, GridSearchCV, RepeatedKFold
from sklearn.preprocessing import MinMaxScaler
# 获取 SPY ETF 的历史市场数据
df = yf.download("SPY", start="2000-01-01", end="2025-01-01")
步骤 2:创建特征变量
接下来,我们构建模型的特征,计算不同时间窗口的历史收益率:
# 计算日收益率
df["Ret"
] = df["Close"].pct_change()
name = "Ret"
# 计算不同时间窗口的滚动复合收益率
df["Ret10_i"] = (df[name].rolling(10).apply(
lambda x: 100 * ((np.prod(1 + x)) - 1))) # 10 天收益率
df["Ret25_i"] = (df[name].rolling(25).apply(
lambda x: 100 * ((np.prod(1 + x)) - 1))) # 25 天收益率
df["Ret60_i"] = (df[name].rolling(60).apply(
lambda x: 100 * ((np.prod(1 + x)) - 1))) # 60 天收益率
df["Ret120_i"] = (df[name].rolling(120).apply(
lambda x: 100 * ((np.prod(1 + x)) - 1))) # 120 天收益率
df["Ret240_i"] = (df[name].rolling(240).apply(
lambda x: 100 * ((np.prod(1 + x)) - 1))) # 240 天收益率
# 清理数据框,删除不需要的列
del df["Open"]
del df["Close"]
del df["High"]
del df["Low"]
del df["Volume"]
df = df.dropna()
步骤 3:定义目标变量
我们的目标是预测未来 25 天的收益率:
# 创建目标变量:未来 25 天的收益率
df["Ret25"] = df["Ret25_i"].shift(-25)
df = df.dropna()
步骤 4:构建和训练线性回归模型
现在开始构建第一个模型,将数据分为训练集和测试集:
# 分离特征和目标变量
X, y = df.iloc[:, 0:-1], df.iloc[:, -1]
# 划分训练集和测试集(50% 用于测试)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=int(len(y) * 0.5), shuffle=False
)
# 训练线性回归模型
reg = linear_model.LinearRegression().fit(X_train, y_train)
# 进行预测
y_pred = reg.predict(X_train) # 训练集预测
y_pred_test = reg.predict(X_test) # 测试集预测
步骤 5:评估模型性能
让我们看看模型的表现如何:
print("样本内性能:")
print("均方误差:%.5f" % mean_squared_error(y_train, y_pred))
print("决定系数(R2):%.5f" % r2_score(y_train, y_pred))
print("\n样本外性能:")
print("均方误差:%.5f" % mean_squared_error(y_test, y_pred_test))
print("决定系数(R2):%.5f" % r2_score(y_test, y_pred_test))
输出结果:
样本内性能:
均方误差:0.05237
决定系数(R2):0.00905
样本外性能:
均方误差:0.03690
决定系数(R2):-0.05915
样本外的负 R² 值表明模型的预测能力极差,存在严重的欠拟合问题。
步骤 6:应用 Elastic Net 正则化
虽然模型存在欠拟合,但应用正则化仍有其价值:它能让模型更稳定,减少对噪声的敏感性。
from sklearn.linear_model import ElasticNet
# 训练 Elastic Net 模型
e_net = ElasticNet(alpha=0.0001, l1_ratio=0.1)
e_net.fit(X_train, y_train)
# 进行预测
y_pred_elastic = e_net.predict(X_test)
mean_squared_error_elastic = np.mean((y_pred_elastic - y_test) ** 2)
print("测试集均方误差(Elastic Net):", mean_squared_error_elastic)
步骤 7:超参数调优
使用网格搜索找到最佳的超参数组合:
model = ElasticNet()
# 设置交叉验证
cv = RepeatedKFold(n_splits=10, n_repeats=3, random_state=1)
# 定义参数网格
grid = dict()
grid["alpha"] = [1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 0.0, 1.0, 10.0, 100.0]
grid["l1_ratio"] = [0, 0.01, 0.1, 0.2, 0.5, 0.7, 1]
# 执行网格搜索
search = GridSearchCV(model, grid, scoring="neg_mean_squared_error", cv=cv, n_jobs=-1)
results = search.fit(X_train, y_train)
print("最佳 MSE:%.3f" % results.best_score_)
print("最佳参数:%s" % results.best_params_)
案例分析:为什么模型表现不佳?
通过上述实验,我们发现即使经过超参数调优,模型的改进也微乎其微。这个结果揭示了几个重要问题:
1. 线性假设的局限性
股票市场的复杂性远超线性关系所能描述的范围。市场受到宏观经济、政策变化、投资者情绪等多重因素影响,简单的线性模型难以捕捉这些复杂的非线性关系。
2. 信噪比过低
金融数据中的噪声远大于有效信号。仅凭历史价格信息预测未来收益,就像在嘈杂的环境中试图听清微弱的声音。
3. 特征工程的重要性
单纯使用历史收益率作为特征过于简单。实际应用中,我们需要引入更多技术指标,如:
改进方向
基于实验结果,我们可以从以下几个方向改进模型:
1. 丰富特征集
# 示例:添加技术指标
def add_technical_indicators(df):
# 添加简单移动平均线
df['SMA_20'] = df[
'Close'].rolling(window=20).mean()
df['SMA_50'] = df['Close'].rolling(window=50).mean()
# 添加 RSI 指标
delta = df['Close'].diff()
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
loss = (-delta.where(delta 0, 0)).rolling(window=14).mean()
rs = gain / loss
df['RSI'] = 100 - (100 / (1 + rs))
return df
2. 尝试非线性模型
考虑使用更复杂的模型,如:
3. 转换为分类问题
将回归问题转换为分类问题,预测价格涨跌方向而非具体收益率:
# 示例:创建分类标签
def create_classification_labels(returns, threshold=0):
"""
将收益率转换为分类标签
返回值大于阈值为 1(上涨),否则为 0(下跌)
"""
return (returns > threshold).astype(int)
总结
本文通过一个完整的 Python 量化金融项目,展示了如何使用时间序列动量策略预测股市收益。虽然简单的线性模型表现不佳,但这个过程让我们深入理解了:
更重要的是,我们学会了如何科学地评估模型性能,识别问题所在,并思考改进方向。在量化金融领域,失败的实验往往比成功的更有价值,因为它们告诉我们什么方法行不通,指引我们探索新的可能性。
记住,量化投资不是寻找"圣杯",而是在不确定性中寻找概率优势。继续学习,不断实践,你将在这个充满挑战的领域中找到属于自己的投资逻辑。
参考文章
加入专注于财经数据与量化投研的知识星球【数据科学实战】,获取完整研究解析、详细回测框架代码实现和完整策略逻辑实操指南。财经数据与量化投研知识社区
核心权益如下:
- 赠送《财经数据宝典》完整文档,汇集多年财经数据维护经验
- 赠送《量化投研宝典》完整文档,汇集多年量化投研领域经验
- 赠送《PyBroker-入门及实战》视频课程,手把手学习量化策略开发
星球已有丰富内容积累,包括量化投研论文、财经高频数据、 PyBroker 视频教程、定期直播、数据分享和答疑解难。适合对量化投研和财经数据分析有兴趣的学习者及从业者。欢迎加入我们!