做期权量化的人,大概都有过这样的阶段:把价格、成交量、MACD这些从股票时代带过来的老朋友直接搬到期权策略里,结果发现在隐含波动率和时间价值面前,传统指标的预测能力大打折扣。我真正开始重视一项冷门数据,是在一次回测连续打脸之后——那个数据就是指数期权持仓量。尤其是以沪深300、中证500这类宽基指数为标的的场内期权,持仓量变化并不是简单的多空人数对比,它更像一张机构资金每天都在更新底牌的记账本。这篇文章围绕指数期权持仓量变化指标展开,适合已经会用Python做数据清洗和回测、但对期权另类因子体系还不熟悉的读者。读完你会掌握持仓量PCR、最大持仓量行权价、单合约持仓异动这几个核心指标的计算方法,并知道如何在量化交易策略中真正使用它们,而不是停留在讲故事层面。
1. 为什么期权持仓量比期货持仓量更能读出资金底牌
很多从期货转过来的交易者会问:期货也有持仓量,为什么不直接用期货持仓量?答案是期权市场的持仓结构天然比期货复杂,但也正因为复杂,里面包含的信息维度更多。
1.1 先搞清楚持仓量代表的四种真实交易行为
期货的持仓量相对简单:多空双方各开一手,持仓量加1;多空双方各平一手,持仓量减1。期权虽然也是双边持仓,但买方和卖方并不是对称的“多空”关系,而是权利仓和义务仓的关系。每一笔成交都发生在四种组合之间:
| 成交双方行为 | 对持仓量的影响 | 典型含义 |
|---|---|---|
| 买方开仓 + 卖方开仓 | 持仓量增加 | 新资金入场,最值得关注 |
| 买方平仓 + 卖方平仓 | 持仓量减少 | 存量资金离场,趋势可能衰竭 |
| 买方开仓 + 卖方平仓 | 持仓量不变 | 有资金在换方向入场 |
| 买方平仓 + 卖方开仓 | 持仓量不变 | 有资金止盈后反手做卖方 |
这里最容易踩的认知坑是:持仓量不变不等于没有资金动作。恰恰相反,第三种和第四种情况往往才是最隐蔽的,有人在平掉原来的头寸,同时有人以相反身份接走头寸,但表面看持仓量毫无波澜。所以我在实际跟踪中从不只看单一数字,而是会把持仓量的日度变化和期权成交量结合起来看。
1.2 指数期权背后的资金画像更偏机构化
相比个股期权,指数类期权的参与者中有大量机构资金,包括保险资金、券商自营、量化私募和做市商。这些人使用期权的目的往往不是单纯看涨看跌,而是做备兑增强、买入保护、波动率管理、对冲尾部风险。这意味着指数期权的持仓结构天然带有“组合意图”,而不是散户式的裸多裸空。读持仓量,本质上是在读这些专业资金的资产负债表变化。当一个组合经理决定买入认沽保护现货头寸时,认沽端持仓量会上升;当他觉得市场恐慌已经释放、卖出认沽赚取权利金时,认沽端持仓量同样会上升。同样是认沽持仓增大,前者是防守信号,后者可能是进攻信号。如果不能结合价格位置和隐含波动率一起判断,指标就会失真。
1.3 持仓量变化为什么经常领先价格
道理其实很朴素:任何一笔需要新增仓位的交易,都会在成交之前完成资金调拨、风控审批和策略触发。机构资金体量大,建仓过程往往需要几天甚至数周,这个动作会提前反映在持仓量曲线上。等到价格真正突破关键位置时,持仓量的积累已经完成了。用传统技术分析术语说,这就是“量在价先”的期权版本,但期权持仓量比股票成交量的信息纯度更高,因为每一张持仓背后都是一份明确到行权价的合约,它不是笼统地告诉你“有人在买”,而是直接告诉你在什么点位、什么方向上有人在布局。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三个会被指数期权交易者忽略的持仓量核心指标
围绕指数期权持仓量变化,市面上最常见的指标是认沽认购比,也就是PCR。但只看一个PCR远远不够,我平时至少同时跟踪三个维度的指标,才敢把持仓量信号放进策略里。
2.1 持仓量PCR:一个需要反着读的情绪温度计
持仓量PCR的计算公式很简单:
[
OI_PCR = \frac{\sum 认沽期权持仓量}{\sum 认购期权持仓量}
]
很多人直接把PCR当成方向性指标,认为PCR高就是市场看空,其实是错的。持仓量PCR是个存量指标,它反映的是已经形成的仓位结构,而不是当下正在发生的买卖。当市场经历大跌后,大量资金买入认沽作为保护,OI_PCR会冲到很高;但此时继续追空的性价比往往已经很差,市场反而容易企稳反弹。反过来,当市场连续上涨、投资者争相买入认购,OI_PCR会被压得很低,此时往往对应情绪过热,距离阶段性顶部不远。
所以在我的框架里,OI_PCR是被当作一个反向情绪指标来用的,但绝不是简单的高抛低吸。我更关注它的分位数和变化速度。比如某品种OI_PCR的历史中枢在0.8附近,如果它快速冲到1.2以上、处于近一年90分位以上,同时在价格上又出现了明显的超跌形态,这时候反向做多的赔率才比较高。反过来,如果OI_PCR连续多日在低位徘徊并且还在缓慢下降,我会更警惕上涨行情的持续性。
这里需要特别提醒一个容易误用的场景:机构的大规模对冲需求会扭曲PCR的信号。比如券商场外衍生品业务大量发行雪球产品时,交易台会在期货和期权市场进行Delta对冲,这会系统性地改变认沽或认购持仓分布。这种结构性因素导致的PCR变化,并不代表市场情绪转向,而是业务曲线本身的副产品。因此,任何PCR策略在上线前都应当做一个简单的结构性检验:该品种持仓量PCR的变动,有多少可以由标准指数估值指标解释,有多少是真正的独立信息。
2.2 最大持仓量行权价:市场自己画出的支撑与压力带
如果说PCR告诉你市场的情绪温度,那么最大持仓量行权价就是在告诉你市场资金画好的关键坐标。方法也很直接:把当天同一到期月份所有认购期权按行权价分组,统计每个行权价上的总持仓量;认沽期权同样处理。找到持仓量最大的那一档行权价,这个价位往往就是多空资金博弈最激烈的位置。
实际操作中,我会同时看四个量:认购端最大持仓行权价、认沽端最大持仓行权价、全市场最大持仓行权价、当前标的价格位置。一种比较典型的格局是这样:当标的价格运行在认沽最大持仓行权价上方、同时在认购最大持仓行权价下方时,市场处于区间结构,那两个价位分别构成支撑和压力。一旦价格放量突破认购最大持仓行权价,上方大量持仓的卖方为了控制风险必须买入平仓,这会形成空头回补的推力,价格往往会出现快速抬升。
反之,如果价格跌破认沽最大持仓行权价,下方堆积的认沽买方会获得较大账面收益,而卖方则面临保证金追加压力,市场更容易形成负反馈。这个由持仓量画出的价格带,比很多人画的所谓整数关口更有实际意义,因为它是真金白银堆出来的位置。
我认为值得再强调一点:最大持仓量行权价不是一个静态坐标,它会随移仓换月动态漂移。每个月期权到期前一周,主力资金会开始逐步把仓位从近月搬到次月,此时近月合约的持仓量下降,次月合约的持仓量上升。如果直接使用全月份汇总数据,你看到的“最大持仓量行权价”实际上是新旧月份合约叠加后的混合物,信号会被严重钝化。我的习惯是对每个月分别计算一张表,主力合约单独看,遇到到期周则直接切换到次月数据。
2.3 单一行权价的持仓量异动:识别大资金的暗度陈仓
PCR和最大持仓量行权价属于截面统计,反应相对偏慢。如果想更早发现资金动作,需要盯单一行权价合约的持仓量突变。所谓异动,并不是指某个合约持仓量比前一天多了一千张这么简单,而是要结合该合约自身的流动性背景来判断。
我常用的筛选方法是先定义剔除异常的口径,把剩余到期天数不足5天的合约全部拿掉,避免换月因素干扰;然后计算每个合约的持仓量变化绝对值,以及它与过去20日平均成交量的比值。如果单日持仓量增幅超过过去5日平均成交量的0.5倍,且绝对值在相关品种上超过一定手数阈值,就标记为异常增仓。
单看增仓还不够,还要看隐含波动率的配合。因为在某个行权价上出现大量新增持仓,通常有两种可能:一种是大资金买入期权,无论买认购还是买认沽,都会直接抬高该合约的隐含波动率;另一种是大资金卖出期权,通过收取权利金建仓,隐含波动率往往不升反降。我一般用这个组合判断来过滤异动:如果持仓量暴增且隐含波动率同步上行,说明有人在主动买入波动率,方向性更强;如果持仓量暴增但隐含波动率平稳甚至下降,说明更可能是卖方在构建备兑、价差或卖出跨式组合,后续行情更容易进入低波动收敛阶段。
3. 从一张T型报价表到干净因子:持仓量数据的预处理实操
讲了这么多指标,如果没有一套靠谱的数据管线,一切都是空中楼阁。这一节直接给出我在本地跑通的处理流程,不涉及具体商业数据库,只需要每日期权行情快照就能复现。
3.1 原始数据到底要哪些字段
无论你用哪种数据源,最终每天收盘后拿到的应该是一张逐合约的期权行情表,至少需要包含以下字段:
| 中文含义 | 建议字段名 | 说明 |
|---|---|---|
| 交易日期 | date | 每一行代表某合约某一天的数据 |
| 合约代码 | contract | 唯一标识某个月份、某个行权价、认购或认沽 |
| 到期月份 | month | 也可直接用到期日 |
| 行权价 | strike | 数值类型,不要存成字符串 |
| 合约类型 | cp | 建议统一为C或P,不要混用大小写 |
| 收盘价 | close | 计算收益和波动率时需要 |
| 持仓量 | open_interest | 核心字段,单位通常是张 |
| 当日成交量 | volume | 辅助判断流动性 |
| 剩余到期天数 | days_to_expiry | 过滤近月垃圾数据用 |
很多数据源还会直接提供“当日持仓量变化”字段,但我一般建议不信任它,而是自己用历史快照去差分。原因很简单,一些数据商对合约调整、到期退市当天的处理不一致,直接把对方给的字段拿来做因子,很容易在月末或到期日产生假信号。
3.2 最容易被忽略的清洗步骤:合约过滤与去重
第一道清洗是格式统一。先确保行权价是数值类型,合约类型统一成大写,到期日解析成datetime格式。这一步看似简单,错就错在你会默认它没问题。
第二道清洗是去重。同一个合约在同一天不应该出现两行记录,但盘口接口偶尔会产生重复行,尤其是上午和下午行情拼接的时候。我一般是按date、contract这两个键去重,保留最后一笔记录。
第三道清洗是过滤临近到期合约。对持仓量类因子来说,剩余到期天数少于5天的合约基本没有参考价值,因为大量持仓会在到期前被迫平仓或移仓,数据噪声极高。我习惯统一保留剩余到期天数大于5天的合约再计算因子。
完成这三步之后的数据才算“能用”。这一步写的代码不复杂,但是偷懒的人后面会付出更多代价。
3.3 核心因子计算代码:从数据帧到可直接入模的因子表
下面给出因子计算的核心函数。这里假设你已经把原始数据读入DataFrame,字段名的映射也已完成。
python复制import pandas as pd
import numpy as np
def clean_oi_data(df):
"""统一字段并过滤近月合约。"""
df = df.copy()
df["cp"] = df["cp"].str.upper()
df["date"] = pd.to_datetime(df["date"])
df["strike"] = pd.to_numeric(df["strike"])
# 剔除剩余到期日不足5天的合约
df = df[df["days_to_expiry"] > 5].copy()
# 同一天同一合约只保留一行
df = df.drop_duplicates(subset=["date", "contract"], keep="last")
return df
def max_oi_strike_by_group(df):
"""找到每一期持仓量最大的行权价。"""
idx = df.groupby("date")["open_interest"].idxmax()
return df.loc[idx, ["date", "strike"]].rename(
columns={"strike": "max_oi_strike"}
)
def compute_oi_factors(df):
"""生成日频持仓量因子表。"""
df = clean_oi_data(df)
# 分别计算认购、认沽的合计持仓量
oi_pivot = df.groupby(["date", "cp"])["open_interest"].sum().unstack(fill_value=0)
oi_pivot["pcr_oi"] = oi_pivot["P"] / oi_pivot["C"].replace(0, np.nan)
# 分别计算认购端和认沽端的最大持仓行权价
call_df = df[df["cp"] == "C"]
put_df = df[df["cp"] == "P"]
call_strike = max_oi_strike_by_group(call_df)
put_strike = max_oi_strike_by_group(put_df)
factor = oi_pivot[["pcr_oi"]].reset_index()
factor = factor.merge(call_strike, on="date", how="left")
factor = factor.merge(put_strike, on="date", how="left", suffixes=("_call", "_put"))
return factor
运行完上面的代码,你会得到一张按日期索引的因子表,字段包括OI_PCR、认购端最大持仓行权价、认沽端最大持仓行权价。这张表可以直接和标的价格序列合并,用于后续策略开发。
3.4 滑动窗口标准化的经验值
原始因子不能直接入模,还要做标准化。我的经验是对OI_PCR使用滚动20日或60日的均值与标准差,计算滚动Z分数。为什么不用全局百分位?因为期权市场存在明显的制度变化,比如新品种上市初期流动性差、保证金规则调整、做市商参与度变化,这些都会导致持仓量中枢发生系统性偏移。滚动窗口能在一定程度上自动适应这种变化。
代码也很短:
python复制factor["pcr_zscore_20"] = (
factor["pcr_oi"] - factor["pcr_oi"].rolling(20).mean()
) / factor["pcr_oi"].rolling(20).std()
对最大持仓量行权价,我一般不用Z分数,而是把它和当前标的价格做差,再归一化到该品种行权价间距的倍数。比如沪深300股指期权的行权价间距是50点,假如标的价格为4000点,认沽最大持仓行权价为3800点,那么价格距离就是4倍间距,这个位置距离“关键支撑”还比较远,反而是中间区域移动得更快。
4. 把持仓量变化接入趋势过滤器:一个可回测的中低频框架
持仓量因子很少被单独当作开仓信号,更常见的做法是和趋势、均线或波动率突破叠加,作为过滤器来提升胜率。下面给出一个我在研究中反复使用的框架。
4.1 信号逻辑:持仓量极端值 + 关键位置突破
整个策略的设计逻辑是这样的:OI_PCR的滚动Z分数描述情绪状态,认购和认沽端的最大持仓量行权价描述价格坐标,两者结合,形成一组比较稳健的过滤条件。
我定义了一个合成信号,规则如下:
- 当OI_PCR的20日滚动Z分数大于1.0,同时标的价格已经向上突破认购端最大持仓量行权价时,视为偏多信号;
- 当OI_PCR的20日滚动Z分数小于-1.0,同时标的价格已经向下跌破认沽端最大持仓量行权价时,视为偏空信号;
- 其余时间为空仓状态。
为什么是这个方向?因为OI_PCR冲到历史高位,代表认沽保护盘已经很拥挤,市场情绪过度防御;这时候如果价格又能向上突破认购端堆积的重压区,说明空头保护需求已经无法压制价格上行,反弹的概率就会增加。反之,OI_PCR极低代表市场极度乐观、认购拥挤,配合价格跌破认沽最大持仓量行权价,往往意味着乐观情绪的瓦解。
这里有个职业交易者需要警惕的地方:最大持仓量行权价突破并不等于持续趋势。价格突破某一行权价位后,可能向上走一两档行权价就又回来。所以我不建议把该框架当成趋势跟踪系统,它更像一个捕捉“拥挤情绪反转”的择时阀。做出来的结果通常胜率尚可,但单笔盈利不一定大,必须配合分批止盈和移动出场规则。
4.2 回测框架的工程细节
由于持仓量数据是在当天收盘后才由交易所发布,任何负责的策略都不应该用当天的持仓量去产生当天开盘的交易信号。正确做法是把因子整体shift(1)一天,用昨天的持仓量信号去决定今天的仓位。这一条看起来简单,但在实际回测中是前视偏差的最大来源,稍不留意就会高估策略绩效。
下面是回测框架的核心部分,用的是ETF作为标的,这样对资金门槛和数据可得性都比较友好:
python复制def build_backtest_data(price_df, factor_df, max_position=1.0):
# price_df: 标的价格日线
# factor_df: 上一节生成的持仓量因子表
data = price_df.merge(factor_df, on="date", how="inner")
# 计算标的价格自身的5日均线,用于确认突破
data["ma5"] = data["close"].rolling(5).mean()
# 构造原始信号
data["signal_raw"] = 0
data.loc[data["pcr_zscore_20"] > 1.0, "signal_raw"] = 1
data.loc[data["pcr_zscore_20"] < -1.0, "signal_raw"] = -1
# 叠加最大持仓量行权价位置过滤
data["signal"] = 0
data.loc[
(data["signal_raw"] == 1) & (data["close"] > data["max_oi_strike_call"]),
"signal"
] = 1
data.loc[
(data["signal_raw"] == -1) & (data["close"] < data["max_oi_strike_put"]),
"signal"
] = -1
# 关键:持仓量信号滞后一天生效,避免前视偏差
data["position"] = data["signal"].shift(1).fillna(0)
return data
def run_backtest(data, cost_per_side=0.0003):
data = data.copy()
data["daily_ret"] = data["close"].pct_change()
data["strategy_daily_ret"] = data["position"] * data["daily_ret"]
# 扣除双边手续费与滑点,模拟成本按单边万三估算
data.loc[data["position"].diff().abs() > 0, "strategy_daily_ret"] -= cost_per_side
data["equity"] = (1 + data["strategy_daily_ret"]).cumprod()
data["benchmark_equity"] = (1 + data["daily_ret"]).cumprod()
return data
这里的cost_per_side为什么设成0.0003?因为如果你交易的是ETF,佣金加滑点的综合成本在正常流动性下大约就是万三左右。如果是中金所的股指期权或股指期货,成本结构和ETF不太一样,需要单独计算。千万别小看这个参数,同样的信号,成本从万1提高到万5,最终年化收益可能从正变成负,很多漂亮的因子曲线就是这么被成本吃掉的。
4.3 参数要怎么调才不过拟合
单独用上面两个阈值实际上是非常粗糙的。同一套参数在沪深300ETF期权上可能有效,但换到中证500或者创业板ETF期权上,由于行权价间距、期权规模、机构参与度都不同,Z分数的经验阈值需要重新标定。
我的做法是对每个品种单独做一次参数扫描,观察1.0和-1.0附近参数平原是否稳定,而不是找最优解。具体说,我把Z分数的开仓阈值从0.5到1.5每隔0.1扫一遍,然后看夏普比率的变化。如果只有某一个参数点表现特别好,两侧迅速恶化,我基本会放弃这个因子;如果在一个较宽的区间内表现差不多,才说明因子具有真实信息含量。稳定比好看重要得多。
这里再补充一个容易被忽略的样本切分:期权类因子一定要把样本分成“正常波动期”和“极端波动期”来检验。比如2024年初A股市场出现连续急跌时,持仓量PCR往往会冲到历史极值,表面看Z分数给出了清晰的看多信号,但如果价格还在下跌趋势里面,过早接飞刀一样会被打止损。所以任何用OCR计算的择时策略,必须叠加一条硬风控规则:价格没有站上短期均线之前,不做多;价格没有跌破短期均线之前,不做空。这也是为什么我在回测代码里先算了一个ma5,实践中趋势过滤器的参数可以放大到10日或20日,但绝不能完全没有趋势约束。
5. 实盘跟踪中真正让我吃过亏的持仓量陷阱
最后这部分才是真正的含金量。很多报告里不会写,但一次实盘失误会让你记住很久。
5.1 到期日效应:信号在月末会突然失真
每个月第四个星期三附近,当月期权到期,大量虚值合约持仓归零,持仓量会出现断崖式下跌。如果你直接把全月份的OI加总,PCR会突然跳变,最大持仓量行权价也经常从近月跳到次月。这种跳变带来的信号,基本是垃圾信号。我在早期的策略回测中吃过一次大亏,当时不知道为什么PCR在某个周五突然暴涨,系统开了一堆多头仓位,结果下周一市场平淡震荡,策略白白支付了手续费和持仓成本。后来排查原因,就是到期日虚值合约集中归零造成的假信号。所以第一道防线是:每次计算因子前,把剩余到期天数小于5天的合约全部剔除;第二道防线是:在每月到期周,强制把主力合约切换到次月。
5.2 移仓换月会给单合约因子注入虚假增量
除了到期归零,移仓换月还会制造“此消彼长”的假象。近月合约持仓量下降,次月合约持仓量上升,单看每个合约的日度增量,会误以为市场在大量平多开空或者反向移仓。特别是在大资金集中换仓的日子里,你会发现次月认购合约的增仓量异常巨大,单合约拣选信号会频繁触发。这个问题目前的处理办法有两个:一是如果做截面汇总类指标,必须确保近月和次月分开计算、不做混合;二是如果做单合约异动类信号,必须用“主力合约占全月持仓的百分比”做过滤,当近月合约持仓占比快速下滑时,暂停单合约异动信号的触发。
5.3 数据源的口径差异隐藏很深
这可能是最坑的一件事。不同数据服务商对“持仓量”的统计口径存在差异。有的给单边持仓,有的给双边持仓;ETF期权和中金所股指期权在公布的持仓量口径上本身就不一样;有的会把做市商持仓剔除,有的不会。如果你用一套代码同时处理多个数据源而没有做口径校验,逻辑上看起来非常严谨,结果却可能完全不可比。这里分享一个实用的校验方法:用“全市场认购总持仓+认沽总持仓”与交易所官方公布的总持仓量对比,误差超过1%就要警惕数据源问题。ETF期权每日的总持仓量是可以在交易所官网查到的,股指期权则在中金所官网每天盘后公布。花十分钟做一次对齐,能帮你省下后面几十个小时的返工。
5.4 持仓量与隐含波动率联动是最后一道确认法
持仓量数字本身不含方向信息,要确认它究竟是买方行为还是卖方行为,必须借助隐含波动率。之前在第2.3节我提过一次,这里再展开讲一下实际操作。假设我观察到某个行权价的认购期权单日持仓量增加5000张,第一反应不是追多,而是去查这个合约的隐含波动率变化。
- 如果隐含波动率同步上升,更大概率是买方推动,代表资金愿意为未来上涨支付更高权利金,方向性偏强;
- 如果隐含波动率没有明显变化,甚至下降,大概率是卖方增加仓位,是机构在卖出虚值认购做备兑或做波动率空头,并不代表看涨,反而可能构成该行权价上方的压力。
反过来,认沽侧持仓量增加时也一样。认沽持仓暴增+隐含波动率上升,是典型的买保护或做空动能强;认沽持仓暴增+隐含波动率不动,则可能是卖出认沽的资金在下方建仓,这种情况经常出现在机构资金愿意在某个点位“接货”的时候。只看持仓量不看波动率,是很多期权量化新手最容易犯的错误。
5.5 别把持仓量指标当成圣杯
如果把上述所有指标叠加起来做成一个策略,你可能会发现它在震荡市里的表现还不错,但在趋势快速展开时经常滞后。原因不复杂:持仓量数据本身就是日度低频数据,再加上T+1公布的限制,它更适合做中低频的择时和过滤,不适合做高频进出场的触发器。我的习惯是,把持仓量因子用在以下几个场景:一是在指数增强型的量化交易策略里做仓位管理,当OI_PCR出现极端值时减仓或加仓;二是作为趋势策略的过滤器,只在持仓量结构配合时才允许开新仓;三是作为事件驱动策略的确认信号,例如在重要政策或宏观数据公布前后,观察持仓量异常变化来调整方向判断。
我对持仓量变化指标的最终实践体会
跟踪指数期权持仓量已经好几年,如果非要提炼一句最核心的体会,那就是:持仓量变化不是水晶球,而是机构资金留下的脚印。脚印只能告诉你有人走过,但具体是去抄底还是去接飞刀,还要结合行权价位置和隐含波动率来判断。把OI_PCR、最大持仓量行权价、单合约异动这三个维度组合起来使用,即使不做成复杂模型,已经能过滤掉大量无效交易。最后送上一句经验:任何持仓量信号上线前,都要先做一遍“逾期一个月数据不用、到期周强制切换、成本按万五估算”的压力测试;能活过这三关的策略,才值得放进实盘。期权杠杆高,信号容错率低,希望你少踩几个我当年踩过的坑。
