问题描述
解决方案
这是一个非常经典的**期现套利(Arbitrage)**策略需求。
策略原理
期现套利主要利用股指期货(如 IF 沪深300期货)与现货指数(如沪深300指数或对应的 ETF)之间的价差(基差)进行获利。
- 正向套利(开仓):当期货价格 远高于 现货价格(升水),且价差超过了资金成本和交易成本时:
- 买入 现货(如沪深300 ETF)。
- 做空 等市值的股指期货。
- 平仓:当期货价格与现货价格 收敛(价差缩小或接近0)时:
- 卖出 现货。
- 平仓 期货空单。
- 获利来源:赚取基差回归的收益。
代码实现要点
在聚宽(JoinQuant)中实现该策略,需要注意以下几点:
- 多账户设置:必须使用
set_subportfolios同时开启股票账户(买ETF)和期货账户(做空期货)。 - 合约匹配:需要计算一手期货合约对应的现货价值(合约乘数通常为300)。
- 主力合约:通常使用
get_dominant_future获取主力合约,或者指定当月合约。
下面是一个完整的、可直接运行的期现套利策略代码。
# -*- coding: utf-8 -*-
from jqdata import *
def initialize(context):
"""
初始化函数
"""
# 1. 设定基准
set_benchmark('000300.XSHG')
# 2. 开启真实价格模式(动态复权)
set_option('use_real_price', True)
# 3. 设置日志级别
log.set_level('order', 'error')
# 4. 设定多账户:仓位0为股票(ETF),仓位1为期货
# 假设初始资金 100万,平分给两个账户(实际操作中需根据保证金比例调整)
init_cash = context.portfolio.starting_cash
set_subportfolios([
SubPortfolioConfig(cash=init_cash * 0.8, type='stock'), # 股票账户买ETF,资金占用大
SubPortfolioConfig(cash=init_cash * 0.2, type='futures') # 期货账户做空,保证金交易
])
# 5. 设置手续费(这里设置一个简化的费率,实盘请按券商设置)
# 股票费率
set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0003,
close_commission=0.0003, min_commission=5), type='stock')
# 期货费率
set_order_cost(OrderCost(open_tax=0, close_tax=0, open_commission=0.000023,
close_commission=0.000023, close_today_commission=0.0023, min_commission=0), type='index_futures')
# 6. 定义全局变量
g.etf_code = '510300.XSHG' # 现货标的:沪深300ETF
g.index_code = '000300.XSHG' # 参考指数:沪深300指数
g.future_underlying = 'IF' # 期货品种:IF
# 7. 套利阈值设置
# 当 (期货 - 现货) / 现货 > open_threshold 时,开仓(正向套利)
g.open_threshold = 0.02 # 2% 的升水才开仓(示例值,实际需计算资金成本)
# 当 (期货 - 现货) / 现货 < close_threshold 时,平仓
g.close_threshold = 0.005 # 0.5% 时平仓
# 运行频率:按分钟回测,捕捉日内价差
run_daily(arbitrage_logic, time='every_bar')
def arbitrage_logic(context):
"""
核心套利逻辑,每分钟运行一次
"""
# 获取当前时间
current_date = context.current_dt.date()
# 1. 获取主力合约代码
future_code = get_dominant_future(g.future_underlying)
if not future_code:
return
# 2. 获取价格数据
# 获取指数现货价格(作为基准计算价差)
spot_data = attribute_history(g.index_code, 1, '1m', ['close'])
if len(spot_data) == 0: return
spot_price = spot_data['close'][-1]
# 获取期货价格
future_data = attribute_history(future_code, 1, '1m', ['close'])
if len(future_data) == 0: return
future_price = future_data['close'][-1]
# 获取ETF价格(实际交易用)
etf_data = attribute_history(g.etf_code, 1, '1m', ['close'])
if len(etf_data) == 0: return
etf_price = etf_data['close'][-1]
# 3. 计算基差率 (Spread Rate)
# 基差率 = (期货价格 - 现货指数价格) / 现货指数价格
spread_rate = (future_price - spot_price) / spot_price
# 4. 获取当前持仓状态
# pindex=0 是股票账户,pindex=1 是期货账户
# 检查期货账户是否有空单
short_positions = context.subportfolios[1].short_positions
has_short_pos = future_code in short_positions and short_positions[future_code].total_amount > 0
# 5. 交易逻辑
# --- 开仓逻辑 (正向套利) ---
# 如果升水超过阈值,且当前没有持仓
if spread_rate > g.open_threshold and not has_short_pos:
log.info("触发正向套利开仓: 期货价格 %.2f, 指数价格 %.2f, 基差率 %.2f%%" % (future_price, spot_price, spread_rate * 100))
# 计算可买手数
# 沪深300期货合约乘数为 300
contract_multiplier = 300
# 计算一手期货对应的市值
one_future_value = future_price * contract_multiplier
# 获取期货账户可用资金
future_cash = context.subportfolios[1].available_cash
# 获取股票账户可用资金
stock_cash = context.subportfolios[0].available_cash
# 简单资金管理:以期货账户能开的空单数量为基准,或者以股票账户能买的ETF为基准,取小值
# 假设期货保证金 15%
margin_rate = 0.15
max_future_hands = int(future_cash / (one_future_value * margin_rate))
max_etf_hands_equivalent = int(stock_cash / one_future_value)
# 决定开仓手数
hands_to_open = min(max_future_hands, max_etf_hands_equivalent)
if hands_to_open > 0:
# 1. 股票账户买入对应价值的 ETF
# 需要买入的 ETF 市值 ≈ 期货市值
target_etf_value = hands_to_open * one_future_value
order_target_value(g.etf_code, target_etf_value, pindex=0)
# 2. 期货账户卖出开空
order(future_code, hands_to_open, side='short', pindex=1)
log.info("开仓完成: 买入ETF市值 %.2f, 做空期货 %d 手" % (target_etf_value, hands_to_open))
# --- 平仓逻辑 ---
# 如果价差回归(小于平仓阈值),且持有空单
elif spread_rate < g.close_threshold and has_short_pos:
log.info("触发套利平仓: 期货价格 %.2f, 指数价格 %.2f, 基差率 %.2f%%" % (future_price, spot_price, spread_rate * 100))
# 1. 卖出所有 ETF (pindex=0)
order_target(g.etf_code, 0, pindex=0)
# 2. 平掉所有期货空单 (pindex=1)
# 注意:如果主力合约发生切换,这里需要遍历持仓平掉旧合约,为简化代码,这里只平当前主力
# 严谨做法是遍历 short_positions.keys() 进行平仓
for code in short_positions.keys():
order_target(code, 0, side='short', pindex=1)
log.info("平仓完成")
# --- 移仓换月逻辑 (可选,简化版) ---
# 实际交易中,如果持有的是非主力合约,且主力合约已切换,需要进行移仓
# 本策略简单处理:如果持有的合约不是当前主力,则强制平仓(下次开仓会自动开在主力上)
if has_short_pos:
for code in list(short_positions.keys()):
if code != future_code:
log.info("主力合约切换,平掉旧合约: %s" % code)
order_target(code, 0, side='short', pindex=1)
# 对应的ETF也应该按比例减仓,或者等待下次信号重新平衡
# 为简化,这里选择全部平仓重来
order_target(g.etf_code, 0, pindex=0)
策略代码详解
-
set_subportfolios(关键):- 这是实现期现套利的核心。我们创建了两个子账户:
type='stock'用于买卖 ETF,type='futures'用于交易股指期货。 - 资金分配比例设为 8:2,因为股票是全额交易,期货是保证金交易(通常10%-15%),所以大部分资金需要放在股票账户。
- 这是实现期现套利的核心。我们创建了两个子账户:
-
get_dominant_future:- 自动获取当前的 IF(沪深300)主力合约。这避免了手动指定合约代码过期的麻烦。
-
基差计算:
spread_rate = (future_price - spot_price) / spot_price- 这里使用
000300.XSHG(指数本身) 来计算基差,因为 ETF 可能会有微小的跟踪误差,但套利的理论依据是指数。
-
对冲比例:
one_future_value = future_price * 300。- 一手 IF 期货代表 300 倍指数点位的价值。买入 ETF 时,必须买入等值的金额,才能实现完全对冲(Delta Neutral)。
-
交易指令:
- ETF 交易使用
pindex=0。 - 期货交易使用
pindex=1,且开空单需指定side='short'。
- ETF 交易使用
常见问题 Q&A
Q: 为什么不使用反向套利(买期货,融券卖现货)?
A: 在 A 股市场,融券卖出 ETF 或一篮子股票的成本较高且券源不稳定。对于普通量化策略,正向套利(买现货抛期货)是最可行的方案。
Q: 这里的阈值 0.02 (2%) 是怎么定的?
A: 这是一个示例值。实际实盘中,你需要计算资金成本(无风险利率)、交易手续费、冲击成本等。只有当价差收益覆盖了这些成本后,套利才有意义。
Q: 如何处理期货交割日?
A: 代码中包含了一个简单的移仓逻辑:如果持仓的合约不再是主力合约,则平仓。在实际策略中,通常会在交割日前几天强制平仓或移仓到下月合约,以避免进入交割流程。
Q: 为什么回测时没有交易?
A: 沪深300期货在成熟市场中,升水(期货>现货)幅度通常不大,甚至长期贴水(期货<现货)。如果回测时间段内没有出现超过 2% 的升水,策略就不会开仓。你可以尝试调低 g.open_threshold 进行测试。