☰
Python复现衍生品对股价波动性实证:GARCH、OLS与代码链路
2026/10/2 22:26:45 网站建设 项目流程

简介:这份面向金融工程与量化分析学习者的论文复现文档,以Python为工具,完整还原上市公司衍生品使用对股价波动性影响的实证研究流程。内容覆盖数据预处理、股价波动率与贝塔系数计算、双向固定效应面板回归建模,并延伸至内生性工具变量处理、规模异质性分析、中介路径剖析及多种稳健性检验,最终通过预测与敏感性测试验证模型鲁棒性。资源包仅含1个docx文件,大小28KB,虽轻量却集中呈现了从变量定义到结论验证的完整代码与逐段解释,适合对照论文理解每一步操作逻辑。目前已有220人学习下载,对希望掌握面板数据建模、时间序列波动率测算与金融因果推断的读者而言,可作为一份可落地的复现代码笔记与排错参考。

1. 用Python复现衍生品波动性实证:先说清楚这个题目在做什么

拿到“金融领域:使用Python分析衍生品对股价波动性的实证研究(论文复现,含详细代码及解释)”这个标题的读者,多半已经在别处看过几篇研报复现帖,发现代码能跑但结论对不上。这篇笔记是我个人对这个题目的完整拆解:从行情数据下载、波动率估计到回归检验,每一步用什么Python库、参数怎么设、为什么这么设、失败时看哪里,全部摊开讲。适合正在做量化研究、写毕业论文、或者刚进入金融数据分析岗位想独立验证一篇论文结论的从业者。核心就一句话:衍生品活跃度到底是在给标的股价波动率“加噪声”还是“加信息”,这个问题靠Python把数据处理、估计、检验串成一条可复现的链路,才真正有答案。

2. 数据准备:从yfinance取行情到衍生品活跃度指标

2.1 数据源选择:用yfinance拿股票与期权链快照的边界

复现这类论文首先要面对的是数据从哪来。常见做法是用yfinance库直接拿股票日线数据,它免费、无需注册、接口简单,适合快速搭原型。但这里有个边界:衍生品的历史日频数据,比如每日期权成交量、每日未平仓量,yfinance并不提供完整历史序列,它只能给你“当前”的期权链快照。所以完整复现时,衍生品活跃度需要用替代指标来构造,常见的有期权链快照成交总量、正股成交量比值(OIR),或者干脆用隐含波动率指数作为衍生品市场情绪的代理。

import numpy as np import pandas as pd import yfinance as yf # 下载苹果公司日线数据,auto_adjust=True 让价格自动复权,避免除权跳空污染收益率 stock = yf.download("AAPL", start="2015-01-01", end="2023-12-31", auto_adjust=True) close = stock["Close"] log_ret = np.log(close / close.shift(1)).dropna() # 抓当前期权链快照,用于构造衍生品活跃度截面指标 tk = yf.Ticker("AAPL") option_dates = tk.options[:6] # 取最近6个到期日 snapshots = [] for d in option_dates: chain = tk.option_chain(d) calls = chain.calls puts = chain.puts snapshots.append({ "date": d, "call_volume": calls["volume"].sum(), "put_volume": puts["volume"].sum(), "call_oi": calls["openInterest"].sum(), "put_oi": puts["openInterest"].sum(), }) snapshot_df = pd.DataFrame(snapshots)

逻辑说明:第一个代码块中,auto_adjust=True会调整历史价格以反映拆股和分红,这避免了收益率序列里出现假跳变。注意shift(1)这一步,它把价格序列整体下移一行,close / close.shift(1)就是“今天除以昨天”,取对数后就是连续复利收益率。

参数说明:start和end决定了样本区间,论文复现时一般要覆盖至少一个完整牛熊周期,五年起步;tk.options[:6]取前六个到期日,是因为近月合约流动性最好、交易最活跃,更能反映衍生品交易热度。

