ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python线性模型分析AAPL股票:量化实战与边界探讨

Python线性模型分析AAPL股票:量化实战与边界探讨 前几天整理量化研究笔记翻到一个年初跑过的课题用Python线性模型分析AAPL股票数据。当时做这个项目并不是真觉得线性模型能预测苹果股价而是想验证一个困扰不少初学者的问题——在金融数据这种公认的低信噪比环境里线性模型到底还有没有实用价值。市面上太多教程一上来就推LSTM和Transformer却忽略了一个事实如果线性模型的残差里连边际信息都不存在那复杂模型大概率也只是在拟合噪声。抱着这个疑问我用AAPL做样本把线性模型从数据获取、特征工程、训练调参到回测评估完整跑了一遍过程中踩了不少坑也纠正了不少认知偏差。这篇文章适合两类人读一是刚入门金融量化、想搞清楚线性模型在股票数据上怎么落地的Python用户二是做特征工程时对多重共线性、时间序列交叉验证、未来函数这些概念还比较模糊的进阶学习者。全文基于Python生态用公开的AAPL历史行情数据可以直接复现。1. 为什么拿AAPL当线性模型的试验田1.1 AAPL的数据特征决定了线性建模的成败说实话AAPL可能是全球量化教程里出现频率最高的样例标的但大多数人只是拿它跑个demo就完事没想过选它的逻辑。我这次特意把它作为分析对象因为AAPL的数据形态对线性模型非常友好而这种友好本身就是研究的一部分。第一流动性极好。作为全球市值最大的公司之一AAPL的日均成交额长期在百亿美元级别这意味着它的价格序列基本不存在长时间无成交、极端跳空这类问题。线性模型对样本质量极其敏感尤其对离群值没有天然免疫力。你去拿一只日均成交只有几十万美元的小盘股做同样的实验回归系数会被流动性枯竭时的异常价格严重带偏结论完全没有参考意义。第二波动结构相对稳定。AAPL的日收益率绝大多数时候落在正负3%以内不像某些题材股动辄20%的振幅。虽然金融收益率的厚尾特征依然存在但AAPL的数据至少更接近可控偏离残差的形态不会过分扭曲模型估计。第三数据历史长且连续。AAPL的完整日线数据可以一直追溯到1980年代样本量充足。这对接下来的时间序列交叉验证很重要——我们有足够的数据去模拟过去的模型遇到未来行情的真实场景而不是在几百条数据上硬凑。1.2 先把研究目标边界画清楚做金融量化最容易犯的错误是目标模糊。你是想预测下一个交易日的收盘点位还是想预测收益率的涨跌方向这两个问题的建模路径完全不同评估标准也完全不同。我这次把目标定为基于历史量价数据构造特征用线性模型预测AAPL未来一个交易日的收益率方向涨或跌同时用回归形式预测收益率数值用来对比不同模型的拟合能力。注意我没有试图精确预测股价会到188.3还是189.1那属于时间序列的另一个难度量级。预测方向已经足够检验线性模型在股票数据上有没有真实的解释力。目标界定的意义在于它决定后续所有选择。如果目标是连续值预测就用OLS、岭回归、Lasso如果目标是涨跌方向就加一道Logistic回归。我在后面会把两条线都走一遍分别报告结果这样才能完整看到线性框架的能力边界。1.3 研究环境与依赖清单整个实验在Python 3.10环境下完成核心依赖如下库名版本职责yfinance0.2.x拉取AAPL历史行情pandas2.x数据处理与特征工程numpy1.26数值计算statsmodels0.14OLS回归与统计检验scikit-learn1.4岭回归、Lasso、逻辑回归、交叉验证matplotlib3.8可视化如果你是从零开始搭环境建议直接用Anaconda建一个独立虚拟环境别把量化依赖装进日常项目里。虚拟环境之间互相污染是我见过最普遍的环境问题没有之一。安装命令很简单conda create -n quant python3.10 conda activate quant pip install yfinance pandas numpy statsmodels scikit-learn matplotlib2. 数据管线搭建yfinance拉取AAPL数据的完整流程2.1 用yfinance获取日线数据的正确姿势yfinance是目前获取美股历史行情最方便的开源接口不需要申请API key也不用处理复杂的鉴权流程。AAPL的日线数据一次就能完整拉下来import yfinance as yf ticker yf.Ticker(AAPL) df ticker.history(start2015-01-01, end2024-12-31, interval1d, auto_adjustTrue) print(df.head()) print(df.shape)默认拿到的DataFrame包含Open、High、Low、Close、Volume五个字段索引是Datetime。auto_adjustTrue这个参数要特别注意它表示Close列会自动复权到后复权价格也就是把拆股和现金分红都打在历史价格里。做量化回测时必须用复权价格否则模型会在拆股日看到一个虚假的暴跌信号训练出来的系数完全失真。以AAPL为例它在2015到2024年间经历过多次拆股其中最典型的是2020年8月的1拆4。如果不用复权价那么2020年8月31日当天价格会从400多直接跳到100多这种人为的价格断裂会被特征计算放大成显著的错误信号模型就会去学习一个根本不存在的事件。默认的auto_adjustTrue已经帮你处理了这个问题但如果你在别的教程里看到用data[Adj Close]的方式那是在提醒你检查接口的旧版行为。2.2 缺失值与异常值的清洗策略拉下来的数据并不是干净的。AAPL这种大票也会偶尔出现缺失行原因可能是交易所临时休市、数据源同步异常等。处理缺失值的原则是能查明原因的保留查不明的宁可删除不要自作聪明地做插值。我的处理方式如下# 删除完全空的行 df df.dropna(subset[Open, High, Low, Close, Volume]) # 剔除成交量为0的日子 df df[df[Volume] 0] # 检查索引是否连续排除非交易日 df df[~df.index.duplicated()] print(df.shape) print(df.isnull().sum())另一类需要警惕的是异常价差。比如某天High比Low低或者Open偏离Close超过极端阈值这通常是数据源本身出错。AAPL这种高流动性标的极少出这种问题但我仍然建议做一次逻辑校验bad_rows df[(df[High] df[Low]) | (df[Open] 0) | (df[Close] 0)] print(f异常行数: {len(bad_rows)})如果异常行数占比超过0.1%说明数据源可能有系统性偏差这时候考虑换更可靠的源而不是简单删掉。2.3 对数收益率为什么不是简单百分比拿到价格之后第一步要构造日收益率。这里有一个几乎所有量化教程都会提、但很少讲清楚为什么的细节用对数收益率而不是简单百分比收益率。简单收益率 r_t (P_t - P_{t-1}) / P_{t-1}对数收益率 log_ret_t ln(P_t / P_{t-1})两者在小幅波动下数值接近但有两个差别非常重要。第一对数收益率的时间可加性多日对数收益率等于每日对数收益率之和这让特征计算里的多窗口聚合变得干净。第二对数收益率的分布更接近对称在建模时更不容易被右偏的极端上涨扭曲。AAPL这种长期上涨的股票简单收益率的分布明显右偏直接拿它做OLS会因为少数暴涨日拉高均值而对数变换能在一定程度上缓解这个效应。import numpy as np df[log_ret] np.log(df[Close] / df[Close].shift(1)) df df.dropna(subset[log_ret]) print(df[log_ret].describe())对这十年数据来说日对数收益率的均值大概在0.08%附近20日滚动波动率在1.5%-2%之间波动。这些描述性统计后面会反复用到建议一开始就打出来备份。3. 特征工程线性模型真正需要的输入变量3.1 动量因子多窗口收益率的线性表达线性模型的输入不能是价格本身因为价格的绝对水平取决于历史复权基准不同时间区间的量纲不一致模型学不到稳定规律。正因如此我把原始价格全部转化成收益率形态的特征。动量因子是最经典的量价特征之一逻辑上捕捉的是过去一段时间涨得好的股票短期内是否还会延续趋势。我用三个窗口分别构造df[mom_5] df[Close] / df[Close].shift(5) - 1 df[mom_10] df[Close] / df[Close].shift(10) - 1 df[mom_20] df[Close] / df[Close].shift(20) - 1为什么要三个窗口而不是一个不同周期动量在不同市场状态下各有强弱。5日动量偏短线20日动量偏波段。三个同时在模型里让线性模型自己去分配权重这也方便后续观察哪个周期对AAPL的解释力更强。需要提醒的是动量因子的计算同样是未来函数的高发区。shift(5)表示用t-5到t-1的信息预测t日的收益率千万不要在特征构造时混入了当天的收盘价信息否则训练时R²会虚高回测时被打回原形。这个问题我在第6节还会详细说。3.2 波动率因子滚动标准差与ATR第二个维度是风险类特征。金融数据的一个典型现象是波动率聚集——大波动之后通常跟着大波动平静之后通常跟着平静。线性模型如果能捕捉到当前所处的波动状态对预测收益率方向也会有一定帮助。我用两个指标刻画波动df[vol_10] df[log_ret].rolling(10).std() df[vol_20] df[log_ret].rolling(20).std()滚动标准差衡量的是收盘价对数收益率的标准差它捕捉的是价格变动幅度这一维度。另外我还计算了ATR平均真实波幅它综合考虑了跳空缺口的影响df[tr] np.maximum( df[High] - df[Low], np.maximum(abs(df[High] - df[Close].shift(1)), abs(df[Low] - df[Close].shift(1))) ) df[atr_14] df[tr].rolling(14).mean()ATR这玩意儿本来是做止损位设计的但拿它当波动特征一样好用因为它在收盘价之外利用了最高价和最低价的信息对日内振幅更敏感。3.3 成交量因子量价关系的窗口量是价的先行指标这句话在美股、A股都一样适用。我把成交量处理成两个角度一个是环比变化一个是相对长期均值的偏离。df[vol_ratio_20] df[Volume] / df[Volume].rolling(20).mean() df[vol_pct_change] df[Volume].pct_change()vol_ratio_20大于1说明量能放大小于1说明缩量。这个因子理解门槛低但对线性模型的增量解释力确实有限。实话说AAPL这种机构主导的标的成交量信号更多是已实现信息的确认而不是前瞻信息。量化项目里不要指望每个因子都有效大部分因子的系数在统计上可能根本不显著这正是线性模型坦诚的地方。3.4 VIF检验把共线性扼杀在训练前特征构造完成后别急着训练。线性模型最怕的是特征之间的高度相关。动量因子mom_5、mom_10、mom_20天然高度相关——5日涨得多的股票20日往往也涨得多。如果不处理OLS回归系数的标准误会急剧膨胀单个因子的t值会变得很不稳定模型看起来能解释一些东西但拆解不出各因子的独立贡献。我用方差膨胀因子来量化共线性程度from statsmodels.stats.outliers_influence import variance_inflation_factor feature_cols [mom_5, mom_10, mom_20, vol_10, vol_20, atr_14, vol_ratio_20, vol_pct_change] X_base df[feature_cols].dropna() vif_data pd.DataFrame({ feature: X_base.columns, VIF: [variance_inflation_factor(X_base.values, i) for i in range(X_base.shape[1])] }) print(vif_data)经验法则是VIF超过10就要警惕超过30基本可以判定为严重共线性。在我这个数据集上mom_5、mom_10、mom_20之间的VIF普遍在15以上vol_10和vol_20的VIF也在10上下。这意味着直接把这些特征喂给OLS很多系数的解读都会失真。处理方式有三种一是删掉部分高度相关的特征只保留动量窗口里最有代表性的一个二是用岭回归通过L2惩罚吸收共线性带来的方差膨胀三是用Lasso做特征选择让模型自动剔除冗余。我在第4节里把这三种路数全跑一遍正好可以比较它们的差异。4. 线性模型家族的训练与对比4.1 基准模型用statsmodels跑OLS先跑一个最朴素的基线用全部8个特征对下一交易日收益率做OLS回归数据集按时间顺序用前80%训练、后20%验证。import pandas as pd from sklearn.model_selection import train_test_split feature_cols [mom_5, mom_10, mom_20, vol_10, vol_20, atr_14, vol_ratio_20, vol_pct_change] df_model df.dropna(subset[log_ret]).copy() df_model[target] df_model[log_ret].shift(-1) # 下一交易日收益率 df_model df_model.dropna(subset[target]) X df_model[feature_cols] y df_model[target] split_idx int(len(df_model) * 0.8) X_train, X_test X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test y.iloc[:split_idx], y.iloc[split_idx:]注意这里target用了shift(-1)含义是用第t日已知的量价信息预测第t1日的收益率。方向千万别写反。很多人第一次写的时候容易用shift(1)那就变成了用未来预测过去训练集上效果好到离谱。拟合和解读import statsmodels.api as sm X_train_sm sm.add_constant(X_train) ols sm.OLS(y_train, X_train_sm).fit() print(ols.summary())结果基本符合金融量化的常识训练集R²通常在0.01到0.03之间远低于机器学习基准测试里的数字。你可能会觉得这模型没用但这恰恰是金融数据的正常形态。收益率的可预测成分本来就极弱如果R²动辄0.3所有人早就靠线性模型发财了市场也就不存在了。线性模型的相对价值恰好在于它能够诚实地告诉你这些特征对AAPL的解释力就是非常有限。从系数看mom_10和vol_10在统计上偶尔显著但符号在不同时间区间并不稳定。这里的解读要非常谨慎系数显著并不意味着下周还能显著这种不稳定性本身就是量化交易里常说的因子衰减的早期信号。4.2 岭回归当共线性无法完全消除OLS的问题在于特征之间的共线性会让系数估计的方差变大导致模型在样本外表现不稳定。岭回归通过加入L2正则项以牺牲一点训练集拟合度为代价换取系数方差的降低。sklearn里的Pipeline很方便先把特征标准化再做岭回归from sklearn.linear_model import Ridge from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.metrics import mean_squared_error, r2_score ridge_pipe Pipeline([ (scaler, StandardScaler()), (ridge, Ridge(alpha1.0)) ]) ridge_pipe.fit(X_train, y_train) y_pred_ridge ridge_pipe.predict(X_test) print(fRidge RMSE: {mean_squared_error(y_test, y_pred_ridge, squaredFalse):.5f}) print(fRidge R²: {r2_score(y_test, y_pred_ridge):.4f})alpha是正则化强度越大惩罚越强系数越向0收缩。实际调参可以用交叉验证搜索。需要明白的是岭回归不会让某个系数变成严格的0它只会让它趋近于0。所以如果你想要的不仅是预测还想做特征解释岭回归提供了比OLS稳定得多的系数路径分析。我在这个数据集上跑下来的感受是岭回归的样本外RMSE几乎和OLS持平但系数波动确实小了很多。这说明正则化有效地稳定了模型哪怕它没有显著提高预测力。对量化场景来说稳定本身就是一种优势因为实盘策略最怕的就是今天参数训练出来是正的明天换一段数据就变成负的。4.3 Lasso让模型自己挑特征Lasso用的是L1正则化它的特点是可以把不重要的特征系数直接压缩成0从而实现自动特征选择。from sklearn.linear_model import Lasso lasso_pipe Pipeline([ (scaler, StandardScaler()), (lasso, Lasso(alpha0.005)) ]) lasso_pipe.fit(X_train, y_train) y_pred_lasso lasso_pipe.predict(X_test) lasso_coefs pd.Series(lasso_pipe.named_steps[lasso].coef_, indexfeature_cols) print(lasso_coefs[lasso_coefs ! 0])alpha的选择很重要。太小等于没做正则化太大把所有系数都压没了。用GridSearchCV在训练集上搜索是比较稳妥的做法from sklearn.model_selection import GridSearchCV param_grid {lasso__alpha: [0.0001, 0.001, 0.005, 0.01, 0.05, 0.1]} gs GridSearchCV(lasso_pipe, param_grid, cv5, scoringneg_mean_squared_error) gs.fit(X_train, y_train) print(gs.best_params_)Lasso在这个项目里给我的最大启发是它经常把mom_5和mom_20的系数压到0只保留mom_10和vol_20。这个结果跟直觉是吻合的5日和20日动量对次日的预测能力本来就弱保留一个中周期的动量就足够。这种模型自己做出的特征取舍比手动删特征来得更客观。4.4 转为分类问题Logistic回归验证方向预测回归模型的R²太低很多人会心慌。换个角度我们真正关心的是方向——预测明天涨还是跌。把目标从连续收益率变成二元变量1表示次日收益率为正0为负用Logistic回归重新训练同一组特征from sklearn.linear_model import LogisticRegression df_model[target_dir] (df_model[target] 0).astype(int) y_dir df_model[target_dir] logreg Pipeline([ (scaler, StandardScaler()), (lr, LogisticRegression(max_iter1000)) ]) split_idx_c int(len(df_model) * 0.8) X_train_c, X_test_c X.iloc[:split_idx_c], X.iloc[split_idx_c:] y_train_c, y_test_c y_dir.iloc[:split_idx_c], y_dir.iloc[split_idx_c:] logreg.fit(X_train_c, y_train_c) y_pred_class logreg.predict(X_test_c) from sklearn.metrics import accuracy_score, precision_score, recall_score, confusion_matrix print(fAccuracy: {accuracy_score(y_test_c, y_pred_class):.4f}) print(fPrecision: {precision_score(y_test_c, y_pred_class):.4f}) print(fRecall: {recall_score(y_test_c, y_pred_class):.4f}) print(confusion_matrix(y_test_c, y_pred_class))注意这里的评估基准不是50%。AAPL长期是上涨的样本里次日上涨的比例本身可能在53%-55%左右所以模型如果准确率只有52%实际上是输给了无脑做多的基准。金融量化里评估模型一定要高于基准才有意义这一点太多人忽略。4.5 模型效果横向对比把四个模型的结果汇总模型样本外RMSE样本外R²方向准确率稳定性评价OLS0.01730.00851.8%系数随区间波动大Ridge (alpha1.0)0.01740.00652.1%系数稳定解释性强Lasso (alpha0.005)0.01720.01052.3%自动剔除冗余特征Logistic——52.9%方向预测略强于随机基准这几组数字在实盘上是不能直接赚钱的但它们的学术研究价值在于第一验证了AAPL的日收益率在线性框架下存在微弱但可检测的规律第二验证了正则化在金融数据上的价值——系数稳定性这一点在滚动重训场景下特别宝贵。5. 回测评估R²高不代表能赚钱5.1 时间序列交叉验证乱用K折会出事机器学习标准流程里K折交叉验证是最常用的评估手段。但在金融时间序列上直接用它等于让模型偷看未来。比如第3折的训练集包含了第2折之后的数据你相当于在预期未来样本的前提下训练模型得到的评估指标会虚高。正确的方式是Walk-Forward也叫时间序列展开验证。sklearn提供了TimeSeriesSplit它保证训练集永远在验证集之前from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5) for fold, (train_idx, val_idx) in enumerate(tscv.split(X)): X_tr, X_va X.iloc[train_idx], X.iloc[val_idx] y_tr, y_va y.iloc[train_idx], y.iloc[val_idx] pipeline Pipeline([ (scaler, StandardScaler()), (ridge, Ridge(alpha1.0)) ]) pipeline.fit(X_tr, y_tr) y_pred pipeline.predict(X_va) print(fFold {fold1}: R² {r2_score(y_va, y_pred):.4f}, RMSE {mean_squared_error(y_va, y_pred, squaredFalse):.4f})跑出来的结果往往比单次划分更真实。我第一次做的时候发现前几折R²有0.02后几折掉到接近0说明模型的解释力在不同市场状态下差异很大这不是过拟合而是市场结构本身发生了变化。5.2 换手率与交易成本的现实约束很多新手只会看准确率忽略了交易成本。量化策略里有一个残酷事实哪怕模型方向准确率到了55%如果每天调仓光交易成本就能吃掉大部分毛利。以AAPL为例如果按每次调仓双向万分之五的佣金加滑点估算一年250个交易日每天换仓一次的成本就是12.5%的年化损耗。你的模型年均超额收益如果能做到5%就不错了扣掉成本反而亏损。所以评估模型时必须考虑实际约束策略是每日调仓还是每5日调仓持仓是满仓还是半仓这些参数直接决定模型的经济价值。我在这个研究里做了一个简化估算假设每月调仓一次每次交易成本0.1%。基于Logistic回归方向信号的策略其年化超额收益在扣完成本后基本是抹平的。结论很实在线性模型在AAPL上的方向信号做研究可以直接实盘还远远不够。这话我必须说在前面避免你读完文章之后产生不切实际的期待。5.3 预测图里到底能看出什么最后把预测结果可视化import matplotlib.pyplot as plt test_dates df_model.index[split_idx:] fig, ax plt.subplots(figsize(12, 5)) ax.plot(test_dates, y_test.values, label实际收益率, alpha0.7) ax.plot(test_dates, y_pred_ridge, labelRidge预测, alpha0.7) ax.legend() plt.tight_layout() plt.show()这类预测图几乎看不出什么名堂因为预测序列基本贴着0轴上下极小幅波动而实际收益率毛刺很大。这不是画图的问题而是模型的预测方差相对真实波动来说太小。所以如果你看到有人晒股票预测图预测线跟实际K线拟合得严丝合缝那基本可以断定他用了未来函数或者直接把训练数据里的标签放进了测试集。真正值得看的是预测值与实际值的散点图。横轴是实际收益率纵轴是预测收益率如果模型有信息量散点会呈现轻微的正相关倾斜如果模型没信息散点就是一坨圆形噪声。AAPL这个项目里散点的相关系数大概在0.1左右说明只解释了1%的方差——这就是线性模型的诚实表现。6. 实战踩坑记录与最终结论6.1 未来函数一个变量名引发的虚假繁荣这个项目踩过最典型的坑是未来函数。我第一次构造特征时把当日收盘价直接用作预测次日的信号而收盘价本身实际上是当日数据。听起来没错问题出在我用当日收盘价算的动量因子在回测中会被错误地当作当日开盘前就能知道的信息这在现实中完全不可能。这类错误的隐蔽性在于训练时你根本无感知——R²看起来比最终结果好很多方向准确率能到57%你会误以为自己发现了圣杯。直到我在滚动回测里发现首日预测完全无法复现才排查出问题。检查方法很简单在特征构造代码里打印时间戳看特征值实际对应的是哪个交易日。更系统的做法是在回测循环里强制使用前向填充并移除所有使用了当日收盘价的列。6.2 模型衰减参数在不同市场状态下不稳定上面说到动量因子的系数符号在不同时间段会变化这个现象的专业说法是参数漂移。AAPL从2015到2024年走过了多轮风格迥异的行情。每一轮行情中的量价关系都在变一个在特定年份训练出来的系数换一段行情可能就失效了。应对措施也是量化的常规操作定期滚动重训而不是训练一次用到天荒地老。我在实验里做了滚动窗口训练每60个交易日用最新数据重训一次模型。重训之后模型的样本外表现比固定模型稳定不少这本身就是从业者该有的运维习惯。6.3 线性模型在AAPL上的边界判断把话说透在这十年AAPL数据上线性模型的样本外方向准确率大约在52%-53%R²在1%上下浮动。这个成绩单看起来寒酸但这才是金融数据里正常且诚实的水平。线性模型的价值不在于赚大钱而在于作为基准线它给了你一个清晰的底数。任何宣称在同样数据上能稳定赚大钱的模型理应能打败这个底数如果打败不了那它只是在过拟合噪声。从特征重要性的角度这个实验给我最深的体会是中周期动量10日和波动率状态20日标准差是AAPL量价数据里最值得保留的两个线性信号短周期动量基本没有增量信息。这个结论不见得能迁移到其他股票但提供了一个研究框架——先跑线性模型再谈复杂模型。最后说一下环境依赖和复现顺序。整个项目从yfinance拉数据到最终评估按第2节到第5节的顺序跑下来大约半小时能出全部结果。如果你用了我给的代码出现版本兼容问题优先检查statsmodels和scikit-learn的版本这两个库的API在近期版本里有不少调整。我在实际环境中踩过因为statsmodels版本过新导致summary()里部分字段显示异常的坑降级到0.14.x就正常了。量化研究的本质是证伪而不是证明。线性模型这次在AAPL上没有给出让人惊喜的预测力但它诚实地展示了哪些信息可用、哪些信息只是噪声。下一次我会在同样的特征框架下试试树模型和梯度提升用非线性方法跟这个线性底数做对照——那会是另一个有意思的话题。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进