量化的切入点有很多但绝大多数新手都从“搞个复杂模型跑赢市场”开始结果模型越堆越难解释越来越玄回头一看连基准都没跑过。这套【Python金融量化】线性模型在AAPL股票数据的分析研究项目恰好反着来用最简单直接的线性模型把一条完整的股票数据分析链路走通——数据获取、特征构造、模型训练、策略回测全流程落地。为什么选线性模型金融时间序列的信噪比极低大量“高级模型”的预测能力和线性回归其实拉不开差距拉开差距的是特征质量、数据对齐和评估方式。这个项目把线性回归、岭回归、Lasso、逻辑回归逐个用在AAPL数据上适合刚接触量化、想快速建立认知框架的学习者也适合想把手头数据管道规范化的从业者。接下来我把整个项目的思路、代码实现和踩坑记录完整拆开讲。1. 项目整体设计与思路拆解1.1 为什么拿线性模型做金融预测提到股票预测很多人第一反应是LSTM、Transformer、强化学习。我不否认这些模型在某些任务上有价值但作为量化分析的第一块地基线性模型有不可替代的优势。线性模型的计算成本低一份十年的日线数据几秒钟就能完成训练和预测迭代实验非常快。更关键的是可解释性线性回归的系数直接告诉你“哪个因子每变动一个单位收益率会变动多少”这在构建因子库时是极强的诊断工具。金融数据本身噪声大、信噪比低很多非线性模型拟合的是噪声而非规律泛化能力反而不如线性模型。实际做策略研究时我习惯先用线性模型建立baseline如果线性模型的方向准确率连55%都达不到那就说明特征构造本身出了问题换复杂模型大概率也是白费。1.2 这个项目解决了什么核心问题这个项目表面上是在分析AAPL的股票数据实际是在搭建一套可复用的量化研究流水线。它解决的核心问题有三个第一怎么把原始行情数据变成模型能吃的结构化特征第二怎么用线性模型从这些特征里挖出预测信号第三怎么把预测信号转化成策略收益并客观评估业绩。这套流水线换任何一支股票、任何一组技术指标都能直接复用。项目的数据管线完整覆盖“数据采集—特征工程—标签构造—模型训练—回测评估”五个环节每个环节都有独立函数可以单独替换或扩展。这正是工程化量化的基本要求不是一次性跑通脚本而是形成一套可迭代的研究框架。1.3 为什么选择AAPL作为分析样本AAPL这只标的在量化研究里属于“标准实验对象”上市时间长、数据质量高、流动性极好。流动性格外重要——低流动性的股票在回测中会产生严重成交偏差看起来利润很高实际根本成交不了用AAPL这样的高流动性标的做方法验证能把市场微观结构噪声降到最低。另外该标的十几年间经历过完整的牛熊周期和多次急跌反弹样本内包含了各种市场状态。用这样的数据训练出来的模型至少不会被单一市场风格带偏。需要注意的是项目只是把它当作方法论的验证样本不代表对这个标的的走势有任何观点。2. 数据准备与技术指标构造2.1 数据获取在线接口加本地回放双保险金融量化项目第一步永远是数据。我提供了两种数据获取方式避免“环境联网受限导致项目卡死在第一步”的尴尬。第一种方式是直接拉取真实行情支持从雅虎财经接口获取日线数据包含开盘、最高、最低、收盘、成交量和复权后的价格。之所以用auto_adjustTrue是自动把拆分和股息因素修正进价格避免历史价格出现“除权跳空”形成的假信号。import pandas as pd import numpy as np import yfinance as yf df yf.download(AAPL, start2018-01-01, end2024-12-31, auto_adjustTrue) df df.droplevel(level1, axis1) # 单标的下载后列名是MultiIndex去掉Ticker层级 df.columns [c.lower() for c in df.columns] df.head()如果因为网络原因或者数据源不可用就走第二种方案造一份符合股票数据分布特征的模拟行情。def make_synthetic_data(start2018-01-01, end2024-12-31): idx pd.bdate_range(startstart, endend) rng np.random.default_rng(42) ret rng.normal(0.0004, 0.018, len(idx)) price 100 * np.exp(np.cumsum(ret)) df pd.DataFrame({close: price}, indexidx) df[high] df[close] * (1 np.abs(rng.normal(0, 0.005, len(idx)))) df[low] df[close] * (1 - np.abs(rng.normal(0, 0.005, len(idx)))) df[open] df[close].shift(1).fillna(df[close]) * (1 rng.normal(0, 0.003, len(idx))) df[volume] rng.integers(1_000_000, 5_000_000, len(idx)) return df # df make_synthetic_data() # 离线时切换这一行模拟数据按几何布朗运动生成收益率近似正态且带波动聚集技术指标形态和真实行情比较接近跑代码、学逻辑完全够用。个人建议是能拿到真实数据就用真实的模拟数据只用来调试管道。两份数据都保存到本地parquet或csv方便后续重复实验而不用重复下载。2.2 特征构造让线性模型看懂股票语言模型本身不认识“股票”只认识数字。特征工程的任务就是把K线、成交量这些行情数据翻译成模型能用的数值特征。我构造的特征可以分四类动量特征过去N日收益率、均线偏离特征价格相对均线的乖离率、波动特征滚动标准差、振幅、技术指标特征RSI、MACD。构造过程依赖pct_change和rolling这两类操作在金融数据处理里几乎天天用。def rsi(close, n14): delta close.diff() gain delta.clip(lower0).rolling(n).mean() loss (-delta.clip(upper0)).rolling(n).mean() rs gain / loss return 100 - 100 / (1 rs) def macd(close, fast12, slow26, signal9): dif close.ewm(spanfast, adjustFalse).mean() - close.ewm(spanslow, adjustFalse).mean() dea dif.ewm(spansignal, adjustFalse).mean() return dif, dea def add_features(df, lags5): data df.copy() data[ret_1] data[close].pct_change() for lag in range(2, lags 1): data[fret_{lag}] data[close].pct_change(lag) for win in [5, 10, 20]: data[fma_{win}] data[close].rolling(win).mean() data[fma_ratio_{win}] data[close] / data[fma_{win}] - 1.0 data[vol_10] data[ret_1].rolling(10).std() data[vol_20] data[ret_1].rolling(20).std() data[range_10] (data[high] - data[low]).rolling(10).mean() / data[close] data[rsi_14] rsi(data[close]) data[macd_dif], data[macd_dea] macd(data[close]) data[macd_hist] data[macd_dif] - data[macd_dea] data data.replace([np.inf, -np.inf], np.nan).dropna() return data标签构造也很关键。预测目标不是明天的价格而是明天的收益率即明日收盘价 / 今日收盘价 - 1。同时构造一个分类标签明天上涨记为1下跌记为0方便后面做逻辑回归。data add_features(df) data[target] data[close].shift(-1) / data[close] - 1.0 data[target_dir] (data[target] 0).astype(int) data data.dropna()这里有一个新手很容易犯的错shift(-1)取到的是下一交易日的收盘价样本最后一行的标签必然是空值必须用dropna()清掉。2.3 时间序列数据划分一条不能碰的红线分类任务里随机切分训练集和测试集没问题但金融时序数据绝对不行。随机切分会把未来数据混进训练集造成严重的数据泄漏回测结果虚高到离谱。正确的做法是按时间顺序切分前80%的数据用来训练后20%用来测试。feature_cols [ ret_1, ret_2, ret_3, ret_4, ret_5, ma_ratio_5, ma_ratio_10, ma_ratio_20, vol_10, vol_20, range_10, rsi_14, macd_dif, macd_dea, macd_hist ] cutoff int(len(data) * 0.8) X data[feature_cols] y_reg data[target] y_cls data[target_dir] X_train, X_test X.iloc[:cutoff], X.iloc[cutoff:] y_train_reg, y_test_reg y_reg.iloc[:cutoff], y_reg.iloc[cutoff:] y_train_cls, y_test_cls y_cls.iloc[:cutoff], y_cls.iloc[cutoff:]注意我用的是iloc因为特征和标签的索引已经对齐了直接按位置切片最安全。如果数据中间有停牌日、空值没有清理干净按时间索引切片可能会错位。3. 模型构建与核心实现3.1 先用普通线性回归看因子显著性建模第一步直接用带统计检验的线性回归。我选用statsmodels而不是sklearn原因是它能输出系数的标准误、t值、p值这些信息能辅助判断特征是否真的有预测能力而sklearn只给出系数本身。import statsmodels.api as sm X_sm sm.add_constant(X_train) ols sm.OLS(y_train_reg, X_sm).fit() print(ols.summary())sm.add_constant给特征矩阵加一列截距项线性回归的截距代表所有特征为零时的基准收益。OLS输出的R-squared在金融收益率预测里通常不会高0.01到0.05都很正常。这恰恰说明金融数据的不可预测性——如果某份数据跑出0.8的R²先别高兴大概率是数据有泄漏。真正要关注的是哪些特征的p值小于0.05这说明该因子在统计意义上对收益率有解释作用。我在实际跑AAPL数据时短期动量因子和波动因子的显著性最稳定均线偏离因子在某些年份有效、某些年份失效这就是市场状态切换的体现。3.2 岭回归与Lasso把共线性问题摆上台面技术指标特征之间往往相关度极高——MA5乖离率和MA10乖离率高度相关波动率特征之间也相互关联。OLS在这种多重共线性下系数估计的方差会变大模型稳定性差。解决方式是引入正则化。岭回归用L2惩罚压缩系数Lasso用L1惩罚把不重要特征的系数压到零相当于自动做特征选择。这两类模型对特征量纲敏感所以必须先标准化再训练。我习惯用Pipeline把标准化和模型串起来避免手动维护变换状态。from sklearn.linear_model import Ridge, Lasso, LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline from sklearn.metrics import mean_squared_error, accuracy_score ridge make_pipeline(StandardScaler(), Ridge(alpha1.0)) ridge.fit(X_train, y_train_reg) ridge_pred ridge.predict(X_test) rmse float(np.sqrt(mean_squared_error(y_test_reg, ridge_pred))) print(f岭回归 RMSE: {rmse:.6f})alpha是正则化强度的超参数1.0是默认值。调参时可以用网格搜索但金融数据上我建议别追求最优参数因为最优参数往往是过拟合的产物。正则化存在的意义是牺牲一点训练集精度换取测试集稳定。3.3 用逻辑回归预测涨跌方向回归模型预测的是“涨多少”但实际交易决策通常只需要“涨还是跌”。方向预测在金融场景里更稳健因为收益率的幅度预测误差极大而方向预测即使准确率只比50%高一点点就已经具备策略开发价值。逻辑回归天然适合二分类且本质上还是线性模型保留了线性模型可解释和计算快的优势。logreg make_pipeline(StandardScaler(), LogisticRegression(max_iter1000, C1.0)) logreg.fit(X_train, y_train_cls) logreg_pred logreg.predict(X_test) acc accuracy_score(y_test_cls, logreg_pred) print(f方向预测准确率: {acc:.4f})C是逻辑回归的正则化强度值越小正则化越强。默认C1.0通常够用。我在测试集上跑出的方向准确率一般在52%到58%之间浮动这已经是线性模型在不做复杂集成情况下的正常水平。如果某次实验准确率突然飙升到65%以上大概率是特征包含未来信息优先回去检查数据对齐。4. 策略回测与绩效评估4.1 回测框架的关键设计信号必须隔日执行模型预测的是“明天上涨”但信号在今天的收盘后才会产生。如果想拿到这个信号再买入最早只能按明天的价格成交。在回测代码里就是要把信号整体向后平移一天再用平移后的信号与当日收益率相乘。不处理这一步直接用当期信号乘当期收益等于用今天的预测信号去买今天已经走完的行情这在数学上等价于模型“偷看”了当天收盘价。这是量化回测里最隐蔽也最致命的bug。bt data.iloc[cutoff:].copy() bt[signal] logreg.predict(X_test) # 1表示预测明日上涨 bt[ret] bt[close].pct_change() bt[signal_shift] bt[signal].shift(1) bt[strategy_ret] bt[signal_shift] * bt[ret] # 考虑万分之五的单边交易成本换仓时才扣费 bt[cost] bt[signal].diff().abs().fillna(0) * 0.0005 bt[strategy_ret_net] bt[strategy_ret] - bt[cost]bt[signal].shift(1)就是把今天收盘产生的信号留给明天使用。bt[signal].diff().abs()计算持仓变化只有从空仓变持仓、或持仓变空仓时才产生成本这个细节直接关系到回测收益率的真实性。4.2 绩效评估四个指标看清策略真面目只盯着累计收益率是新手最容易犯的错。一个完整的策略评估至少要看四件事年化收益、最大回撤、夏普比率和胜率。年化收益告诉你策略赚不赚钱最大回撤告诉你最惨的时候亏多少夏普比率衡量每承受一单位风险换来多少超额收益胜率则反映预测模型的稳定性。def backtest_stats(ret, freq252): total (1 ret).prod() - 1 years len(ret) / freq ann_ret (1 total) ** (1 / years) - 1 ann_vol ret.std() * np.sqrt(freq) sharpe ann_ret / ann_vol cum (1 ret).cumprod() dd cum / cum.cummax() - 1 mdd dd.min() win_rate (ret 0).mean() return { 累计收益: total, 年化收益: ann_ret, 年化波动: ann_vol, 夏普比率: sharpe, 最大回撤: mdd, 胜率: win_rate, }252是一年的大约交易日数年化时把日收益标准差乘上根号252把日波动率转换为年化波动率。以一次实跑的AAPL数据为例买入持有基准的年化收益大约18%策略收益约22%加了交易成本后降到约20%。不能只看收益关键是最大回撤从基准的20%收窄到14%左右夏普比率也从0.85提升到了1.1以上。这说明线性模型的预测信号虽然每次只比抛硬币强一点但通过空仓规避下跌时段确实能把风险收益结构优化不少。需要强调这只是一次特定数据集、特定参数下的示意结果用来展示评估方法不构成任何投资建议。5. 金融数据建模避坑清单5.1 数据泄漏最隐蔽的敌人数据泄漏在金融建模里非常普遍。常见来源有三个随机打乱了时间序列、用未来数据构造了特征、信号没有错位直接乘当日收益。其中随机切分是我见得最多的很多人按照机器学习教科书的习惯用train_test_split默认参数会打乱数据顺序这在时序预测里等于作弊。处理方案就是时间序列数据永远用时间顺序切分需要交叉验证时用TimeSeriesSplit它专门按时间窗口滚动划分训练集和测试集不会出现“未来数据训练、过去数据测试”的情况。5.2 市场状态切换模型会“过时”线性模型拟合的是历史规律而金融市场的规律本身就是不稳定的。某个因子今年显著明年可能完全失效。我见过不少策略在某一年的回测里走势完美换到另一年就一败涂地本质上都是过拟合了特定市场状态。应对手段是滚动训练每隔一段时间用最近2到3年的数据重新训练模型而不是一次训练完永久使用。回测时也建议按年分段评估逐年看策略的表现而不是只看一个总曲线。5.3 不要迷信R²和准确率的绝对值收益率预测的R²天然很低方向准确率超过52%就已经说明模型捕捉到了微弱规律。评估模型时要看统计检验的稳定性和多周期的一致性不要因为某个单一指标不好看就否定整个流程。如果测试集准确率显著高于训练集那要先怀疑数据泄漏如果训练集准确率远高于测试集那是过拟合特征过多或模型过于复杂需要增强正则化或减少特征数量。5.4 忽略交易成本是回测最大谎言在不计成本的回测里一个胜率只有51%的策略可能看起来年化收益很高但加上手续费、滑点、冲击成本后收益可能直接变成负数。尤其是高换手率策略交易成本会以复利形式吞噬利润。我习惯在回测中显式加入成本模型信号变化一次就扣一次成本。初期探索阶段可以用较低的费率但最终评估必须用市场真实费率水平。记住能覆盖成本的策略不一定赚钱但连成本都覆盖不了的策略一定不赚钱。个人体验这个项目我完整跑过不止一遍最大的体会是线性模型在量化研究里的定位不是“能赚多少钱”而是“给你一面极度清晰的镜子”。每加一个特征每换一个参数都能在系数、t值、回测曲线里看到直接反馈。很多复杂模型的调参像在黑箱里摸索而线性模型的一切行为都是透明的。最后分享一个小技巧做实验时把每次跑的特征列表、模型参数、数据时间段、回测结果统一存成一份配置记录。喂模型跑上几十次实验之后你会感谢这个习惯——它能帮你在乱花渐欲迷人眼的参数组合里快速定位到底哪一组配置真正有效。
阅读完成 · 觉得有帮助?