这里必须坦白:期权链快照只有当天数据,没法重构历史序列。所以后续建模时,如果坚持用期权成交量,就只能做截面回归,验证“某一天期权交易活跃的股票,其当天波动率是否更高”。如果想做时间序列面板回归,更现实的衍生品活跃度替代指标是隐含波动率指数,或者用新闻热度、搜索量这类外部数据。论文读到的“衍生品交易量显著影响标的波动率”结论,背后用了什么样的数据频率,直接影响你复现时能复现到什么程度——这一步想清楚了,后面才不会被数据缺口卡死。

2.2 收益率与滚动波动率:对数收益率和年化口径怎么算

实证研究里波动率很少直接用价格计算,而是带在收益率上。最简单的波动率估计是历史滚动标准差——把过去N天的收益率当成一个样本窗口,计算标准差后年化。这里有两个参数要定:窗口长度N和年化因子。窗口选5天得到周度波动率,选22天得到月度波动率,选252天得到年度波动率。金融论文里最常见的组合是22天窗口和sqrt(252)年化,这样日度波动率就被换算成年化百分比,可以直接和期权隐含波动率对比。

# 22个交易日约等于一个月,年化因子 sqrt(252) 把日波动率换成年度口径 monthly_vol = log_ret.rolling(22).std() * np.sqrt(252) weekly_vol = log_ret.rolling(5).std() * np.sqrt(252)

逻辑说明:rolling(22).std()做的事情是:从第一个数据点开始,每次取连续22个收益率算一个标准差,然后窗口向后滑动一天。这样得到的波动率序列天然带有“随时间变化”的特征,能反映波动率聚集现象——大波动后面跟着大波动。

参数说明:年化因子的数学依据是独立同分布假设。如果收益率是独立同分布的,n天的累计方差等于单日方差的n倍,所以标准差乘sqrt(n)。一年大约252个交易日,所以日波动率乘sqrt(252)。但注意,如果你的数据是周度频率,年化因子要换成sqrt(52);是月度就换sqrt(12)。很多复现时数字差好几倍,就是年化因子用错了。

用对数收益率而不用普通收益率,是因为对数收益率可加性更好:两天的连续复利收益率可以直接相加得到累计收益率,而且它近似服从正态分布,符合GARCH族模型的分布假设。普通收益率只能做乘积,数学处理上麻烦得多。这个选择不是偏好问题,是后面接GARCH模型时能否顺利收敛的前提。

2.3 衍生品活跃度指标:OIR与期权未平仓量怎么落到面板上

有了波动率序列,还需要一个代表衍生品活跃度的解释变量。我一般用OIR,也就是期权总成交量与正股成交量的比值。逻辑很直观:股票本身成交清淡但期权成交活跃,说明衍生品市场在价格发现中扮演了更重要的角色,这种信息结构差异会反映到股价波动上。OIR的数据构造如下。

# 假设stock_df包含正股成交量列 Volume stock_volume = stock["Volume"] # 用前面抓到的期权链快照,把看涨看跌的成交量合并 total_option_volume = snapshot_df["call_volume"].sum() + snapshot_df["put_volume"].sum() # 构造当日OIR(截面,单日) oir_today = total_option_volume / stock_volume.iloc[-1] # 如果要做面板,把每天的期权链快照汇总后拼进DataFrame panel = pd.DataFrame(index=log_ret.index) panel["vol_22d"] = monthly_vol panel["ret"] = log_ret panel["stock_volume"] = stock_volume

逻辑说明:OIR大于某个阈值(比如1)时,说明期权市场成交量已经超过正股市场,衍生品交易者主导了信息传递。这个比值做截面比较时要注意流动性差异——小盘股的期权成交天然不活跃,OIR普遍偏低,归回时要控制市值因子,否则结论会有偏。

参数说明:如果拿不到历史期权成交量,退而求其次的方案是用未平仓量OI的变化量。OI增加代表新开仓,减少代表平仓,只有新开仓才对波动率的边际影响最大。snapshot_df["call_oi"].sum()是没用的,要差分后再用。不过同样受限于yfinance不提供历史快照,这一步只能验证当日截面关系。真正做完整面板时,我建议直接换数据源或者换隐含波动率指数,不要在一个拿不到的数据维度上硬耗——这是复现论文时最容易钻进死胡同的地方。

3. 波动率估计:GARCH(1,1)与EWMA的参数设置和Python实现

