Python实现上证50ETF期权波动率策略回测框架全解析
2026/9/10 18:26:13 网站建设 项目流程

简介:面向期货与期权交易策略研究的Python回测源码包,聚焦波动率策略的收益验证与信号执行,适合金融量化交易学习者、期货期权投资者及有Python基础的程序化交易爱好者。通过历史行情模拟、策略信号生成与持仓调整等模块,用户可在本地搭建一套完整的策略回测流程,用于评估不同市场波动场景下的风险收益表现。压缩包共23个文件,约5.49MB,以7个Python脚本和5个Excel工作簿为主,另有4个编译后pyc文件、3个XML配置及说明文档等;Python脚本覆盖波动率计算、持仓调整、模拟交易、期权交易和信号生成等环节,Excel文件则用于存储历史数据、对冲仓位等回测结果,整体目录清晰、便于二次开发。目前已有177人学习下载。资源价值在于既提供可直接运行的回测框架,又保留原始数据与输出文件,便于学习策略参数优化、分析回测细节并迁移至个人交易思路中。

1. 一个波动率策略回测项目,值不值得拆?

做场内期权的人应该都有过这种经历:手工在Excel里拉历史数据,用BS公式反推隐含波动率,再对照标的价格手动记录账户盈亏。整套流程下来,数据对不上、口径不统一、复盘时根本说不清某笔亏损到底是策略问题还是执行问题。这个项目就是为了解决这个问题诞生的。它把波动率计算、交易信号生成、持仓调整、模拟撮合和收益归因串成了一条完整的本地回测链路,数据源是上证50ETF期权和对应期货合约的历史行情,脚本之间通过Excel落盘交换数据,结构清晰,适合想自己搭一套衍生品策略研究框架的人直接改着用。接下来我会按数据如何组织、波动率怎么算、信号如何触发、调仓和撮合怎么衔接这个顺序,把这个项目拆开讲。

2. 回测前先理顺数据:Excel工作簿在项目里到底承担什么角色

2.1 三张核心数据表的字段与用途

拿到源码包,先别急着看Python脚本。先把5个Excel工作簿打开看一遍,你会发现这个项目的核心设计思路是“数据文件即接口”。也就是说,每个Python脚本负责一个计算环节,脚本与脚本之间不直接传递变量,而是通过读写Excel文件来交换中间结果。这个设计对回测项目来说其实非常实用,因为每一步中间结果都能被打开检查,任何一步出了问题,都能直接定位到具体数据。

先看futures.xlsx和sse50ETF.xlsx这两张表。它们存放的是期货合约和ETF标的的历史行情,字段基本一致,都以日期为主键,包含开盘价、最高价、最低价、收盘价、成交量、持仓量这六列。这里有一个值得注意的细节:futures.xlsx里的合约代码字段是类似“IH2106”这样的格式,这说明数据源是股指期货而非商品期货,与上证50ETF期权形成对冲组合时,delta对冲工具就是它。如果你打算把手里的数据换成其他品种,只要保证字段名一致、日期对齐,代码本身不需要改动。

options.xlsx是期权合约的行情快照表,这张表的列结构比前两张复杂得多。它记录了每个交易日的全部期权合约,包括认购/认沽标识、行权价、到期月份、当日收盘价、结算价,以及由系统计算出的隐含波动率。这里有一个常见坑需要提前说明:盘中快照只记录了“当日最后一笔成交”,如果标的在尾盘出现大幅波动,这一笔成交价可能偏离真实价值。建议在回测时用结算价而非收盘价作为期权合约的估值基准,因为结算价经过了交易所的加权平均计算,更能反映合约当日的公允水平。

2.2 日期对齐与数据清洗的必要性

回测引擎跑出来的结果是否可信,80%取决于数据是否对齐。这个项目里,三张行情表来自三个不同的数据源,它们的交易日期集合并不是完全重合的。期权市场、期货市场、ETF现货市场虽然都以交易所交易日为基础,但由于最后交易日、行权日规则不同,偶尔会出现某一天股指期货有交易但期权没有报价的情况。如果不做对齐,后续计算波动率时就会出现NaN值传播,最终导致策略在某个时间点突然仓位变成零。

import pandas as pd opt = pd.read_excel('options.xlsx', sheet_name='all', parse_dates=['date']) fut = pd.read_excel('futures.xlsx', parse_dates=['date']) etf = pd.read_excel('sse50ETF.xlsx', parse_dates=['date']) opt = opt.set_index('date') fut = fut.set_index('date') etf = etf.set_index('date') common_idx = opt.index.intersection(fut.index).intersection(etf.index) opt = opt.loc[common_idx].ffill() fut = fut.loc[common_idx].ffill() etf = etf.loc[common_idx].ffill()

