问题描述
解决方案
这是一个基于PTrade平台的股指期货对冲市场中性策略(Market Neutral Strategy)。
策略逻辑说明
-
多头端(Alpha):
- 选股范围:沪深300成分股(000300.SS)。
- 选股因子:低市盈率(PE)策略。选取PE(TTM)最小的前20只股票。
- 权重:等权重买入。
- 目的:获取超越指数的超额收益(Alpha)。
-
空头端(Beta对冲):
- 标的:沪深300股指期货(IF)。
- 合约选择:自动计算当月主力合约(简单的逻辑:每月第三个周五前使用当月合约,之后切换至下月合约)。
- 对冲比例:1:1 完全对冲。即:空头期货合约价值 $\approx$ 多头股票持仓市值。
- 目的:剥离市场风险(Beta),无论大盘涨跌,只要选出的股票表现优于大盘即可获利。
-
资金管理:
- 预留20%资金作为期货保证金及应对波动,80%资金用于股票投资。
-
调仓频率:
- 每日进行监测,根据持仓市值变化调整期货空单数量。
- 每月定期(或每日)调整股票持仓。
策略代码
import datetime
import numpy as np
def initialize(context):
"""
初始化函数
"""
# 1. 设置股票池:沪深300
g.index_code = '000300.SS'
# 2. 设置期货品种:IF (沪深300股指期货)
g.future_product = 'IF'
# 3. 设定持仓股票数量
g.stock_num = 20
# 4. 设定合约乘数 (IF为300)
g.contract_multiplier = 300
# 5. 记录当前持有的期货合约代码
g.current_future_code = None
# 设置基准
set_benchmark(g.index_code)
# 设置手续费 (股票万3,期货万0.23)
set_commission(commission_ratio=0.0003, min_commission=5.0, type='STOCK')
# 注意:PTrade回测中期货手续费设置需使用 set_future_commission
set_future_commission("IF", 0.000023)
# 开启每日定时运行 (每天14:50进行调仓和对冲检查)
run_daily(context, trade_func, time='14:50')
def get_current_future_contract(context):
"""
获取当月主力合约代码逻辑
规则:每月第三个周五是交割日。
若当前日期在交割日之前,使用当月合约;否则使用下月合约。
"""
current_dt = context.blotter.current_dt
year = current_dt.year
month = current_dt.month
day = current_dt.day
# 计算当月第三个周五的日期
first_day_of_month = datetime.date(year, month, 1)
# weekday() 返回 0-6 (周一-周日),周五是4
first_friday_offset = (4 - first_day_of_month.weekday() + 7) % 7
third_friday_day = 1 + first_friday_offset + 14
# 简单的换月逻辑:如果今天已经过了第三个周五(或者就是当天,为了安全起见建议当天也换),则切换到下个月
if day >= third_friday_day:
if month == 12:
year += 1
month = 1
else:
month += 1
# 拼接合约代码,例如 IF2309.CCFX
# 注意:年份取后两位
year_str = str(year)[2:]
month_str = "%02d" % month
contract_code = "%s%s%s.CCFX" % (g.future_product, year_str, month_str)
return contract_code
def select_stocks(context):
"""
选股逻辑:获取沪深300中PE最小的前N只股票
"""
# 获取沪深300成分股
universe = get_index_stocks(g.index_code)
# 获取基本面数据:PE (pe_ttm)
# 注意:get_fundamentals 查询大量股票时建议分批或确保性能,此处演示直接获取
df = get_fundamentals(universe, 'valuation', ['pe_ttm', 'secu_code'],
date=context.blotter.current_dt.strftime("%Y%m%d"))
if df is None or len(df) == 0:
return []
# 过滤掉PE为负(亏损)或异常值的股票,并按PE升序排列
df = df[df['pe_ttm'] > 0]
df = df.sort_values(by='pe_ttm', ascending=True)
# 取前N只
target_stocks = df['secu_code'].head(g.stock_num).tolist()
return target_stocks
def trade_func(context):
"""
主交易函数:包含股票调仓和期货对冲
"""
# ---------------------------------------------------
# 第一步:股票端调仓 (Alpha获取)
# ---------------------------------------------------
target_stocks = select_stocks(context)
if not target_stocks:
log.warning("今日未选出股票,跳过调仓")
return
# 获取当前持仓的股票
holding_positions = context.portfolio.positions
holding_stocks = [s for s in holding_positions if get_stock_blocks(s) is not None] # 简单判断是否为股票
# 卖出不在目标池中的股票
for stock in holding_stocks:
if stock not in target_stocks:
order_target_value(stock, 0)
# 买入目标池中的股票
# 资金分配:预留20%现金作为期货保证金,80%用于股票
total_asset = context.portfolio.portfolio_value
stock_allocation = total_asset * 0.8
target_value_per_stock = stock_allocation / len(target_stocks)
for stock in target_stocks:
order_target_value(stock, target_value_per_stock)
# ---------------------------------------------------
# 第二步:期货端对冲 (Beta剥离)
# ---------------------------------------------------
# 1. 计算当前股票持仓总市值
total_stock_value = 0.0
# 重新获取持仓信息以获得最新市值
for stock in context.portfolio.positions:
pos = context.portfolio.positions[stock]
# 过滤掉期货持仓,只计算股票市值
if pos.business_type == 'stock':
total_stock_value += pos.last_sale_price * pos.amount
log.info("当前股票持仓总市值: %.2f" % total_stock_value)
# 2. 确定当月主力合约
target_future = get_current_future_contract(context)
# 3. 移仓换月逻辑
# 如果当前持有的合约不是目标主力合约,且持有旧合约空单,则平旧开新
if g.current_future_code and g.current_future_code != target_future:
old_pos = get_position(g.current_future_code)
# 如果有空单 (short_amount > 0)
if old_pos.short_amount > 0:
log.info("主力合约切换,平仓旧合约: %s" % g.current_future_code)
# 买入平仓 (平空)
buy_close(g.current_future_code, old_pos.short_amount)
g.current_future_code = target_future
if g.current_future_code is None:
g.current_future_code = target_future
# 4. 计算需要对冲的合约数量
# 获取期货最新价格
future_snapshot = get_snapshot(g.current_future_code)
if not future_snapshot:
# 如果取不到快照(如回测刚开始),尝试用get_price或跳过
# 回测中get_snapshot可能不可用,改用get_price
hist = get_price(g.current_future_code, count=1, frequency='1m', fields=['close'])
if hist is not None and len(hist) > 0:
future_price = hist['close'][-1]
else:
log.warning("无法获取期货价格,跳过对冲")
return
else:
future_price = future_snapshot[g.current_future_code]['last_px']
if future_price <= 0:
return
# 合约价值 = 价格 * 合约乘数
contract_value = future_price * g.contract_multiplier
# 理论所需空单数量 (四舍五入取整)
if contract_value > 0:
target_short_count = int(round(total_stock_value / contract_value))
else:
target_short_count = 0
# 5. 调整期货仓位
current_pos = get_position(g.current_future_code)
current_short_amount = current_pos.short_amount
diff = target_short_count - current_short_amount
if diff > 0:
# 需要增加空单:卖出开仓
log.info("增加对冲空单: %s, 数量: %d" % (g.current_future_code, diff))
sell_open(g.current_future_code, diff)
elif diff < 0:
# 需要减少空单:买入平仓
reduce_amount = abs(diff)
log.info("减少对冲空单: %s, 数量: %d" % (g.current_future_code, reduce_amount))
buy_close(g.current_future_code, reduce_amount)
else:
log.info("对冲比例合适,无需调整期货仓位。当前空单数: %d" % current_short_amount)
关键点解析
-
合约代码处理 (
get_current_future_contract):- PTrade中的中金所期货代码后缀为
.CCFX(例如IF2309.CCFX)。 - 代码中实现了一个简单的逻辑来判断何时移仓换月(每月第三个周五)。这是实盘和回测中非常重要的一环,否则会导致持有到期被强制交割或流动性枯竭。
- PTrade中的中金所期货代码后缀为
-
对冲计算:
total_stock_value:遍历持仓,累加股票市值。target_short_count:$\text{股票总市值} / (\text{期货价格} \times 300)$。这里采用四舍五入取整,尽可能使净敞口接近0。
-
交易指令:
- 股票使用
order_target_value,方便直接调整到目标金额。 - 期货开空单使用
sell_open(卖出开仓)。 - 期货平空单使用
buy_close(买入平仓)。
- 股票使用
-
兼容性:
- 代码遵循Python 3.5语法(不使用f-string),符合PTrade环境要求。
- 使用了
get_stock_blocks来辅助判断持仓是否为股票(过滤掉期货持仓本身),防止计算市值时重复计算。
常见问题 Q&A
Q: 为什么资金只分配80%给股票?
A: 期货交易采用保证金制度,且每日结算。如果股票满仓,一旦期货端发生亏损(即大盘上涨,股票端赚钱但期货端亏钱),账户需要现金来补足期货保证金。如果现金不足,会导致强平风险。预留20%现金是安全垫。
Q: 如何处理基差(Basis)风险?
A: 股指期货价格通常与现货指数价格不同(贴水或升水)。如果期货深度贴水(期货价格远低于现货),做空期货会有额外的成本(基差收敛带来的亏损)。本策略为基础模板,未包含基差择时逻辑。进阶策略可以在贴水过深时减少对冲比例。
Q: 回测时报错 get_snapshot 怎么办?
A: PTrade的回测模式有时不支持 get_snapshot。代码中已添加了容错逻辑:如果 get_snapshot 返回空,则尝试使用 get_price 获取分钟线收盘价作为替代。