3.1 波动率代理指标的三个层次与选择原则

滚动波动率虽然直观,但它有个致命弱点:窗口内所有天数的权重相等,过去22天前的那天和昨天对今天波动率的贡献一样大。这不符合金融市场的直觉——昨天的冲击对今天的影响应该远大于一个月前。所以论文里更常用的是EWMA(指数加权移动平均)和GARCH(广义自回归条件异方差)模型。选哪个当波动率代理指标,直接影响后续回归的显著性:EWMA波动率更平滑,GARCH波动率能捕捉波动率聚集,滚动波动率则会引入大量序列自相关,干扰回归标准误。我的一般原则是:主回归用GARCH条件波动率,稳健性检验用EWMA,滚动波动率只做交叉验证。

3.2 用arch包在Python里拟合GARCH(1,1):命令与参数

Python里拟合GARCH模型最顺手的库是arch。GARCH(1,1)的数学形式是:当前条件方差等于一个常数项、上一期收益率平方项和上一期条件方差项的线性组合。它意味着今天的波动率不仅受昨天实际收益冲击影响,还受昨天波动率水平影响,这就是波动率聚集能被建模的原因。

from arch import arch_model # 用对数收益率拟合带常数均值的GARCH(1,1) model = arch_model(log_ret, mean="Constant", vol="GARCH", p=1, q=1, dist="normal") result = model.fit(disp="off") print(result.summary()) # 提取条件波动率序列,作为后续回归的因变量 conditional_vol = result.conditional_volatility

逻辑说明:mean="Constant"表示均值方程只估计一个常数,不在均值里引入AR项。复现论文时这个设置够用,但如果收益率序列有显著的一阶自相关,就改成mean="AR"。p=1, q=1是GARCH族最经典的配置,绝大多数金融时间序列用这一个组合就够描述波动率特征,加高阶容易过拟合。

参数说明:dist="normal"假设标准化残差服从正态分布,这是基准设置。很多论文会说收益率有厚尾,这时把dist换成"t"让模型自己估计自由度,往往能明显改善拟合优度。disp="off"关掉了迭代过程输出,跑批量循环时不会刷屏。

拟合完要看两个数:alpha[1]是上一期收益冲击的系数,beta[1]是上一期条件方差的系数,两者之和越接近1,说明波动率的持续性越强。我见过不少复现里alpha估计出来是负的或者不显著,一旦出现这种情况,先检查收益率序列里有没有极端异常值,再看要不要换成Student-t分布,这两个原因解决了大半问题。

3.3 EWMA和历史波动率的Python实现:参数怎么定

EWMA波动率不需要估计参数,公式固定:今天的方差等于lambda乘昨天的方差加1减lambda乘昨天的收益率平方。最关键的是lambda取值。RiskMetrics给出的经典值是0.94,意思是昨天的权重是0.94,新冲击只占0.06。这个值在日度数据上表现稳定,想更灵敏就把lambda降到0.90,但代价是波动率序列会变得很毛糙,回归时噪声变大。

def ewma_vol(returns, lam=0.94): var = np.zeros_like(returns, dtype=float) var[0] = returns.iloc[0] ** 2 for i in range(1, len(returns)): var[i] = lam * var[i - 1] + (1 - lam) * returns.iloc[i - 1] ** 2 return pd.Series(np.sqrt(var * 252), index=returns.index) ewma_vol_series = ewma_vol(log_ret)

逻辑说明:这个迭代式体现的核心思想是指数衰减——越久远的数据对今天的影响按指数速度衰减。var[i-1]前乘0.94,昨天的收益率平方前乘0.06,合起来权重之和等于1,保证方差序列是无偏的。

参数说明:lam=0.94是日度数据的经验值,如果用周度数据要降到0.97左右,因为低频数据的自相关更强。这个参数不建议自由发挥,除非你有明确理由。EWMA的优势是不需要优化算法,几行代码就能跑出来,适合当GARCH的对照——如果两个波动率序列在回归里得出的结论方向不一致,那说明结论对波动率度量方式太敏感,论文的稳健性存疑。