逻辑说明:先用三张表的日期索引做交集,只保留三个市场都有交易的日期,然后按这个共同日期序列重新索引并向前填充。如果某一天期权表没有数据,ffill()会用最近一个交易日的价格填充,保证后续计算不会断档。

参数说明:ffill()只适用于缺失值极少且行情波动不大的场景。如果某段行情出现连续两天缺失(例如春节长假前后),用前值填充会严重失真。我一般会加一个日期间隔检查逻辑,超出3个自然日没有行情的就主动跳过该周期,而不是强制填充。另外,parse_dates这个参数在读取时要把日期列明确指定出来,否则Pandas会把日期当成字符串,排序时得出完全错误的结果。

2.3 回测起点与结束点的选择

很多人在设计回测时有一个误区,觉得历史数据越长越好。但在期权回测里,数据长度要服从于合约流动性和波动率结构。在有据可查的行情区间内,50ETF期权的隐含波动率经历了从高位回落到均值下方的完整周期,数据本身具备代表性,但如果你把数据起点前移,会发现那时的期权合约数量极少、买卖价差极大,回测结果被交易成本吃掉大半,策略真实的择时能力反而看不出来。

这里建议的起点是第一次出现“当月合约同时存在4个不同行权价且有成交记录”的日期。判断标准很简单:如果某一天options.xlsx里当月认购合约的数量少于4条,说明市场深度不足,这一天应当从回测区间中剔除。这一点在源码里没有体现,需要在预处理阶段手工完成。同样,回测结束时点不要选在最后一天,建议留出至少5个交易日作为观察窗口,因为涉及期权移仓换月的动作需要时间完成。

3. 波动率计算模块:从历史波动率到EWMA的动态切换

3.1 为什么先看VolatilityCalculating.py

这个项目的核心计算引擎是VolatilityCalculating.py。策略的买卖决策全部建立在波动率的水平判断和趋势判断上,所以这一章的算法直接决定信号质量。打开这个脚本看,会发现它实现了两种波动率估计方法:一是普通历史波动率,即标的收益率在过去N日的标准差年化值;二是EWMA(指数加权移动平均)波动率,它给近期的收益率更高的权重,能够更快反映波动率水平的变化。

这两种方法对应的是两种完全不同的策略假设。用长期历史波动率作为基准线,适合做均值回归类策略——当短期波动率远高于长期均值时做空波动率,反之做多;用EWMA波动率作为动态阈值,适合做趋势跟踪类策略——波动率快速上升时意味着市场开始剧烈分化,此时应当降低仓位或买入保护性期权。一个完整的波动率策略框架,通常需要同时保留这两种估计,并把它们的比值作为信号来源。

import numpy as np def historical_volatility(close: np.ndarray, window: int = 20) -> np.ndarray: log_ret = np.log(close[1:] / close[:-1]) hv = np.full(len(close), np.nan) for i in range(window, len(close)): hv[i] = np.std(log_ret[i - window:i], ddof=1) * np.sqrt(256) return hv def ewma_volatility(close: np.ndarray, span: int = 10) -> np.ndarray: log_ret = np.log(close[1:] / close[:-1]) ewma_var = pd.Series(log_ret).ewm(span=span, adjust=False).var() return (ewma_var * 256).pow(0.5).values

逻辑说明:historical_volatility函数先计算相邻收盘价的对数收益率,然后用np.std求滚动窗口内收益率的标准差,最后乘以年化因子。注意这里使用了ddof=1,即样本标准差而非总体标准差,这是因为我们是用样本去估计总体波动率,用ddof=0会系统性低估波动率,导致后续信号阈值偏高。

参数说明:窗口期window=20是按月度交易日数设定的,约等于一个自然月的交易日数量。如果你做的是高频调仓策略,这个窗口要缩到5到10;如果做的是季度级调仓,可以放宽到60。年化因子用的是256而不是365,这是国内市场的行规,因为A股及衍生品市场一年的实际交易日大约在242到256天之间,用365会直接高估年化波动率,进而影响期权定价和保证金计算。span=10在计算EWMA时表示指数衰减的滞后期数,数字越小对近期数据越敏感,建议在10到20之间调试,不要低于5,否则噪声会被放大到无法区分信号的程度。

3.2 波动率状态的判定逻辑

