问题描述
解决方案
这是一个基于聚宽(JoinQuant)平台的**期权牛市价差(Bull Call Spread)**策略。
策略原理
牛市价差策略是指在看涨标的资产,但预期涨幅有限或希望降低权利金成本时使用的策略。
- 操作:买入一份较低行权价($K_L$)的认购期权(Call),同时卖出一份较高行权价($K_H$)的认购期权(Call)。两份期权属于同一标的、同一到期日。
- 收益:标的大涨时获利,最大收益锁定为 $(K_H - K_L) - \text{净权利金支出}$。
- 风险:标的大跌时亏损,最大亏损锁定为初始支付的净权利金。
策略代码
以下代码以 50ETF (510050.XSHG) 为标的,构建牛市价差组合。
# -*- coding: utf-8 -*-
import jqdata
import math
import datetime
import re
def initialize(context):
"""
初始化函数
"""
# 设定基准
set_benchmark('510050.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# 全局变量设置
g.underlying = '510050.XSHG' # 标的资产:50ETF
g.trade_day_num = 0 # 记录交易天数
# 策略参数
g.spread_width = 1 # 价差宽度(档位),例如买入平值,卖出虚值1档
g.close_days_before_expire = 2 # 距离到期日几天前平仓
# 每天开盘时运行
run_daily(trade_logic, '09:30')
def trade_logic(context):
"""
每日交易主逻辑
"""
# 1. 检查持仓,如果临近到期,则平仓
check_and_close_positions(context)
# 2. 如果没有持仓,则开仓
if len(context.portfolio.positions) == 0:
open_bull_spread(context)
def check_and_close_positions(context):
"""
检查持仓并平仓
"""
if len(context.portfolio.positions) == 0:
return
# 获取当前持仓的一个合约(假设都是同一月份到期)
holding_code = list(context.portfolio.positions.keys())[0]
info = get_security_info(holding_code)
# 计算距离到期日的天数
days_to_expire = (info.end_date - context.current_dt.date()).days
# 如果距离到期日小于设定阈值,或者已经过了最后交易日,全平
if days_to_expire <= g.close_days_before_expire:
log.info("合约临近到期(剩余%d天),平仓所有头寸" % days_to_expire)
for code in context.portfolio.positions.keys():
order_target(code, 0)
def open_bull_spread(context):
"""
开仓牛市价差组合
"""
# 1. 获取标的当前价格
underlying_price = get_current_data()[g.underlying].last_price
# 2. 获取所有期权合约
# 注意:这里获取的是当天的所有期权,需要筛选
options = get_all_securities(types=['option'], date=context.current_dt.date())
if options.empty:
return
# 3. 筛选出50ETF的认购期权(Call)
# 命名规则通常包含标的名称,且认购包含"购"
call_options = []
for code, row in options.iterrows():
# 简单筛选:代码对应50ETF,且是认购期权,且未退市
# 注意:实际生产中应通过 underlying_symbol 属性筛选,这里简化通过名称判断
if '50ETF' in row['display_name'] and '购' in row['display_name']:
# 排除已经到期或即将到期的(比如当天就是到期日)
if row['end_date'] > context.current_dt.date() + datetime.timedelta(days=g.close_days_before_expire + 5):
call_options.append(code)
if not call_options:
log.info("未找到合适的认购期权合约")
return
# 4. 按到期日分组,选择最近的一个主力月份(通常是当月或次月)
# 获取合约详细信息以得到到期日
option_infos = [get_security_info(code) for code in call_options]
# 找到最近的到期日
sorted_dates = sorted(list(set([x.end_date for x in option_infos])))
target_date = sorted_dates[0]
# 筛选出该到期日的所有合约
target_month_options = [x for x in option_infos if x.end_date == target_date]
# 5. 解析行权价并排序
# 聚宽API没有直接给出strike_price字段,通常需要从display_name解析
# 格式如 "50ETF购8月3000",最后的数字即为行权价
options_with_strike = []
for info in target_month_options:
strike = parse_strike_price(info.display_name)
if strike:
options_with_strike.append({'code': info.code, 'strike': strike, 'name': info.display_name})
# 按行权价从小到大排序
options_with_strike.sort(key=lambda x: x['strike'])
if not options_with_strike:
return
# 6. 寻找平值合约(ATM)作为买入腿
# 找到行权价最接近标的当前价格的合约
atm_option = min(options_with_strike, key=lambda x: abs(x['strike'] - underlying_price))
atm_index = options_with_strike.index(atm_option)
# 确定买入合约(低行权价)
buy_leg = atm_option
# 确定卖出合约(高行权价)
# 卖出虚值合约(行权价 > 标的价格),这里简单取 ATM 向上 g.spread_width 档
sell_index = atm_index + g.spread_width
if sell_index >= len(options_with_strike):
log.info("无法找到足够高行权价的卖出合约")
return
sell_leg = options_with_strike[sell_index]
# 再次确认是牛市价差:买低行权价,卖高行权价
if buy_leg['strike'] >= sell_leg['strike']:
log.info("行权价选择错误,未开仓")
return
log.info("标的价格: %.3f" % underlying_price)
log.info("构建牛市价差: 买入 %s (行权价%.3f), 卖出 %s (行权价%.3f)" %
(buy_leg['name'], buy_leg['strike'], sell_leg['name'], sell_leg['strike']))
# 7. 下单
# 买入认购
order(buy_leg['code'], 1)
# 卖出认购
order(sell_leg['code'], -1)
def parse_strike_price(display_name):
"""
从期权名称中解析行权价
例如: "50ETF购8月3000" -> 3.000
"""
try:
# 提取名称末尾的数字
match = re.search(r'(\d+)$', display_name)
if match:
strike_str = match.group(1)
# 50ETF期权通常行权价是放大了1000倍显示的,比如3000代表3.0
# 但具体倍数可能随交易所规则变化,这里假设是标准格式
# 也可以通过 get_price 获取价格反推,或者假设名称最后就是行权价
# 聚宽数据中,50ETF期权名称通常是 "50ETF购X月2.100" 或者 "3000" 形式
# 如果包含小数点
if '.' in display_name:
match_float = re.search(r'(\d+\.\d+)$', display_name)
if match_float:
return float(match_float.group(1))
# 如果是整数,通常需要除以1000 (针对50ETF期权旧命名规则)
# 现在的命名规则很多直接带有小数点,或者直接是行权价
# 为了稳健,我们这里做一个简单的处理:
# 如果数字大于100,除以1000;否则直接用
val = float(strike_str)
if val > 100:
return val / 1000.0
return val
except:
pass
return None
策略要点说明
-
合约筛选 (
get_all_securities):- 我们使用
types=['option']获取所有期权。 - 通过名称过滤出
50ETF和购(Call)。 - 通过
end_date过滤掉即将到期的合约,避免刚开仓就面临交割。
- 我们使用
-
行权价解析 (
parse_strike_price):- 聚宽的基础数据 API 返回的对象中,行权价信息不直接暴露在
SecurityInfo的属性中,通常包含在display_name里。 - 代码中使用正则表达式提取名称末尾的数字作为行权价。针对 50ETF 期权,如果解析出的数字很大(如 3000),通常代表 3.000 元,代码中做了除以 1000 的处理。
- 聚宽的基础数据 API 返回的对象中,行权价信息不直接暴露在
-
构建组合:
- 买入腿 ($K_L$):选择平值期权(ATM),即行权价最接近当前标的价格的合约。
- 卖出腿 ($K_H$):选择虚值期权(OTM),即行权价高于 ATM 的合约(代码中通过
g.spread_width控制档位)。
-
风控与平仓:
- 在
check_and_close_positions函数中,每日检查距离到期日的天数。 - 为了避免进入行权交割流程(这通常需要大量现金或现券),策略设定在到期前 2 天强制平仓。
- 在
Q&A
Q: 为什么使用 order(code, -1) 来卖出期权?
A: 在聚宽中,amount 为负数代表卖出。对于期权,如果当前持仓为 0,卖出即为卖空(Short Open),即成为义务方,收取权利金。
Q: 如何调整价差的宽度?
A: 修改 initialize 函数中的 g.spread_width。设置为 1 表示卖出比买入高 1 个档位的行权价;设置为 2 则价差更宽,最大潜在收益更高,但胜率可能降低。
Q: 为什么需要解析行权价?
A: 聚宽的标准 get_security_info 返回的对象主要包含上市退市时间等基础信息,不直接包含 Greeks 或行权价字段。虽然可以通过扩展数据获取,但解析名称是最通用且不依赖额外数据权限的方法。