这里也提醒一个新手常踩的坑:GARCH和EWMA给出的都是日度条件波动率,要用np.sqrt(252)年化后才能和滚动波动率对比。忘记年化会导致回归系数量级差几十倍,而且不同波动率代理之间数值不可比。把三个序列画在一张图里检查量级是否一致,是正式回归前必做的一步,我的习惯是先画图再跑模型,不做这一步就回归等于闭眼开车。

4. 实证回归设计:衍生品活跃度对波动性的检验步骤

4.1 基准OLS回归与解释变量分层

波动率序列有了,衍生品活跃度指标也有了,接下来就是实证回归。第一步不能上来就堆变量,而是先跑一个最简模型:波动率对衍生品活跃度指标回归,控制条件放最少。这样能看出去掉所有干扰后,衍生品活跃度和股价波动性之间有没有最朴素的相关关系。这看起来简单,但意义很大:后续每加一个控制变量,这个系数的变化轨迹就是在告诉你结论的稳健性边界在哪。

import statsmodels.api as sm # 组装面板数据 df = pd.DataFrame({ "vol": conditional_vol, # GARCH年化条件波动率 "ret": log_ret, # 当日收益率 "stock_volume": stock_volume, # 正股成交量 "oir": oir_series, # 衍生品活跃度指标 }) # 基准模型:只放OIR X1 = sm.add_constant(df[["oir"]]) y = df["vol"] model1 = sm.OLS(y, X1).fit() print(model1.summary()) # 扩展模型:加入收益率和正股成交量作为控制 X2 = sm.add_constant(df[["oir", "ret", "stock_volume"]]) model2 = sm.OLS(y, X2).fit() print(model2.summary())

逻辑说明:sm.add_constant会给解释变量矩阵加一列全1,对应截距项。model1里如果oir的系数显著为正,说明衍生品活跃日标的波动率更高;为负则说明衍生品交易平抑了波动。但model1的问题很明显——它没控制正股自身的成交活跃度,而股票的成交量和波动率本来就有强相关,不控制就会把“股票交易活跃导致的波动”误记到衍生品头上。

参数说明:基准模型里ret作为控制变量,是为了剔除收益率方向对波动率的不对称影响;stock_volume取原始值即可,但如果你发现系数量级奇怪,可以取对数,因为成交量的分布右偏严重,对数变换能让它更接近正态。这一层模型的系数要是和model1方向相反,多半是OIR构造有问题,比如分母用了未复权的成交量。

检验基准模型输出时,重点看oir的t值和p值。p值大于0.05说明统计上不显著,但别急着下结论——先看样本量。如果只有几百个交易日,不显著可能只是功效不足,扩样本后再看才是正路。

4.2 加入市场因子后的稳健性回归

单只股票的波动率里有相当一部分是市场系统性波动,不是个股特有信息。所以严谨的做法是把市场波动率或市场收益率放进去当控制变量。常见的做法是引入SPY(标普500ETF)的收益率和波动率,把市场层面的冲击从个股波动率里剥离出来。这个步骤如果省略,复现出来的系数很可能被系统性风险污染——2020年这种年份,所有股票的波动率都在涨,你会错误地归因于衍生品交易。

# 下载SPY作为市场代理 spy = yf.download("SPY", start="2015-01-01", end="2023-12-31", auto_adjust=True) spy_ret = np.log(spy["Close"] / spy["Close"].shift(1)).dropna() df["spy_ret"] = spy_ret.reindex(df.index) df["oil_volume"] = df["stock_volume"] / df["stock_volume"].rolling(22).mean() # 标准化成交量 X3 = sm.add_constant(df[["oir", "ret", "stock_volume", "spy_ret"]]) model3 = sm.OLS(y, X3).fit() print(model3.summary())

逻辑说明:spy_ret在这里扮演两个角色,一是剔除系统性收益冲击对个股收益率的同步影响,二是间接控制市场波动状态。reindex(df.index)确保SPY的收益率严格对齐到个股交易日索引上,这一步经常因为交易日不齐而出NaN,后面踩坑章节会详细说。

