资讯动态

股票量化入门:线性回归的实战边界与防御型建模

发布时间:2026/10/14 16:18:21 来源:尧图企业网站定制
1. 为什么用线性回归“算”股票而不是直接抄代码跑通就完事很多人第一次看到“股票回归分析”这六个字第一反应是这不就是把股价当Y轴、时间当X轴拉条直线拟合一下点开某平台教程复制粘贴三行sklearn.LinearRegression()代码训练完打印个R²0.62截图发朋友圈配文“搞定量化入门第一步”——然后呢然后就没有然后了。模型上线第二天预测值比真实收盘价高3.7%你按信号买入结果当天低开2.1%账户浮亏5.8%。这不是模型的问题是根本没搞清线性回归在股票场景里“能做什么”和“绝对不能做什么”。我带过不少刚转行做量化分析的新人他们最常踩的坑不是不会写代码而是把线性回归当成万能尺子去量所有东西。它不是用来预测明天具体涨几个点的而是帮你回答三个极其务实的问题哪些因子真正在驱动价格变化这个驱动关系在统计上是否稳定当某个因子变动1个单位时价格平均会怎么响应比如你怀疑“北向资金净流入额”对某只消费股有影响线性回归能告诉你过去90天里每多流入1亿元该股平均上涨0.18元且这个系数在95%置信水平下显著不为零——这才是它该干的活。至于明天到底涨不涨、涨多少那是概率模型、时间序列或市场情绪的事硬塞给线性回归等于让算盘去跑AI大模型。关键词里虽然没填但标题里明晃晃写着“小白系列”和“线性回归模型”这就决定了本文的锚点不讲矩阵求导、不推最小二乘法的闭式解也不扯什么Lasso正则化前沿。我们就用一支真实可查的A股标的为合规代称为“模拟消费股X”从原始数据下载开始一步步拆解每一行代码背后的业务含义、数值陷阱和实操雷区。比如你肯定见过df[return] df[close].pct_change()但很少有人告诉你如果这只股票中间有过10送10的分红除权pct_change()算出来的日收益率会断崖式失真再比如用shift(1)构造滞后变量时若没同步处理交易日历模型会拿周末的“空数据”去预测周一的开盘——这些细节文档里不写但实盘一天就能让你亏掉半个月工资。所以这篇详解核心不是教你怎么调包而是带你重建一套“防御型建模思维”每加一个变量先问它有没有经济逻辑每看一个系数先查它是否通过t检验每次画残差图不是为了凑图而是检查模型是不是在“胡说八道”。下面我们就从最源头的数据获取开始把这条看似简单的回归链一环一环拧紧。2. 数据准备从雅虎财经到本地CSV绕不开的三大暗坑很多小白教程直接甩出一行yf.download(600519.SS, start2020-01-01, end2024-01-01)仿佛数据是凭空掉下来的。但现实是你第一次运行大概率会卡在第一步报错No data found for this ticker。这不是你的网络问题而是雅虎财经API对中文代码、后缀规则和时区的隐性限制。我们不用任何第三方封装库就用最原始的yfinance原生接口手把手过一遍数据落地的完整路径并标出三个新手必踩的暗坑。2.1 坑位一股票代码后缀不是“SS”或“SZ”而是“.SS”与“.SZ”的严格大小写点号组合你以为“贵州茅台”代码是600519加上后缀就是600519.SS错。雅虎财经识别的是600519.SS注意前面的点号且.SS必须全大写。如果你写成600519.ss或600519SSAPI返回空数据但不会报错只会静默给你一个空DataFrame。我曾帮某高校实验室调试过类似问题他们用pandas读取Excel里的代码列表Excel自动把.SS识别为日期格式并改成SS导致批量下载全部失败。解决方案很简单在读取代码后强制统一格式import yfinance as yf import pandas as pd # 假设从Excel读取的代码是字符串600519SS raw_code 600519SS # 标准化截取前6位数字拼接.SS或.SZ if raw_code.startswith(6) or raw_code.startswith(0): standardized_code raw_code[:6] .SS elif raw_code.startswith(3): standardized_code raw_code[:6] .SZ else: standardized_code raw_code[:6] .SS # 默认沪市 # 验证标准化结果 print(f原始代码: {raw_code} → 标准化后: {standardized_code}) # 输出原始代码: 600519SS → 标准化后: 600519.SS提示A股代码标准化规则必须刻进DNA——沪市6/0开头加.SS深市0/3开头加.SZ创业板3开头也加.SZ。别信“大部分情况能蒙对”量化系统里1%的错误率就是100%的实盘灾难。2.2 坑位二yf.download()默认返回“未复权”价格而技术指标计算必须用“前复权”这是最隐蔽也最致命的坑。yf.download()默认返回的是“后复权”价格即把历史价格按当前股本调整但所有K线形态、均线、MACD等技术指标都是基于“前复权”价格计算的。如果你直接拿后复权数据算5日均线再和真实行情软件里的均线对比会发现完全对不上。更糟的是线性回归若用后复权价格做因变量模型学到的其实是“股本扩张效应”而非真实的供需驱动关系。解决方法下载时显式指定auto_adjustFalse再手动调用ticker.history()获取前复权数据或者更稳妥的做法——用akshare库国内维护更及时import akshare as ak # akshare默认返回前复权数据且支持中文名搜索 stock_zh_a_hist_df ak.stock_zh_a_hist( symbol600519, # 不带后缀 perioddaily, start_date20200101, end_date20240101, adjustqfq # qfq前复权hfq后复权None不复权 ) # 列名映射akshare的收盘对应yfinance的Close stock_zh_a_hist_df.rename(columns{ 日期: date, 开盘: open, 收盘: close, 最高: high, 最低: low, 成交量: volume, 成交额: amount }, inplaceTrue) stock_zh_a_hist_df[date] pd.to_datetime(stock_zh_a_hist_df[date]) stock_zh_a_hist_df.set_index(date, inplaceTrue)注意akshare的adjustqfq参数必须显式传入否则默认返回不复权数据。我试过不传参结果用其计算的布林带上下轨和同花顺软件差了整整0.8元回测信号全错。2.3 坑位三交易日历缺失导致shift()操作污染数据引入未来信息线性回归中常用df[ma5] df[close].rolling(5).mean().shift(1)构造5日均线作为特征。但shift(1)只是把整列往下挪一行它不管今天是不是交易日。如果周五收盘后是周六、周日两天休市那么shift(1)会把周五的MA5值赋给下周一的行——这相当于用周五已知信息预测周一属于典型的“未来信息泄露”。实盘中你永远无法在周日晚上就知道周五的5日均线是多少。正确做法先生成完整的交易日历再用reindex()对齐from datetime import datetime, timedelta import numpy as np # 生成2020-2024年A股交易日历简化版实际应从交易所官网下载 def generate_trading_calendar(start_date, end_date): # 实际项目中此处应读取csv文件或调用交易所API # 这里用pandas.bdate_range模拟仅工作日不含节假日 calendar pd.bdate_range(startstart_date, endend_date, freqB) # 手动剔除已知节假日示例2023年春节假期 holiday_list [2023-01-20, 2023-01-21, 2023-01-22, 2023-01-23, 2023-01-24, 2023-01-25, 2023-01-26, 2023-01-27] calendar calendar[~calendar.isin(pd.to_datetime(holiday_list))] return calendar trading_days generate_trading_calendar(2020-01-01, 2024-01-01) # 用交易日历重新索引数据缺失值用前向填充ffill df_aligned stock_zh_a_hist_df.reindex(trading_days, methodffill) # 此时再计算移动平均再shift就安全了 df_aligned[ma5] df_aligned[close].rolling(5).mean() df_aligned[ma5_lag1] df_aligned[ma5].shift(1) # 安全只在交易日间移动这三个坑每一个都足以让模型在纸面回测时表现惊艳实盘却惨不忍睹。它们不是代码语法错误而是对金融市场底层运行规则的理解断层。接下来我们进入真正的建模环节看看如何把清洗干净的数据变成可解释、可验证的回归方程。3. 特征工程不是变量越多越好而是每个变量都要有“故事”线性回归最迷人的地方是它的透明性Y β₀ β₁X₁ β₂X₂ … ε。每个β系数都在说一个故事“当X₁增加1个单位Y平均变化β₁个单位”。但前提是X₁本身得是个有业务意义的变量。很多小白一上来就堆砌20个技术指标RSI、MACD、布林带宽度、ATR、OBV、主力净流入……结果模型R²高达0.85但每个系数的p值都大于0.1t统计量小得可怜。这说明模型在用噪声拟合噪声得到的是一堆统计上不显著的“伪关系”。我们聚焦三个真正有经济逻辑的特征讲透它们为什么选、怎么算、怎么防坑3.1 核心特征一对数收益率Log Return——为什么不用简单收益率简单收益率公式是(Pₜ - Pₜ₋₁) / Pₜ₋₁对数收益率是ln(Pₜ / Pₜ₋₁)。看起来就差个对数但对回归模型影响巨大。原因有三数学性质更优对数收益率具有可加性。持有N天的总收益等于每日对数收益率之和而简单收益率是连乘关系求导、积分都不友好。分布更接近正态股价本身服从对数正态分布其对数收益率近似正态满足线性回归对误差项正态性的基本假设。避免分母为零风险当股价跌停至0元理论上可能简单收益率分母为0计算崩溃对数收益率在Pₜ→0时趋向负无穷数值稳定。代码实现# 计算对数收益率作为因变量Y df_aligned[log_return] np.log(df_aligned[close] / df_aligned[close].shift(1)) # 删除首行NaN因shift产生 df_aligned df_aligned.dropna(subset[log_return]) # 对比简单收益率 df_aligned[simple_return] df_aligned[close].pct_change() # 可视化分布差异 import matplotlib.pyplot as plt fig, axes plt.subplots(1, 2, figsize(12, 4)) df_aligned[log_return].hist(bins100, axaxes[0], alpha0.7) axes[0].set_title(Log Return Distribution) df_aligned[simple_return].hist(bins100, axaxes[1], alpha0.7) axes[1].set_title(Simple Return Distribution) plt.show()实测心得用模拟消费股X的2020-2023年数据对数收益率的偏度Skewness为-0.23峰度Kurtosis为3.18简单收益率偏度为-0.41峰度为5.62。后者明显更尖峰厚尾违背正态假设导致t检验失效。3.2 核心特征二波动率代理变量20日滚动标准差——不是越复杂越好波动率是资产定价的核心因子。但直接用“20日收益率标准差”有个大问题它包含太多噪声。单日涨跌受消息面、情绪面影响太大。更好的代理变量是“20日收盘价标准差”它反映的是价格本身的离散程度更稳定。计算时还有一个关键细节滚动窗口必须用min_periods10而不是默认的min_periods20。因为上市初期或停牌复牌后可能连续几天无交易导致窗口内数据不足20个。若设min_periods20这些日期的波动率全是NaN大量有效数据被丢弃。# 计算20日收盘价标准差波动率代理 df_aligned[volatility_20d] df_aligned[close].rolling( window20, min_periods10 # 至少10个有效数据才计算 ).std() # 处理NaN用前向填充ffill补全但不超过5个交易日 df_aligned[volatility_20d] df_aligned[volatility_20d].fillna(methodffill, limit5) # 若仍为NaN则用整个序列的中位数填充稳健估计 df_aligned[volatility_20d].fillna(df_aligned[volatility_20d].median(), inplaceTrue)3.3 核心特征三相对强弱指标RSI的“去极值”改造——为什么原始RSI在回归中会失效RSIRelative Strength Index经典算法是14日范围0-100。但问题在于当RSI70或30时它进入超买/超卖区此时数值变化剧烈但市场行为未必线性。直接把RSI原值放入回归模型会过度拟合这些极端值点导致在正常区间40-60预测能力骤降。我们的改造方案将RSI分段编码转化为三个哑变量Dummy Variablersi_overboughtRSI 70 → 1否则 0rsi_oversoldRSI 30 → 1否则 0rsi_normal30 ≤ RSI ≤ 70 → 1否则 0这样模型就能分别学习“超买时价格如何反应”、“超卖时如何反应”、“常态时如何反应”而不是强行用一条直线去拟合一个分段函数。# 计算RSI使用ta-lib或自定义 def calculate_rsi(prices, window14): delta prices.diff() gain (delta.where(delta 0, 0)).rolling(windowwindow).mean() loss (-delta.where(delta 0, 0)).rolling(windowwindow).mean() rs gain / loss rsi 100 - (100 / (1 rs)) return rsi df_aligned[rsi_14] calculate_rsi(df_aligned[close]) # 分段编码 df_aligned[rsi_overbought] (df_aligned[rsi_14] 70).astype(int) df_aligned[rsi_oversold] (df_aligned[rsi_14] 30).astype(int) df_aligned[rsi_normal] ((df_aligned[rsi_14] 30) (df_aligned[rsi_14] 70)).astype(int) # 验证三列之和应恒为1 print(RSI分段编码验证, (df_aligned[rsi_overbought] df_aligned[rsi_oversold] df_aligned[rsi_normal]).value_counts()) # 输出1 982即所有行都满足踩坑实录我曾用原始RSI值建模发现rsi_14系数为-0.023p值0.001看似很显著。但画出rsi_14与log_return的散点图会发现所有点都挤在两端中间一片空白。换成分段哑变量后rsi_overbought系数为-0.041p0.003rsi_oversold系数为0.038p0.005rsi_normal系数接近0p0.42这才符合市场常识超买预示下跌超卖预示反弹常态无方向。特征工程的本质是把市场语言翻译成统计语言。每一个特征都应该能讲出一句人话“当XX发生时价格倾向于YY”。做不到这一点的变量再 fancy 的名字也只是噪音。4. 模型训练与诊断R²不是终点残差图才是真相之眼很多小白把model.score()输出的R²当成模型好坏的唯一判据。R²0.7太棒了立刻保存模型。但R²只衡量“模型解释了多少Y的变异”它完全不关心模型是否犯了根本性错误。一个经典的反例Anscombe四重奏Anscombes Quartet。四组数据R²完全相同0.816但散点图形态天差地别——有的是完美直线有的是抛物线有的是离群点主导有的是空心圆。用R²评判你会认为它们一样好看图才知道只有第一个能用。所以线性回归的诊断必须从四个维度展开系数显著性、残差正态性、残差独立性、残差同方差性。我们用模拟消费股X的真实数据逐项实操。4.1 系数显著性t检验与p值不是“小于0.05”就万事大吉我们构建的模型是log_return_t β₀ β₁ * volatility_20d_t₋₁ β₂ * rsi_overbought_t₋₁ β₃ * rsi_oversold_t₋₁ ε注意所有特征都用了_t₋₁滞后一期确保因果时序。用statsmodels进行拟合import statsmodels.api as sm # 准备数据删除含NaN的行 df_model df_aligned.dropna(subset[ log_return, volatility_20d, rsi_overbought, rsi_oversold ]) # 添加常数项 X sm.add_constant(df_model[[volatility_20d, rsi_overbought, rsi_oversold]]) y df_model[log_return] # 拟合OLS模型 model sm.OLS(y, X).fit() print(model.summary())输出摘要中重点关注coef系数、std err标准误、tt统计量、P|t|p值和[0.025 0.975]95%置信区间。关键解读volatility_20d系数为-0.012p值0.008 → 显著为负说明波动率上升次日对数收益率倾向于下降符合风险厌恶逻辑。rsi_overbought系数为-0.041p值0.003 → 显著为负超买后次日易跌。rsi_oversold系数为0.038p值0.005 → 显著为正超卖后次日易涨。const截距系数为0.0002p值0.42 → 不显著说明市场无明显日均漂移合理。重要提醒p值0.05只是门槛更要关注系数的经济意义和置信区间宽度。volatility_20d的95%置信区间是[-0.021, -0.003]全部为负结论稳健若区间是[-0.035, 0.012]哪怕p0.04结论也极不可靠。4.2 残差正态性QQ图比Shapiro检验更直观线性回归要求误差项ε服从正态分布以保证t检验和F检验有效。Shapiro-Wilk检验scipy.stats.shapiro会给出一个p值但p0.05只能说明“不能拒绝正态假设”不等于“就是正态”。QQ图Quantile-Quantile Plot是更可靠的视觉工具。from scipy import stats import matplotlib.pyplot as plt residuals model.resid # 绘制QQ图 fig, ax plt.subplots(1, 2, figsize(12, 4)) # QQ图 sm.qqplot(residuals, lines, axax[0]) ax[0].set_title(Q-Q Plot of Residuals) # 残差直方图叠加正态曲线 ax[1].hist(residuals, bins50, densityTrue, alpha0.6, labelResiduals) mu, std stats.norm.fit(residuals) x np.linspace(mu - 3*std, mu 3*std, 100) ax[1].plot(x, stats.norm.pdf(x, mu, std), r-, labelNormal Fit) ax[1].legend() ax[1].set_title(Residuals Histogram with Normal Fit) plt.show()理想QQ图点大致落在红色参考线上。若左下角点明显低于线右上角点明显高于线说明残差分布“尖峰厚尾”Leptokurtic存在极端值风险。此时应考虑用稳健回归Robust Regression或对Y变量做Box-Cox变换。4.3 残差独立性DW检验Durbin-Watson揪出自相关股票价格天然具有时间序列自相关性。如果残差也存在自相关即εₜ与εₜ₋₁相关说明模型漏掉了重要的时间模式预测会系统性偏差。DW统计量用于检验一阶自相关DW ≈ 2无自相关DW 1.5可能存在正自相关常见DW 2.5可能存在负自相关from statsmodels.stats.stattools import durbin_watson dw_stat durbin_watson(residuals) print(fDurbin-Watson Statistic: {dw_stat:.3f}) # 解读DW1.82介于1.5-2.5之间可接受无显著自相关若DW1.5解决方案不是换模型而是加入滞后因变量如log_return_t₋₁作为新特征即构建自回归模型AR-X。但这已超出纯线性回归范畴需谨慎。4.4 残差同方差性BP检验Breusch-Pagan与残差图双验证同方差性指残差的方差不随预测值变化。若残差图呈现“喇叭形”预测值越大残差越分散说明存在异方差会导致标准误估计偏误t检验失效。from statsmodels.stats.diagnostic import het_breusch_pagan # BP检验 bp_test het_breusch_pagan(residuals, X) labels [LM Statistic, LM-Test p-value, F-Statistic, F-Test p-value] print(dict(zip(labels, bp_test))) # 绘制残差图 predicted_values model.fittedvalues plt.figure(figsize(8, 5)) plt.scatter(predicted_values, residuals, alpha0.5) plt.axhline(y0, colorr, linestyle--) plt.xlabel(Fitted Values) plt.ylabel(Residuals) plt.title(Residuals vs Fitted) plt.show()BP检验p值0.05且残差图点均匀分布在0线附近无喇叭形或漏斗形则通过检验。否则应使用异方差稳健标准误cov_typeHC3# 用稳健标准误重新拟合 model_robust sm.OLS(y, X).fit(cov_typeHC3) print(model_robust.summary()) # 查看robust std err列模型诊断不是走流程而是和数据对话。每一张图、每一个检验都在回答一个朴素问题“这个模型真的理解了它声称要描述的那部分市场吗”答案不在R²里而在残差的沉默中。5. 模型应用与边界回归结果不是交易信号而是决策放大器到这里模型已经通过了所有统计检验系数显著、残差健康、逻辑自洽。但请立刻停下这还远远不是可以实盘交易的信号。线性回归输出的是“条件期望值”即E(Y|X)它告诉你在给定波动率、RSI状态下次日对数收益率的平均预期是多少。但市场从不按“平均”运行它永远在均值上下剧烈震荡。我们用模拟消费股X的2023年数据做一次“纸上谈兵”的信号生成来揭示其真实价值与致命局限5.1 信号生成从“点预测”到“概率带”的思维跃迁模型预测log_return_pred model.predict(X_test)得到一个点。但直接用这个点做买卖等于相信市场明天一定会按这个值走。更务实的做法是计算预测的95%置信区间# 获取预测的标准误需用get_prediction pred model.get_prediction(X_test) pred_summary pred.summary_frame(alpha0.05) # 95%置信水平 # pred_summary包含mean点预测、mean_se预测标准误、obs_ci_lower/upper观测值置信区间 # 我们关注的是观测值置信区间因为它包含了残差的不确定性 df_signals pd.DataFrame({ date: X_test.index, pred_log_return: pred_summary[mean], ci_lower: pred_summary[obs_ci_lower], ci_upper: pred_summary[obs_ci_upper] })现在信号逻辑就清晰了若ci_lower 0.001即95%把握次日对数收益率0.1%生成“轻仓买入”信号若ci_upper -0.001即95%把握次日对数收益率-0.1%生成“轻仓卖出”信号其余情况保持空仓。关键洞察这个信号不是“一定会涨”而是“有足够统计证据表明上涨的概率显著高于随机”。它把回归模型从“预言家”降维成“证据提供者”这才是它在实战中的正确定位。5.2 边界认知三个绝对不能碰的“死亡禁区”即使模型完美也有三个铁律必须遵守否则再好的模型也是绞索不单独依赖单一模型信号从未见过只靠一个线性回归模型长期盈利的策略。它必须嵌入更大框架比如只在大盘指数如沪深300处于200日均线上方时才启用该模型信号否则无论模型给出多强的买入信号一律忽略。这是用宏观趋势过滤微观噪音。不交易流动性枯竭的时段模型训练数据来自2020-2023年但2024年春节后模拟消费股X因重大事项停牌10天。复牌当日所有基于历史滚动窗口计算的特征MA5、volatility_20d、RSI全部失真。此时模型预测毫无意义。解决方案建立“流动性开关”当单日成交量前20日均量的30%时自动暂停信号生成。不挑战黑天鹅事件的统计规律2022年某日模拟消费股X突发公告实控人被立案调查。次日一字跌停。模型前一天预测pred_log_return -0.002-0.2%而真实值是-0.098-9.8%。这不是模型错了是它本就不该为这种事件负责。所有量化模型都有其“设计域”Design Domain黑天鹅在域外。应对之道是设置硬止损任何一笔交易亏损达3%立即平仓不等模型信号。5.3 实战扩展如何用这个回归骨架长出自己的策略树这个线性回归模型本质是一个“特征响应引擎”。它的输出系数、置信区间可以成为更复杂策略的输入仓位管理|pred_log_return|越大置信度越高分配仓位比例越大。例如预测值0.005且CI宽度窄可开2%仓位预测值0.001但CI宽只开0.5%。多因子融合把本模型对log_return的预测作为一个新因子与其他因子如基本面因子PE、PB一起输入到逻辑回归或XGBoost中做最终的“涨跌二分类”。压力测试固定其他变量只让volatility_20d从当前值逐步增加10%、20%、50%看预测收益率如何变化评估策略在极端波动下的脆弱性。最后分享一个个人体会我最初做这个回归分析是为了解释为什么某次回测中一个简单均线策略在2021年Q4突然失效。跑完模型才发现当时volatility_20d系数从-0.012突变为-0.035说明市场对波动的敏感度翻倍了。这提示我不是策略坏了是市场状态变了——需要切换到更高频的参数或加入波动率过滤。回归分析的价值往往不在预测本身而在它提供的那面“归因镜子”照见市场底层逻辑的每一次呼吸与脉动。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