简介:面向量化投资者、金融工程专业学生及研究学者的实证研究复现资料,以Python完整再现“衍生品使用对股价波动性影响”的论文分析流程,解决实际金融数据下如何构建双向固定效应面板模型并验证结论稳健性的问题。压缩包内含1个docx文档,约28KB,已有220人学习浏览。文档从导入pandas、statsmodels、linearmodels、arch等库开始,逐步演示股价年化波动率、贝塔系数及Hedge代理变量的计算,定义市值、市净率、净资产收益率、资产负债率等控制变量,并基于PanelOLS实现个体与时间双向固定效应回归。随后依次展开工具变量内生性处理、规模异质性分析、中介变量路径检验,以及更换因变量、行业年份分解、空间效应等多种稳健性测试,最后通过预测与敏感性分析评估模型表现。文档还涵盖缺失值删除、聚类稳健标准误设置等关键细节,所有代码配有详细注释,并强调实际应用中需使用国泰安、Wind等真实数据源替换示例数据,适合希望系统掌握金融实证研究方法或复现论文结果的进阶读者。
1. 用Python复现衍生品波动性研究:为什么实证结论常常反转
做衍生品交易的人,十有八九都听过一句话:衍生品会放大正股波动。可真把历史数据拉出来跑一遍回归,你会发现结论经常是反的——有的窗口里衍生品持仓量和波动率显著正相关,换一个牛熊区间又变成负相关甚至不显著。论文复现最让人头疼的就是这个:模型代码难度不高,但数据口径、波动率度量方式、事件窗口选取这些「研究设计」层面的细节,才是决定结论能不能立住的关键。这篇文章用Python把一套完整的实证框架拆开讲透——从假设设计、数据清洗、GARCH建模到稳健性检验,每个环节给出可运行的代码和参数说明。适合正在做金融论文复现、量化策略研究或者想验证衍生品对标的资产真实影响的研究者,读完你能直接照着跑通自己的样本。
2. 从论文假设到实证设计:先想清楚你在检验什么传导机制
2.1 信息假说、投机压力与对冲需求:衍生品影响正股波动率的三种路径
复现这类论文之前,第一件事不是写代码,而是把经济学逻辑捋清楚。衍生品对标的股价波动性的影响,学界公认的传导路径大致有三条:信息假说认为衍生品交易提高了市场信息含量,价格发现效率提升,波动率反而会下降;投机假说认为衍生品降低了交易门槛,吸引大量投机资金进入,短期噪音交易变多,波动率上升;对冲压力假说则强调衍生品为持有正股的机构提供了对冲工具,持仓者可以更坚定地持有现货,减少因恐慌抛售导致的过度波动,所以波动率会下降。
这三条路径在同一个数据集里可能同时存在,实证结果取决于哪条路径在样本期内占主导。所以我一般建议在写代码前先明确一个核心假设:你是检验「衍生品交易活跃度提升会使正股波动率上升」,还是检验「衍生品上市这一事件会改变波动率水平」?前者需要构建连续的衍生品交易活跃度变量,后者适合做事件研究。很多论文复现翻车,就是这两种设计混在一起,最后回归结果解说不出所以然。
2.2 波动率度量:对数收益率标准差、已实现波动率与GARCH条件波动率的区别
波动率本身是个潜变量,怎么度量直接决定结论方向。初学者最容易踩的坑是把「收益率标准差」直接当波动率——月度数据的标准差和日度数据的标准差年化后差异极大,而且标准差只捕捉了无条件波动,无法反映波动率聚集效应。论文复现里更常见的三种做法:一是已实现波动率,用日内高频收益率的平方和开方,数据要求高,但最贴近真实波动;二是GARCH条件波动率,把波动率建模为历史冲击和前期波动率的函数,适合日频数据;三是简单滚动标准差,计算简单,但滞后性强。
具体选择要看你的数据频率。如果只有日频收盘价,我建议用GARCH(1,1)提取条件波动率作为被解释变量,这也是金融实证里最主流的做法。如果手头有分钟数据,可以额外算一个已实现波动率做稳健性替换。关键是要在论文复现里明确说明:你的结论是不是依赖某一种波动率度量方式?如果换一种度量结论就反了,这不是稳健性不足,而是研究设计有缺陷。
2.3 模型选型:GARCH(1,1)做主模型、EGARCH做非对称检验、事件研究做补充
主模型层面,GARCH(1,1)是标准起点。它的好处是参数少、解释直观:α衡量新信息冲击对波动率的影响,β衡量波动率自身的持续性,α+β接近1说明波动率冲击衰减极慢,也就是所谓的「波动率聚集」。如果你想检验衍生品是否改变了波动率的非对称性——利空消息和利好消息对波动率影响不同——那要用EGARCH或GJR-GARCH,它们能捕捉杠杆效应。
事件研究是另一个常用补充:以衍生品上市日(比如个股期权上市、股指期货上市)为事件日,比较事件前后窗口内波动率均值是否发生显著变化。它的优势是因果关系更清晰,但难点在于事件窗口内可能混入大盘波动、行业政策等噪音,所以要做市场调整——用个股波动率减去同期大盘波动率,或者把大盘波动率放进回归里做控制变量。我的建议是主回归用GARCH + 控制变量,事件研究作为佐证,两个结论互相印证才有说服力。
3. 构建实证数据集:日频行情、衍生品持仓量与变量的对齐
3.1 数据源选型与对齐:A股用AkShare、美股用yfinance,衍生品持仓怎么拿
选数据源要务实:你要分析的是股票还是指数?衍生品数据是持仓量还是成交量?不同市场的公开数据源差异很大。A股这边,股票日线用AkShare能直接拉前复权数据,衍生品持仓量(比如股指期货、ETF期权)也可以用AkShare的历史持仓接口拿;美股那边,yfinance能拿股票日线和部分衍生品数据,但期权持仓量历史数据通常要付费源。论文复现阶段没必要追求多贵的数据,先把日频股票数据和衍生品代理变量对上就行。
对齐是数据预处理最耗时的环节。股票交易日和衍生品交易日基本一致,但要注意交割日、临时停牌、长假前后两者可能错位。我的习惯是先把两个数据源都转成以日期为索引的DataFrame,再按inner join对齐——宁可丢掉几个缺失日,也不能用NaN填充制造虚假数据。衍生品持仓量通常是每日收盘后的总量数据,和股票日K天然能对齐到同一天,这是它比成交量更适合做实证变量的原因之一。
3.2 数据清洗:复权处理、剔除停牌日与极端值截尾
行情数据拿到手,第一件事做复权。前复权和后复权在计算对数收益率时结果一致,但如果你要算市值、换手率等水平值变量,用后复权价格算市值会失真,所以一般用前复权价格算收益率,用不复权价格算市值。停牌日要单独标记或直接剔除——停牌一天的股票收益率是0,如果不剔除,它会拉低波动率序列,造成虚假的波动率下降。极端值也必须截尾,A股单日涨跌幅限制在±10%(ST是±5%),但复牌首日、新股上市头几天可能没有涨跌幅限制,一根大阳线能把滚动标准差拉飞。我一般对收益率做上下1%的缩尾处理,缩尾而不是直接删除,保住样本量。
3.3 核心变量构建:对数收益率、年化已实现波动率、持仓量变化率
变量构建是整个复现的地基,代码不复杂,但口径要严格。以下用一段示例代码演示从原始行情到核心变量的完整流程:
import akshare as ak import pandas as pd import numpy as np # 拉取A股日线数据,前复权 df = ak.stock_zh_a_hist( symbol="600519", period="daily", start_date="20200101", end_date="20241231", adjust="qfq" ) df.columns = [ "date", "open", "close", "high", "low", "volume", "amount", "amplitude", "pct_change", "change", "turnover" ] df["date"] = pd.to_datetime(df["date"]) df = df.set_index("date").sort_index() # 对数收益率:相比百分比变动,对数收益率在时间上可加 df["log_ret"] = np.log(df["close"] / df["close"].shift(1)) # 已实现波动率:用5日滚动窗口,年化处理 df["rv_5"] = df["log_ret"].rolling(5).std() * np.sqrt(252) # 剔除非交易日产生的NaN df = df.dropna(subset=["log_ret"]) # 模拟衍生品持仓量代理变量:这里用成交量换手率做示例 # 实际使用时替换为真实衍生品持仓数据并做对齐 df["volume_lag"] = df["turnover"].shift(1) df["d_volume"] = np.log(df["turnover"] + 1) - np.log(df["volume_lag"] + 1) print(df[["close", "log_ret", "rv_5", "d_volume"]].tail())逻辑说明:滚动标准差用的是rolling(5).std(),默认是样本标准差,分母是n-1,日频收益率的标准差年化乘以sqrt(252),因为A股一年大概有252个交易日。换手率做对数差得到连续复利式的变化率,这比简单差分更能捕捉持仓/交易活跃度的相对变化幅度,而且在量纲上和收益率更匹配。
参数说明:滚动窗口5天偏短,适合捕捉短期波动突变;如果你要做中长期影响分析,改成20日更稳。adjust="qfq"是前复权,算收益率没问题。市值变量需要单独拉总市值接口。dropna(subset=["log_ret"])顺带删掉了区间第一天,因为第一天没有前一日收盘价。
4. 核心实证复现:GARCH建模、事件研究与面板回归的可运行代码
4.1 用arch库拟合GARCH(1,1):条件波动率提取与参数解读
数据变量就绪后,进入核心的GARCH建模环节。Python里做GARCH模型,主流选择是arch库,它是Kevin Sheppard维护的专业金融计量库,支持GARCH、EGARCH、GJR-GARCH等多种设定。拟合之前有个经验:把收益率序列乘以100转换成百分比,数值上更接近优化器的舒适区间,能明显减少收敛失败的概率。下面是我常用的拟合代码:
from arch import arch_model # 用百分比收益率做GARCH(1,1),常数均值模型 ret = df["log_ret"].dropna() * 100 model = arch_model( ret, mean="Constant", vol="GARCH", p=1, q=1, dist="normal", rescale=False ) res = model.fit(disp="off", show_warning=False) # 提取核心参数 omega = res.params["omega"] alpha = res.params["alpha[1]"] beta = res.params["beta[1]"] print(f"omega={omega:.6f}, alpha={alpha:.4f}, beta={beta:.4f}") print(f"alpha+beta={alpha + beta:.4f}") # 条件波动率序列(单位是百分比,转回小数) cond_vol = res.conditional_volatility / 100 df["garch_vol"] = cond_vol逻辑说明:模型输出里最关键的是alpha[1]和beta[1],alpha是ARCH项系数,表示当期新息冲击对波动率的影响强度;beta是GARCH项系数,表示上一期波动率对当期的影响。alpha+beta接近1意味着波动率冲击衰减很慢,一个利空消息会让波动率在高位持续很久,这是金融资产普遍的波动率聚集特征。dist="normal"是正态分布假设,如果你的收益率序列厚尾明显,改成dist="t"通常拟合更好,但参数估计会慢一些。
参数说明:mean="Constant"表示均值方程只含常数项;rescale=False表示不做内部自动缩放,避免和外部手动缩放冲突;disp="off"关闭迭代过程输出,省得日志刷屏。如果模型报ConvergenceWarning,先检查收益率是否做了缩放,再考虑把迭代次数调大(options={"maxiter": 5000})。
4.2 事件研究:用事件窗口比较衍生品上市前后的波动率变化
事件研究的核心是定义事件日、事件窗口和估计窗口。以个股期权上市为例,事件日是上市首日,事件窗口通常是[-10, +10]共21个交易日,估计窗口是[-130, -11]共120个交易日,用来估计「如果没有事件发生,波动率应该是多少」。做法是先用估计窗口拟合GARCH,外推出事件窗口的条件波动率预测值,再和实际观测值做对比。下面是简化版的事件研究代码:
# 定义事件日(示例:假设某股票期权于2024-01-17上市) event_date = pd.Timestamp("2024-01-17") # 用位置索引对齐交易日,避免停牌导致的日期偏移 pos = df.index.get_indexer([event_date], method="nearest")[0] # 事件窗口:事件日前5日至后10日 win_start = pos - 5 win_end = pos + 10 # 估计窗口:事件日前30日至前6日 est_start = pos - 30 est_end = pos - 6 # 用估计窗口拟合GARCH,再预测事件窗口的条件波动率 est_ret = df["log_ret"].iloc[est_start:est_end] * 100 est_model = arch_model(est_ret, mean="Constant", vol="GARCH", p=1, q=1) est_res = est_model.fit(disp="off") # 获取事件窗口的实际条件波动率(简化:用已实现波动率做替代) actual_vol = df["rv_5"].iloc[win_start:win_end] # 对比均值:事件前5日 vs 事件后10日 pre_vol = actual_vol.iloc[:5].mean() post_vol = actual_vol.iloc[5:].mean() print(f"事件前5日平均波动率: {pre_vol:.4%}") print(f"事件后10日平均波动率: {post_vol:.4%}") print(f"变化幅度: {(post_vol / pre_vol - 1):.2%}")逻辑说明:这里用get_indexer方法把事件日期映射到交易日位置索引,比直接按日期切片更稳,因为停牌会造成日期空洞。先用估计窗口的120个交易日拟合GARCH,本质上是在估计「若无事件干扰的波动率基准」,然后拿事件窗口的实际波动率和基准比。示例里简化成了前后均值对比,完整做法应该用预测误差的累计值并做t检验。
参数说明:窗口长度没有金标准,[-5, +5]和[-10, +10]都是常见选择。窗口太短统计功效低,太长容易混入其他事件噪音。我习惯事件前窗口短一点(5天),事件后窗口长一点(10天),因为衍生品上市的影响可能滞后显现。如果你有多只股票在同一天上市,要做事件聚类处理,否则t值虚高。
4.3 面板回归:把衍生品变量和波动率放在同一张回归桌上
事件研究只能看单一事件前后的均值变化,要控制大盘波动、市值、换手率这些混杂因素,需要面板回归。把N只股票的时间序列堆成一个面板,每一行是一次「股票-日期」观测,被解释变量是GARCH条件波动率或已实现波动率,核心解释变量是衍生品持仓变化率,控制变量包括大盘波动率、个股换手率、市值对数和Beta。面板回归的标准做法是固定效应模型,但statsmodels里的PanelOLS对数据格式要求严格,我一般用带聚类稳健标准误的OLS近似,结果和固定效应差异不大:
import statsmodels.api as sm # 构造示例面板数据:每行是某只股票某一天的观测 # 实际使用时需要把多只股票的数据纵向拼接,并增加 stock_id 列 panel = df[["rv_5", "d_volume", "turnover", "pct_change"]].copy() panel["stock_id"] = "600519" panel = panel.dropna() # 控制变量:大盘波动率用沪深300指数已实现波动率,这里用个股自身做占位 panel["mkt_vol"] = panel["rv_5"].rolling(20).mean() # 固定股票效应:用虚拟变量或组内去心,这里演示组内去心 X = panel[["d_volume", "turnover", "mkt_vol"]] y = panel["rv_5"] # 组内去心:每个变量减去股票自身均值 X_demeaned = X - X.groupby(panel["stock_id"]).transform("mean") y_demeaned = y - y.groupby(panel["stock_id"]).transform("mean") # 加常数项并做聚类稳健标准误 X_demeaned = sm.add_constant(X_demeaned) model = sm.OLS(y_demeaned, X_demeaned) result = model.fit(cov_type="cluster", cov_kwds={"groups": panel["stock_id"]}) print(result.summary())逻辑说明:组内去心是固定效应模型的最小二乘实现,等价于给每只股票加一个哑变量,但计算更快。cov_type="cluster"把标准误按股票聚类,允许同一只股票在不同日期的扰动项自相关——这是金融面板数据最常见的序列相关问题,不聚类的话t值会严重高估。核心解释变量d_volume的系数如果是显著正数,说明衍生品交易活跃度上升和波动率上升存在统计上的正相关。
参数说明:cov_kwds={"groups": panel["stock_id"]}必须传入聚类分组变量,否则默认是普通OLS标准误。mkt_vol用滚动20日均值做控制,是为了捕捉市场层面波动对个股的溢出。如果你的核心解释变量不显著,先检查它和控制变量之间的相关系数,衍生品持仓量和换手率经常高度共线,共线会导致标准误膨胀、系数不显著,这是后面要讲的常见坑。
5. 复现过程中的避坑指南:5个让实证翻车的隐藏问题
5.1 GARCH模型不收敛,迭代到一半报错
现象:arch模型调用fit()后一直输出迭代日志,最后直接抛ConvergenceError或者参数变成NaN。
原因:最常见是收益率序列量级过大或过小。原始日收益率是0.01量级的小数,GARCH参数初始值对这么小的数值不敏感,优化器容易在平坦区域打转。
解决:把收益率乘以100变成百分比再拟合,这是最简单有效的办法。仍然不收敛就换分布假设——正态分布改t分布,或者把优化方法换成BFGS。我遇到过一次极端情况,是数据里混入了一个极端离群值(某天涨了30%),缩尾之后立刻收敛。
5.2 衍生品变量回归系数不显著,核心结论立不住
现象:持仓量变化率对波动率的回归p值高达0.4,换成交量代理变量也不显著,论文复现卡在最核心的位置。
原因:要么是真没有效应,要么是变量之间存在多重共线性。衍生品持仓量和正股成交量、换手率高度相关,它们都在反映市场交易活跃度,一起放进回归后标准误膨胀,谁也争不到显著性。
解决:先看相关系数矩阵,如果核心解释变量和控制变量相关系数超过0.7,考虑正交化——用衍生品变量对换手率回归取残差作为新的解释变量。这个残差代表「剔除交易活跃度后衍生品特有的信息增量」,在理论上也更贴合信息假说的含义。
5.3 事件研究窗口重叠,导致同一事件被重复计数
现象:多只股票的衍生品上市日集中在同一个月,事件窗口互相重叠,每条观测里都混入了其他股票的事件影响。
原因:如果直接用全样本做事件研究,事件日接近的股票会被重复计算,而且样本不独立,t值被系统性高估。
解决:两步走。第一步只保留「首次上市」的样本,排除多次增发或加挂合约造成的事件污染;第二步按事件日聚类标准误,把同一天发生的事件当成一个聚类。我一般直接按事件日分组做聚类,代码里是cov_kwds={"groups": event_date_group}。
5.4 停牌复牌日造成波动率假跳,滚动窗口被一根K线带偏
现象:某只股票停牌重组三个月后复牌,当天没有涨跌幅限制,收益率直接干了+40%,随后5天滚动标准差一直维持在极高水平,归因到衍生品变量上完全失真。
原因:长停牌后复牌首日的收益率不是正常交易形成的,它是停牌期间积累的信息一次性释放,本质上是个异常点而非当日波动。
解决:在清洗阶段识别停牌天数,复牌首日如果停牌超过5个交易日,把当日收益率做缩尾处理或者单独加一个复牌哑变量。哑变量方案更干净,它允许复牌效应存在,但不会让它污染其他样本的估计。
5.5 时间序列平稳性没检验,回归结果看着显著其实是伪回归
现象:波动率序列和衍生品持仓量序列都是缓慢变化的高持久性变量,直接做OLS回归R²高达0.9,看起来完美,其实两者都是非平稳的I(1)过程,回归关系是虚假的。
原因:金融时间序列普遍存在单位根,两个带趋势的序列即使毫无因果关系,也能回归出显著的系数。这是论文复现最隐蔽的坑。
解决:先对每个变量做ADF检验,核心解释变量和被解释变量必须都是平稳的。如果持仓量序列不平稳,改用差分后的持仓量变化率,通常一阶差分就平稳了。波动率序列本身一般是平稳的但持久性强,可以做稳健性替换——用波动率的对数形式或差分形式再跑一遍。
6. 验证结论可信度:样本外预测与滚动子样本检验
实证结果跑出来之后,很多人直接开始写论文结论,但还想让结论更扎实,额外做两件事能显著提升可信度。一是滚动子样本检验,把全样本按年份切成三段,分别跑回归看核心系数方向是否一致,一致性比显著性重要,方向都相反的结果基本是数据噪声;二是样本外预测检验,用前70%的数据拟合GARCH,预测后30%的样本外波动率,看预测误差有多大。这两步验证做完,结论才是能站得住的。
样本外预测可以用一段简短的代码完成:
# 用前70%数据拟合,预测后30%波动率 split = int(len(ret) * 0.7) train, test = ret.iloc[:split], ret.iloc[split:] model = arch_model(train, mean="Constant", vol="GARCH", p=1, q=1) res = model.fit(disp="off") # 样本外条件波动率预测 forecasts = res.forecast(horizon=len(test), reindex=False) pred_vol = np.sqrt(forecasts.variance.values[-1]) / 100 # 用实际已实现波动率做对比 actual_vol = df["rv_5"].iloc[split:].values # 计算均方根误差 rmse = np.sqrt(np.mean((pred_vol - actual_vol) ** 2)) print(f"样本外波动率预测RMSE: {rmse:.6f}")参数说明:预测步长horizon=len(test)一次预测后30%全部长度,reindex=False保持索引对齐。实际上多步预测的误差会累积,更严谨的评估方式是滚动一步预测——每预测一天就重新拟合一次,计算量是前者的几十倍,但结果更可信。RMSE没有绝对的好坏标准,关键是比较不同模型的RMSE:如果加了衍生品变量后预测误差比基准GARCH更低,说明衍生品信息确实对波动率有解释力。
另外一个我常用的验证技巧是变量替代法:把GARCH条件波动率换成已实现波动率、把持仓量变化率换成成交量变化率,重跑整个流程。如果核心结论在多种度量方式下方向一致,就算通过了最严格的稳健性检验。最早复现这类论文时,我的GARCH模型反复不收敛,折腾了一晚上才发现是收益率没做百分比缩放——这个教训让我后来所有金融计量模型都先做量纲归一化。希望这些经验能帮你少走弯路,把力气花在真正的研究设计上,而不是和优化器较劲。
本文还有配套的精品资源,点击获取