参数说明:标准化成交量stock_volume / rolling(22).mean()是为了去除成交量的长期趋势——股票成交量随市值增长年年变大,不标准化的话这个变量在多年样本里会带时间趋势,造成伪回归。标准化后,它代表的是“相对过去一个月,今天的成交量放大了还是缩水了”,这才是控制变量的正确口味。

加了spy_ret之后,重点观察oir系数有没有大幅变小。如果变小,说明原始基准模型里有一部分效应其实是市场波动贡献的,衍生品活跃度本身的边际解释力没那么强。这不是坏事,恰恰是复现论文时最有价值的发现——论文结论在你这个样本上到底站不站得住,答案就在这里。

4.3 子样本期回归:不同市场状态下的结论差异

衍生品和波动率的关系不是铁板一块。低波动率环境下,期权交易者少,衍生品对股价的影响弱;高波动率环境下(比如2020年疫情冲击),对冲需求暴增,衍生品交易量飙升,这时OIR与波动率的正相关会更显著。如果全样本回归出一个不温不火的系数,拆开看子样本期往往能看到完全不同的图景。

pre_mask = df.index < "2020-01-01" post_mask = df.index >= "2020-01-01" for name, mask in [("2015-2019", pre_mask), ("2020-2023", post_mask)]: sub = df[mask].copy() X_sub = sm.add_constant(sub[["oir", "ret", "stock_volume", "spy_ret"]]) model_sub = sm.OLS(sub["vol"], X_sub).fit() print(f"=== {name} ===") print(model_sub.summary())

逻辑说明:布尔索引df.index < "2020-01-01"直接切出样本区间,sub是原DataFrame的一份拷贝,避免切片赋值时的SettingWithCopyWarning。逐段跑回归并对比系数,是论文里“分样本稳健性”的常规操作。

参数说明:切分点选2020年不是随意的——疫情导致全球市场波动率结构突变,以这个点为界,两段样本的波动率特征差异足够大。你也可以参考VIX指数的走势图来选结构突变点,而不是机械地按年份切。

这个步骤最有价值的地方在于:它告诉你论文结论的成立条件。如果前一段不显著、后一段显著,那说明衍生品对波动率的影响不是线性的、稳定的,而是依赖于市场状态的。写结论时不能写“衍生品提升波动率”,要写“高波动时期衍生品活跃度与波动率正相关”。这种措辞上的差别,正是一篇有说服力的实证研究和一篇看似严谨实则在过度解读的复现之间的分界线。拆子样本是论文复现里成本最低、信息量最大的稳健性检验,没有之一。

5. 论文复现避坑:5个让结果对不上的常见问题与排查

5.1 日期索引错位导致merge后全是NaN

现象:跑回归时发现样本量从2000多掉到几百,解释变量那一列为空。

原因:yfinance返回的DataFrame索引带时区信息,而自己构造的Panel里日期是纯日期,两者类型不一致时reindex或merge会把所有日期匹配失败,返回NaN。另一个常见原因是股票和SPY的交易日历不完全一致,美国市场有节假日,个股和ETF的停牌日也可能不同。

解决:统一先reset_index()把日期变成普通列,用pd.to_datetime统一格式,再用how="inner"做merge丢弃不匹配的日期。merge之后用dropna()清一遍,确保没有残余空值进入回归。

5.2 GARCH拟合报奇异矩阵或收敛失败

现象:model.fit()报错,提示Singular matrix,或者迭代几百次后结果里alpha为负。

原因:收益率序列里有极端异常值(比如某天涨跌超过20%),正态分布假设被厚尾破坏,方差方程的迭代矩阵接近奇异。另一个常见原因是收益率序列未清理停牌日,连续多个0收益率会把波动率瞬间压到零附近。

解决:先对收益率做winsorize处理,把上下0.5%分位的极端值截断到分位数边界;再把dist="normal"换成dist="t",厚尾分布对异常值容忍度高很多。如果还不行,检查数据里是否有连续重复的0值,有就把这些日期删掉。这一步不处理,后面所有波动率序列都是错的。

5.3 回归结果换个样本期就翻车

现象:用2015-2019年数据回归,OIR系数显著为正,换成2020-2023就变成不显著,甚至符号反转。