VolatilityCalculating.py的输出不仅仅是两条波动率曲线。脚本会把短期EWMA波动率与长期历史波动率做比较,生成一个波动率状态标记。当EWMA值向上穿越历史波动率1.2倍时标记为高波动状态,向下穿越0.8倍时标记为低波动状态。这两个阈值是经验值,源码里直接硬编码为常量,读脚本时会看到。

这个设计有一个值得学习的地方:它不是简单比较两个数值的大小,而是引入了滞回区间。这样做的好处是避免波动率在临界点附近反复穿越导致信号频繁开平仓。1.2和0.8这个带宽并非对称,目的就是让进入高波动状态的难度略高于退出,天然带有一定的趋势跟踪特征。如果你要修改这个逻辑,注意不要只改某一侧的阈值,两侧要同步调整,否则会出现“进入容易退出难”的单向锁死现象。

3.3 波动率数据在策略里的实际用途

波动率数据在策略文件里有两个用途:一是确定整体仓位水平,二是决定期权合约的选择。当系统判定当前处于低波动状态时,策略会倾向于卖出跨式组合来获取时间价值,此时选择平值附近的合约,因为平值期权的Theta绝对值最大,时间价值衰减最快;当系统判定进入高波动状态时,策略会转为买入价外合约来对冲尾部风险,此时选择虚值一到两档的合约,在保留一定安全垫的同时控制权利金成本。

这个逻辑在源码中表现为一个状态变量,策略信号模块在每根K线收盘后读取这个状态,再决定接下来的动作。需要说明的是,这个项目里的波动率值是滞后指标,它是用收盘价数据计算出来的,所以当信号触发时价格已经是收盘价了。如果你用这个系统做盘中实盘参考,需要把计算频率从日线级别提升到分钟级别,否则信号会晚半天。

4. 信号生成与调仓执行:TradingSignal到ChangePosition的完整链路

4.1 TradingSignal.py如何把波动率状态转成交易动作

TradingSignal.py这个脚本是策略层和执行层之间的桥梁。它读取VolatilityCalculating.py输出的波动率状态序列,再结合当天标的价格、期权合约的隐含波动率数据,生成具体到合约的买卖信号。信号不是简单的“买入”或“卖出”,而是带方向、带合约选择条件、带仓位权重的结构化指令。

def generate_signal(hv_series: pd.Series, ewma_series: pd.Series, option_chain: pd.DataFrame): signals = [] high_vol_thresh = 1.2 low_vol_thresh = 0.8 for i in range(1, len(hv_series)): h = hv_series.iloc[i] e = ewma_series.iloc[i] ratio = e / h if h != 0 else 1.0 if ratio > high_vol_thresh: sig = 'BUY_PUT' elif ratio < low_vol_thresh: sig = 'SELL_STRANGLE' else: sig = 'HOLD' signals.append({'date': hv_series.index[i], 'signal': sig, 'ratio': ratio}) return pd.DataFrame(signals)

逻辑说明:信号生成的核心是计算当前EWMA波动率与历史波动率的比值。当比值大于1.2时说明波动率处于快速上升阶段,此时买入认沽期权对持仓进行保护;当比值低于0.8时说明市场进入低波动状态,此时卖出宽跨式组合来赚取时间价值。这里的SELL_STRANGLE是一个组合动作,需要在期权链上同时选择一份虚值认购和一份虚值认沽合约。

参数说明:high_vol_threshlow_vol_thresh的取值决定策略对波动率变化的敏感度。1.2和0.8的组合,在50ETF期权历史数据上大约每年触发3到5次方向性转变,频率适中。如果把阈值改成1.1和0.9,信号触发次数会增加一倍,但假信号的比例也会显著上升,因为波动率在小幅波动时会频繁穿越阈值。这个参数强烈建议用前文提到的滞回区间来平滑。另外,信号判断用的是收盘后的数据,所以生成信号的时间点实际上比执行时间晚了一个交易日,这是日线频次回测的标准做法——T日收盘生成信号,T+1日开盘执行,可以避免前视偏差。

4.2 持仓调整的边界条件与规则

ChangePosition.py的核心任务是接收交易信号并执行仓位调整。这个脚本里有一个重要概念叫做“目标仓位”,它不等于信号本身,而是信号经过风险约束过滤后的结果。比如信号说要买入认沽期权,但当时账户里已有的认沽合约数量已经超过总资金比例的阈值,此时信号会被降权执行或者直接取消。

