
用 Python 揭秘均值回归策略:你的收益从何而来?
2026年重磅升级已全面落地!欢迎加入专注财经数据与量化投研的【数据科学实战】知识星球!您将获取持续更新的《财经数据宝典》与《量化投研宝典》,双典协同提供系统化指引;星球内含 500 篇以上独有高质量文章,深度覆盖策略开发、因子分析、风险管理等核心领域,内容基本每日更新;同步推出的「量化因子专题教程」系列(含完整可运行代码与实战案例),系统详解因子构建、回测与优化全流程,并实现日更迭代。我们持续扩充独家内容资源,全方位赋能您的投研效率与专业成长。无论您是量化新手还是资深研究者,这里都是助您少走弯路、事半功倍的理想伙伴,携手共探数据驱动的投资未来!
金融市场并非一成不变,它会在「风险偏好」(Risk-on)与「风险规避」(Risk-off)之间不断切换。当波动率飙升、信用利差走阔时,市场往往进入「压力状态」;而当行情平稳、资金涌入高收益资产时,市场则处于「扩张状态」。这些隐藏的「市场状态」(Market Regime)如果能被提前识别,对资产配置、波动率预测和系统化交易都极具价值。
本文将带你走完一套完整的混合机器学习(Hybrid ML)流程:从多资产数据获取、特征工程、PCA 降维、K-Means 聚类,到监督学习分类与可解释性评估。整个流程用 Python 实现,适合想把机器学习落地到金融场景的同学练手。
传统做法有两条路,各有短板:
混合框架巧妙地把两者结合,也被称为半监督状态建模或潜在状态预测,核心思路分两个阶段:
这样一来,标签由数据自动生成,预测模型也有了训练目标,一举解决了两条老路的痛点。
本案例特意挑选了一组信号更「混叠」、更难分离的资产组合,用来检验框架的稳健性:
第一步是获取从 2020 年到 2026 年的历史数据。下面用一个通用函数封装 API 调用:
import pandas as pdimport numpy as npstart_date = "2020-01-02"end_date = "2026-05-23"def fetch_eod(symbol, start_date, end_date): """获取指定标的的日线行情数据""" url = f"https://example-data-api.com/eod/{symbol}" params = { "from": start_date, "to": end_date, "fmt": "json" } resp = requests.get(url, params=params) data = resp.json() df = pd.DataFrame(data) df = df.sort_values("date") # 按日期排序,保证时间顺序 df["date"] = pd.to_datetime(df["date"]) df.set_index("date", inplace=True) # 把日期设为索引 return df# 分别拉取 5 个标的的数据spy_df = fetch_eod("VTI", start_date, end_date) # 全市场股票iwm_df = fetch_eod("IWO", start_date, end_date) # 小盘成长hyg_df = fetch_eod("JNK", start_date, end_date) # 高收益债lqd_df = fetch_eod("AGG", start_date, end_date) # 投资级债vix_df = fetch_eod("VXX", start_date, end_date) # 波动率小贴士:金融数据建模对数据质量极其敏感。如果数据太短或缺失太多,状态划分会非常不可靠,因此务必先用
df.info()检查数据完整性。
原始的 OHLCV 数据信息量有限,关键在于构造能反映市场结构的「衍生特征」。
用高收益债与投资级债的对数价差变化,刻画信用条件的松紧:
df = spy_df.copy().sort_index()# 内连接合并债券价格,只保留共同交易日df = df.join(hyg_df["adjusted_close"].rename("jnk"), how="inner")df = df.join(lqd_df["adjusted_close"].rename("agg"), how="inner")# 信用利差 = 高收益债与投资级债对数价格之差的变化量df["Credit_Spread"] = (np.log(df["jnk"]) - np.log(df["agg"])).diff()df = df.dropna()捕捉日内、短期、中期、长期的动量信号:
win_s, win_m, win_l = 15, 50, 150 # 短、中、长期窗口df["SPX_Daily_Return"] = spy_df["close"].pct_change() # 日收益率df["SPX_21D_Return"] = spy_df["close"].pct_change(win_s) # 短期df["SPX_63D_Return"] = spy_df["close"].pct_change(win_m) # 中期df["SPX_126D_Return"] = spy_df["close"].pct_change(win_l) # 长期用指数加权的对数收益率计算年化波动率:
def realized_vol(series, span=21): """基于指数加权对数收益率计算年化已实现波动率""" series = series.copy() series = series.replace([0, np.inf, -np.inf], np.nan) # 清除无效值 series = series.dropna() returns = np.log(series).diff() # 转为连续收益率 return returns.ewm(span=span).std() * np.sqrt(252) # 年化(252 个交易日)df["SPX_21D_RealVol"] = realized_vol(df["SPX_Daily_Return"], span=win_s)df["SPX_63D_RealVol"] = realized_vol(df["SPX_Daily_Return"], span=win_m)此外还可以构造波动率比值、回撤(Drawdown)等特征。最后记得清洗数据并剔除原始价格列,只保留工程化后的数值特征,避免「原始价格偏差」。
特征多了之后往往高度相关,先用 PCA(主成分分析)压缩维度:
from sklearn.decomposition import PCA# 标准化(这里用扩展窗口的均值和标准差,避免未来数据泄漏)X_scaled = (X_clean - X_clean.expanding().mean()) / X_clean.expanding().std()X_scaled = X_scaled.dropna()pca = PCA()X_pca = pca.fit_transform(X_scaled)explained_var = pca.explained_variance_ratio_ # 各主成分的方差贡献率案例结果显示:约 1314 个主成分就能保留 **95%96%** 的信息,说明原始特征存在大量冗余,降维空间很大。
接着用轮廓系数(Silhouette Score)来挑选最佳聚类数 K(系数越接近 +1 越好):
from sklearn.cluster import KMeansfrom sklearn.metrics import silhouette_scoredef silhouette_over_k(X, k_min=2, k_max=6, seed=42): """遍历不同 K 值,计算轮廓系数""" scores = {} for k in range(k_min, k_max + 1): km = KMeans(n_clusters=k, n_init=50, random_state=seed) labels = km.fit_predict(X) scores[k] = silhouette_score(X, labels) return scores# 最终选定 K=2,对全数据聚类kmeans = KMeans(n_clusters=2, n_init=50, random_state=42)final_labels = kmeans.fit_predict(X_final)df.loc[valid_idx, "Regime"] = final_labels # 把状态标签写回原表虽然轮廓系数只有约 0.18(分离度一般),但通过分组统计,两个状态依然展现出清晰的经济含义:
这说明聚类确实捕捉到了真实的市场风险结构,而非随机分组。
把状态标签当作目标变量,训练一个逻辑回归分类器:
from sklearn.model_selection import train_test_splitfrom sklearn.linear_model import LogisticRegressionfrom sklearn.metrics import accuracy_scoreX = X_final # PCA 降维后的特征y = df.loc[df["Regime"].notna(), "Regime"].astype(int).values# 分层抽样划分训练集与测试集X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.4, random_state=42, stratify=y)clf = LogisticRegression(max_iter=500, solver="lbfgs")clf.fit(X_train, y_train)y_pred = clf.predict(X_test)print("准确率:", accuracy_score(y_test, y_pred)) # 约 0.988测试集准确率高达 98.8%,两个状态的精确率、召回率、F1 分数都在 0.98 以上。这意味着波动率、收益率、信用利差等特征中,蕴含着高度可区分的状态信息。
光有高准确率还不够,还要让模型「可解释、可信赖」。常用的诊断工具包括:
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplayimport matplotlib.pyplot as plt# 计算并可视化混淆矩阵cm = confusion_matrix(y_test, y_pred, labels=[0, 1])disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=[0, 1])disp.plot(cmap=plt.cm.Blues)plt.title("混淆矩阵:状态 0 vs 状态 1")plt.show()延伸知识:ROC-AUC 衡量整体排序能力,而 KS 统计量关注两类概率分布的最大分离点;在样本不均衡时,精确率-召回率(PR)曲线往往比 ROC 更有参考意义。本案例两个状态样本量为 539 与 906,存在一定不均衡,因此多指标交叉验证尤为重要。
本文完整复现了一套用于市场状态识别的混合机器学习流程,核心要点可以归纳为:
这套「无监督发现 + 监督预测」的框架,为状态感知的资产配置、波动率预测和系统化交易提供了一个可扩展、可复现的实用范式。无论你是金融从业者还是机器学习爱好者,都值得动手跑一遍。
免责声明:本文内容仅供学习与教育用途,不构成任何投资建议。任何投资决策请结合自身情况并咨询专业人士。
2026年全面升级已落地!【数据科学实战】知识星球核心权益如下:
星球已沉淀丰富内容生态——涵盖量化文章专题教程库、因子日更系列、高频数据集、PyBroker实战课程、专家深度分享与实时答疑服务。无论您是初探量化的学习者,还是深耕领域的从业者,这里都是助您少走弯路、高效成长的理想平台。诚邀加入,共探数据驱动的投资未来!
好文推荐
1. 用 Python 打造股票预测系统:Transformer 模型教程(一)
2. 用 Python 打造股票预测系统:Transformer 模型教程(二)
3. 用 Python 打造股票预测系统:Transformer 模型教程(三)
4. 用 Python 打造股票预测系统:Transformer 模型教程(完结)
6. YOLO 也能预测股市涨跌?计算机视觉在股票市场预测中的应用
9. Python 量化投资利器:Ridge、Lasso 和 Elastic Net 回归详解
好书推荐