原因:衍生品活跃度与波动率的关系存在状态依赖性。2020年之后波动率整体抬升,所有股票的波动率都在涨,OIR的边际解释力被市场因子吸收;而在低波动期,衍生品交易量的边际信息含量更突出。还有一个可能是两个时期里你用的期权快照流动性差异很大。

解决:先跑子样本回归确认方向差异,然后不要试图用全样本调和出“唯一正确答案”。论文复现的价值就该体现在这里——同一个模型在不同市场状态下结论不同,这是发现而不是错误。报告两段结果并说明原因,比强行删数据改出显著性好得多。

5.4 期权历史数据拿不到:别在结构缺口上硬拼

现象:想在日度面板里放期权成交量作为连续解释变量,发现yfinance只能给当前快照,历史序列完全缺失。自己动手天天跑脚本抓数据,抓了几个月发现存下来的数据结构混乱,无法使用。

原因:yfinance的期权接口设计就是获取当前市场状态,不是历史数据接口。这是工具的结构性限制,不是代码能绕过的。硬抓数据只会浪费大量时间在数据清洗上。

解决:换思路。用隐含波动率指数(比如VIX对SPY,或者个股的已实现波动率与历史波动率之差)作为衍生品市场情绪的代理指标,从公开数据源直接下载。或者重新设计研究问题,改成截面回归:在某一天,比较不同股票的OIR与其当日波动率的关系。这样既能用上期权快照,又不与数据结构缺口硬碰。

5.5 Windows系统跑代码报msvcp140.dll找不到

现象:刚配好的VSCode环境里,import pandas或arch时直接报错由于找不到msvcp140.dll无法继续执行代码,代码完全跑不起来。

原因:arch和statsmodels这类科学计算库在Windows上依赖微软Visual C++运行库。新装的系统或精简版系统缺这个运行库,pip安装时又不自动附带。

解决:安装Microsoft Visual C++ Redistributable(2015-2022 x64版本),装完重启终端即可。如果你用的是VSCode,装完运行库后记得在设置里确认Python解释器指向你创建虚拟环境的那一个,因为VSCode左侧选择解释器和终端里实际生效的解释器可能不一致,这又是个隐性坑。这个坑本身和金融模型无关,但它能卡住你半天时间,属于典型的环境血泪经验。

6. 用模拟数据先验证整条代码链路:一个省时间的进阶技巧

6.1 构造带波动率聚集的模拟价格序列

真实数据里就算结果显著,你也无法确定是代码写对了还是数据碰巧撞出了个显著。我的习惯是先用模拟数据把“数据下载-波动率估计-回归检验”整条链路跑通,确认每个环节输出正常之后,再换真实数据。模拟数据的优势是生成参数完全已知,比如设定alpha=0.08、beta=0.90,跑完GARCH之后如果估计出来的参数和设定值偏差不大,说明估计代码没有问题。

np.random.seed(42) n = 3000 omega_true, alpha_true, beta_true = 0.05, 0.08, 0.90 sigma2 = np.zeros(n) eps = np.zeros(n) for t in range(1, n): sigma2[t] = omega_true + alpha_true * eps[t-1] ** 2 + beta_true * sigma2[t-1] eps[t] = np.sqrt(sigma2[t]) * np.random.standard_t(5) sim_ret = pd.Series(eps * 0.01, index=pd.date_range("2015-01-01", periods=n, freq="D"))

逻辑说明:这个循环严格按照GARCH(1,1)定义生成数据,先算今天的条件方差(用昨天的收益率平方和昨天的方差),再从t分布里抽一个随机数作为标准化残差,乘上标准差得到今天的收益率。用t分布而不是正态分布,是为了模拟真实收益率的厚尾特征。

参数说明:np.random.seed(42)固定随机种子,保证模拟数据可复现。eps * 0.01把收益率倍数调整到实际量级,避免方差数值过大影响GARCH估计数值稳定性。自由度设5的t分布尾部比正态更厚,更接近真实行情。

6.2 用模拟数据跑通指标构建-估计-回归三段代码