def adjust_position(current_pos, signal, price, risk_limit=0.2): if signal == 'BUY_PUT': target_delta = -0.15 elif signal == 'SELL_STRANGLE': target_delta = 0.0 else: return current_pos new_pos = current_pos.copy() pos_value = sum(abs(p['qty'] * p['unit_price']) for p in current_pos) if pos_value > risk_limit: scale = risk_limit / pos_value for p in new_pos: p['qty'] = max(0, int(p['qty'] * scale)) return new_pos

逻辑说明:脚本通过设置target_delta来控制组合对标的涨跌的敏感度。买入认沽期权时目标delta设为负值,表示希望组合在标的下跌时获得正收益;卖出宽跨式时目标delta设在0附近,表示保持市场中性的思路。risk_limit是组合总市值的比例上限,防止单次调仓投入过大。当现有持仓市值已经超过上限时,会对新开仓量进行等比缩放。

参数说明:risk_limit=0.2意味着组合中有期权持仓的市值不超过总资金的20%,剩下来的资金用于期货保证金和应急。这个数值在实盘中要根据账户保证金比例做调整。使用这个参数可以看到,new_pos里的乘数scaletarget_delta没有联动关系,这在某些极端行情下会出现目标delta到了但仓位过轻的情况,跑回测时可以把这两个约束解耦,先控制风险,再追求delta命中。

4.3 OptionsChange.py在调仓中的位置

OptionsChange.py这个脚本在项目里的作用是处理期权到期移仓和合约替换。期权是有到期日的,当月合约临近到期时流动性会急剧下降,买卖价差扩大到无法接受的程度,这时候需要把持仓从当月合约移到下月合约。这个过程不是简单的平仓再开仓,因为涉及到delta的连续性保持。

实际执行时,通常在到期日前第5个交易日启动移仓流程。算法会先计算当前持仓合约的剩余delta敞口,然后在下月合约中选择一个合约,使替换后的组合delta与移仓前基本一致。移仓操作是在一个交易日内逐笔完成的,而不是一次性全部平仓再全部开仓,这是为了减少对市场的冲击。对于回测系统来说,这里需要做的是在模拟撮合时添加一个额外的滑点系数,因为移仓操作实际产生的成本通常比常规开平仓高出20%左右。

5. 模拟交易执行:SimulatedTrading与OptionsTrading的撮合差异

5.1 同步撮合与异步撮合的选择

回测系统里模拟撮合的方式直接决定结果的真实性。源码包里的SimulatedTrading.py和OptionsTrading.py分别对应两类资产的不同撮合模式。期货部分用的是同步撮合——信号发生在T日收盘后,成交价取T+1日的开盘价,交易即成交,没有滑点和延迟;期权部分用的则是异步撮合——信号触发后,按照指定的限价单价格在后续K线中持续等待成交,如果当天没有触及委托价,则当日不成交,信号视为失效。

期权之所以不用同步撮合,是因为期权报价的价差远大于期货,而且50ETF期权的tick数据存在明显的买卖价差跳跃。如果假设所有信号都能以开盘价成交,回测结果会高估策略收益,尤其是在卖出开仓的策略中,实际成交价往往比理想价低几个tick,长期累积下来对收益的影响不容忽视。在这个项目的默认配置里,期货的滑点设置为0.5个最小变动价位,期权的滑点设置为1.5个最小变动价位,合约越小、流动性越差,滑点设置应当越高。

5.2 保证金与资金占用

期货和期权的保证金制度完全不同,回测时必须分别处理。期货端采用的是交易所标准保证金率加期货公司上浮的固定比例,上证50股指期货的保证金比例约为12%到15%;期权端则要按组合保证金规则计算,卖出跨式组合在交易所层面有优惠,保证金要求会低于简单加总两张期权合约的保证金。

def calc_margin(position_type, option_price, strike, etf_price, multiplier=10000): if position_type == 'future': return etf_price * multiplier * 0.15 elif position_type == 'option_sell': return max(option_price * multiplier + 0.12 * etf_price * multiplier * 0.15, option_price * multiplier * 1.2)

逻辑说明:期权卖方保证金的计算采用了交易所通用的公式,取“权利金加标的保证金的一定比例”和“权利金的固定倍数”两者的较大值。multiplier=10000表示期权合约单位是10000份,ETF价格乘以乘数得到一手的名义市值。计算时要注意,卖出期权所收取的权利金可以抵扣保证金,这个逻辑在源码中体现为第一项里包含的option_price * multiplier

参数说明:0.12是交易所规定的认购期权虚值部分的扣除比例,实值程度越深,这个系数越低。实盘中券商会在这个基础上上浮2到3个百分点,回测时最好把整体保证金率保持在15%左右,否则在极端波动下会出现账户保证金不足的假信号,导致系统误判仓位上限。资金占用这一块在回测报告里容易被忽略,但它直接影响后续调仓的自由度,资金利用率高的策略往往更依赖这一块的准确计算。