生成模拟数据之后,不要直接丢进GARCH就完事。要把真实数据流程完整走一遍:先算22日滚动波动率,再拟GARCH提取条件波动率,然后构造一个假的OIR序列(比如直接用某个正态随机数加正股模拟成交量),跑一次完整回归。重点不是回归系数,而是确认每一段代码在数据形态真实时都不会报错、不会出现莫名NaN。

# 模拟正股成交量与OIR sim_volume = pd.Series(np.random.lognormal(mean=15, sigma=0.5, size=n), index=sim_ret.index) sim_oir = pd.Series(np.random.uniform(0.3, 1.5, size=n), index=sim_ret.index) # 重复第3章的GARCH估计和第4章的回归流程 sim_model = arch_model(sim_ret, mean="Constant", vol="GARCH", p=1, q=1, dist="t") sim_res = sim_model.fit(disp="off") print(sim_res.params[["alpha[1]", "beta[1]"]]) sim_df = pd.DataFrame({ "vol": sim_res.conditional_volatility, "ret": sim_ret, "stock_volume": sim_volume, "oir": sim_oir, }) X = sm.add_constant(sim_df[["oir", "ret", "stock_volume"]]) sim_ols = sm.OLS(sim_df["vol"], X).fit() print(sim_ols.summary())

逻辑说明:用t分布拟合模拟数据时,如果估计出的alpha和beta接近0.08和0.90,就说明GARCH估计在厚尾数据上工作正常。回归部分OIR系数理论上应该不显著,因为模拟数据里OIR完全独立于波动率生成——如果它显著了,说明回归代码里有bug或变量构造时混入了未来信息。

参数说明:lognormal先生成正股成交量,是为了模拟真实成交量的右偏分布;uniform(0.3, 1.5)生成OIR,这个范围参考了美股常见个股的实际OIR区间。模拟回归里OIR不显著是预期结果,这恰好验证了回归代码不会“无中生有”地造出显著性。

这一步做完,你对代码链路的信心会强很多。真实数据复现时再出问题,就可以把注意力集中在数据质量上,而不是怀疑是估计函数用错了。

6.3 模拟数据与真实数据的差距在哪里

模拟数据能验证链路,但永远验证不了经济结论。三组gap必须清楚。第一,模拟数据没有跳跃与断点,真实数据里有财报暴雷、熔断、突发事件,这些极端事件会扭曲GARCH参数估计;第二,模拟数据没有杠杆效应,即收益率为负时波动率上升更快,真实数据里这一效应普遍存在,需要用EGARCH或GJR-GARCH才能捕捉;第三,模拟数据没有时变参数,真实世界里alpha和beta本身也在缓慢漂移,十年样本里用固定参数的GARCH(1,1)本身就是近似。

模拟数据是工程层面的保险丝,不是经济结论层面的论证。跑完模拟验证后,回到真实数据时要做好心理准备:系数可能不显著、方向可能反转、子样本结论可能分裂。这些不是代码bug,而是真实世界的复杂度。模拟链路帮我排除掉七成以上的低级错误,剩下的变量和模型选择问题,才是论文复现真正要花心思的地方。

6.4 完整复现后如何判断论文结论是否仍然成立

链路全通之后,最终判断分三步走:第一,系数符号与原论文一致吗?如果符号都反了,先假设自己的数据或指标构造有误,回头查日志再查代码,但不要直接认定论文是错的——更可能是样本期不同导致的真实差异。第二,显著性水平在子样本里稳定吗?如果只在某一时期显著,结论要降级为“特定市场状态下成立”。第三,换波动率代理指标后结论是否一致?GARCH和EWMA结果方向一致,说明结论对度量方式不敏感;不一致,说明结论的依赖条件要明确写出来。

三年的实际数据复现经验告诉我,论文复现最大的收获不是验证别人对错,而是被迫把数据、估计、检验每个环节的决策都暴露在阳光下。我现在的习惯是:拿到一篇论文,先花半天做模拟数据验证自己的代码链路,再花两天下载和清洗真实数据,最后半天跑回归。第一天晚上看到显著结果不要高兴,第二天早上换个样本期跑一遍再下结论。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询