5.3 模拟撮合的主循环设计

SimulatedTrading.py的主循环结构决定了整个回测的执行速度和可调试性。通常的做法是逐日遍历行情数据,每一天先检查挂单是否成交,再处理信号、生成新订单、更新持仓市值。这个主循环的执行顺序非常关键,如果把信号生成放在挂单检查之前,就会出现同一天生成的信号当天立即成交的bug,这在真实交易中是不可能的。

for trading_date in trading_dates: fill_order(pending_orders, trading_date) daily_signal = get_signal(trading_date) order_list = build_orders(daily_signal) pending_orders = execute(order_list, trading_date) update_portfolio(trading_date)

逻辑说明:循环的执行顺序是“先成交后生成”,即先处理之前挂出的等待成交的订单,再计算当天的信号。这样做可以确保信号建立在当天收盘数据的基础上,订单则从下一个交易日开始生效,避免使用未来数据。fill_order函数内部会判断委托价格与当天最低/最高价的关系,当限价单价格落在当天价格区间内时成交。

参数说明:pending_orders是一个订单队列,每天成交完未能成交的订单会继续保留在队列中。这里要注意,不建议把订单保留超过3个交易日,否则会出现“陈年旧单”在很久以后突然成交的情况,这在真实交易中极不现实。可以设置一个最大存活天数参数,超过期限的订单直接撤销,并在日志中记录撤销原因。

5.4 回测结果表outcome的工作机制

交易结束后,系统会把每一天的持仓明细、账户权益、组合delta输出到outcome工作簿中。文件名里包含hedge_delta_position.xlsx和pure_delta_position.xlsx两个文件,两者的区别在于前者是经过期货对冲后的组合delta,后者是纯期权持仓的裸delta。对比这两张表的差异,可以直接看出期货对冲对整个组合风险敞口的影响。

测试时可以先跑一次纯期权持仓的回测,观察净值波动幅度,再加上期货对冲重新回测,对比最大回撤和夏普比率。如果两者的差异不大,说明期权组合本身的delta敞口已经很小,期货对冲的贡献有限;如果差异极大,说明持仓暴露风险很高,对冲动作是必要的。这一组对照实验是验证整个回测系统有效性的一个重要维度,改好参数后记得重跑这两个场景。

6. 从两层验证到参数敏感性:这组源码的进阶用法

回测脚本的核心价值不在于看最终收益数字,而在于验证“策略在不同参数下的表现稳定性”。Test.py负责的就是这个功能,但它的作用不只是跑一次冒烟测试,更重要的是暴露策略中那些对参数极其敏感的部分。以波动率阈值为例,把高波动阈值从1.2调到1.1再调到1.3,分别记录交易次数、胜率和最终收益,如果三个结果差异巨大,说明策略在阈值选择上没有冗余度,换一段行情可能就会失效。

验证仓位调整逻辑是否发生不合理的跳变,可以打开hedge_delta_position.xlsx,检查组合delta的时间序列。正常情况下,delta应该在一个相对窄的区间内缓慢变化。如果发现在某个日期delta从正0.2直接跳到负0.3,说明当天发生了整体平仓并反向开仓的操作,这种操作在真实交易中会产生非常高的交易成本和滑点损耗。对于这种跳变,可以在ChangePosition.py中加入最大单日调仓比例参数,限制单次调仓的合约数量上限,从而平滑换仓过程。

最后一个值得细看的地方,是期权持仓表中到期日的集中度分布。如果某个到期月份同时积累了多个方向的仓位,到期日前后可能会出现流动性集中的问题。这个项目里的Excel工作簿允许你直接筛选同月份的持仓记录,提前发现这种集中风险。更稳妥的做法是在OptionsChange.py里增加一个“最大单月持仓比例”的约束——当某个月份合约总市值超过组合的30%时,提前执行移仓操作。这虽然会付出额外的交易成本,但从实盘角度看,能让组合在时间维度上保持分散,避免因单一月份的流动性枯竭导致被迫在最不利的价格平仓。

用这种思路去检查每一处硬编码参数,你会发现自己对策略行为的理解会比单纯看回测净值曲线更精准。这些验证方法不需要额外安装第三方库,只需要项目自带的Pandas和NumPy就能完成,在把这套系统接入实盘交易或者更大的研究框架之前,值得先把这套体检流程完整过一遍。

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

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

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

立即咨询