问题描述
解决方案
这是一个基于 PTrade 平台的市场中性 Alpha 对冲策略的设计与实现。
策略核心逻辑
- Alpha 端(多头股票组合):
- 选股范围:沪深300成分股。
- 选股因子:低市盈率(PE)。我们假设低估值的股票在长期能跑赢指数。
- 权重分配:等权重买入排名靠前的股票(例如前 30 只)。
- Beta 端(空头期货对冲):
- 对冲标的:沪深300股指期货(IF)。
- 对冲比例:等市值对冲(Dollar Neutral)。即
空头期货合约价值 ≈ 多头股票持仓市值。 - 目标:剥离市场波动(Beta),仅获取选股带来的超额收益(Alpha)。
- 资金管理:
- 预留约 20%-30% 的资金作为期货保证金和应对价格波动的缓冲,其余资金用于买入股票。
策略代码实现
import numpy as np
import math
import datetime
def initialize(context):
"""
初始化函数,设置策略参数
"""
# 设定基准为沪深300
set_benchmark('000300.SS')
# 开启真实市价成交模式(回测中模拟滑点)
set_slippage(slippage=0.002)
set_commission(commission_ratio=0.0003, min_commission=5.0, type='STOCK')
# 策略参数设置
g.stock_index = '000300.SS' # 股票基准指数
g.future_code_prefix = 'IF' # 期货品种代码前缀 (IF: 沪深300, IC: 中证500)
g.future_exchange = '.CCFX' # 期货交易所后缀
g.stock_count = 30 # 持仓股票数量
g.margin_reserve_ratio = 0.2 # 预留现金比例(用于期货保证金)
# 全局变量
g.target_stocks = [] # 目标股票列表
g.current_future = None # 当前持有的期货合约
# 设置定时任务:每周第一个交易日进行调仓
# 注意:这里为了演示方便设为每周,实际Alpha策略可能为月度调仓
run_daily(context, rebalance_strategy, time='10:00')
def before_trading_start(context, data):
"""
盘前处理:更新股票池,确定当月期货合约
"""
# 1. 获取当月主力合约代码 (简化的合约推算逻辑)
# 实际交易中建议使用主力合约切换算法,这里简化为交易当月合约
current_dt = context.blotter.current_dt
year_str = str(current_dt.year)[2:] # 取年份后两位
month_str = "{:02d}".format(current_dt.month)
# 简单的合约代码构建:IF + YYMM + .CCFX
# 注意:临近交割日(每月第三个周五)需要处理移仓换月,本示例简化处理
g.current_future = g.future_code_prefix + year_str + month_str + g.future_exchange
def rebalance_strategy(context):
"""
调仓主逻辑:选股 -> 买卖股票 -> 计算对冲市值 -> 调整期货空单
"""
log.info("开始执行调仓...")
# --- 第一步:Alpha选股 (低PE策略) ---
# 获取沪深300成分股
index_stocks = get_index_stocks(g.stock_index)
# 过滤停牌和ST股 (简单过滤)
# 注意:get_stock_status 在回测中可能需要注意未来函数,这里使用简单逻辑
trade_stocks = []
for stock in index_stocks:
# 过滤掉停牌的股票(这里假设无法获取行情的即为停牌或未上市)
# 实际生产中建议使用 get_stock_status 接口
trade_stocks.append(stock)
# 获取基本面数据:PE (市盈率)
# q = query(valuation.code, valuation.pe_ratio).filter(valuation.code.in_(trade_stocks)).order_by(valuation.pe_ratio.asc()).limit(g.stock_count)
# PTrade API 使用 get_fundamentals
df = get_fundamentals(trade_stocks, 'valuation', ['pe_ttm'], date=None, count=1)
if df is None or len(df) == 0:
log.warning("未获取到基本面数据,跳过本次调仓")
return
# 排序并取前 N 只股票 (低PE)
# 注意:PTrade返回的df索引通常是股票代码,或者需要根据列排序
# 假设返回的是DataFrame,列为 pe_ttm
# 过滤掉PE为负或异常值的
df = df[df['pe_ttm'] > 0]
df = df.sort_values(by='pe_ttm', ascending=True)
g.target_stocks = df.index[:g.stock_count].tolist()
log.info("目标持仓股票数量: %s" % len(g.target_stocks))
# --- 第二步:执行股票交易 ---
# 1. 卖出不在目标列表中的股票
positions = get_positions()
for security in positions:
# 仅处理股票持仓
if positions[security].business_type == 'stock' and positions[security].amount > 0:
if security not in g.target_stocks:
order_target_value(security, 0)
# 2. 买入目标股票
# 计算股票账户可用资金 (总资产 * (1 - 预留比例))
total_value = context.portfolio.portfolio_value
target_stock_value = total_value * (1 - g.margin_reserve_ratio)
if len(g.target_stocks) > 0:
value_per_stock = target_stock_value / len(g.target_stocks)
for security in g.target_stocks:
order_target_value(security, value_per_stock)
# --- 第三步:执行期货对冲 (Beta Hedging) ---
adjust_futures_position(context)
def adjust_futures_position(context):
"""
调整期货空单仓位以匹配股票市值
"""
# 1. 计算当前股票持仓总市值
stock_market_value = 0.0
positions = get_positions()
for security in positions:
pos = positions[security]
if pos.business_type == 'stock':
stock_market_value += pos.last_sale_price * pos.amount
log.info("当前股票持仓总市值: %.2f" % stock_market_value)
if stock_market_value <= 0:
return
# 2. 获取期货合约信息
future_code = g.current_future
# 获取期货最新价格
# 注意:get_price 返回的是 DataFrame 或 Panel
price_data = get_price(future_code, count=1, frequency='1m', fields=['close'])
if price_data is None or len(price_data) == 0:
log.warning("无法获取期货价格: %s" % future_code)
return
# 兼容不同版本的返回值,获取最新收盘价
if isinstance(price_data, dict):
# 如果是字典
current_future_price = price_data['close'][-1]
else:
# 如果是DataFrame
current_future_price = price_data['close'].iloc[-1]
# 获取合约乘数 (沪深300 IF 合约乘数为 300)
# 可以通过 get_instruments 获取,这里为了简化直接赋值,IF/IC/IH 乘数不同需注意
contract_multiplier = 300.0
if 'IC' in future_code:
contract_multiplier = 200.0
elif 'IM' in future_code:
contract_multiplier = 200.0
# 3. 计算需持有的空单合约数量
# 公式:空单价值 = 股票市值
# 合约数量 = 股票市值 / (期货价格 * 合约乘数)
# 向上取整或四舍五入,这里采用四舍五入
target_contracts = int(round(stock_market_value / (current_future_price * contract_multiplier)))
log.info("目标对冲合约: %s, 目标空单数量: %d" % (future_code, target_contracts))
# 4. 执行期货交易
# 获取当前期货持仓
current_short_pos = 0
pos = get_position(future_code)
if pos:
# PTrade中期货持仓方向:sell_amount 或 short_amount
current_short_pos = pos.short_amount
diff = target_contracts - current_short_pos
if diff > 0:
# 需要增加空单 (卖出开仓)
log.info("增加空单: %d 手" % diff)
sell_open(future_code, amount=diff)
elif diff < 0:
# 需要减少空单 (买入平仓)
# 注意:平仓时优先平今还是平昨取决于交易所规则,这里简化为直接平仓
close_amount = abs(diff)
log.info("减少空单: %d 手" % close_amount)
buy_close(future_code, amount=close_amount)
else:
log.info("期货仓位无需调整")
def handle_data(context, data):
"""
盘中运行函数
"""
# 可以在这里添加止损或风控逻辑
pass
代码关键点解析
-
选股逻辑 (
rebalance_strategy):- 使用
get_fundamentals获取市盈率(pe_ttm)。 - 选取 PE 最低的 30 只股票构建组合。这是一种经典的价值因子 Alpha。
- 使用
order_target_value确保每只股票等权重配置。
- 使用
-
对冲逻辑 (
adjust_futures_position):- 计算股票总市值:遍历持仓,累加所有类型为
'stock'的持仓市值。 - 计算期货合约数:
合约数量 = 股票总市值 / (期货价格 * 合约乘数)。 - 完全对冲:这里采用的是 1:1 的市值对冲(Beta 默认为 1)。如果你的股票组合 Beta 偏离 1 较大,可以在计算
target_contracts时乘以组合的 Beta 值。
- 计算股票总市值:遍历持仓,累加所有类型为
-
期货交易指令:
sell_open(contract, amount): 卖出开仓(建立空单)。buy_close(contract, amount): 买入平仓(减少空单)。- 注意:PTrade 的期货接口与股票不同,需要明确开平仓方向。
-
资金分配:
g.margin_reserve_ratio = 0.2:预留 20% 资金。这是因为期货交易需要保证金(通常 10%-15%),且股票满仓波动可能导致可用资金不足。预留现金可以防止因期货亏损(虽然股票端会盈利)导致的保证金不足强平风险。
进阶优化建议
- 移仓换月:上述代码简单地使用了当月合约。在实盘中,必须在交割日(每月第三个周五)之前,将持仓从当月合约平仓,并开仓下月合约。
- Beta 动态调整:低估值股票的 Beta 值通常小于 1。如果按 1:1 对冲,可能会导致“过度对冲”,即市场上涨时,期货亏损大于股票盈利。建议计算组合过去 N 天的 Beta 值来调整对冲比例。
- 风格因子控制:单纯低 PE 可能会导致行业集中(如全买了银行股)。可以使用多因子模型或行业中性化处理来分散风险。
Q&A
Q: 为什么使用 get_fundamentals 而不是 get_history 来选股?
A: get_fundamentals 专门用于获取财务数据(如 PE、PB、ROE 等),而 get_history 主要用于获取量价数据(开盘价、收盘价等)。Alpha 策略通常基于基本面因子,因此需要财务数据。
Q: 代码中的 contract_multiplier = 300.0 是什么意思?
A: 这是沪深300股指期货(IF)的合约乘数。一张 IF 合约的价值 = 指数点位 * 300元。如果是中证500(IC)或中证1000(IM),乘数则是 200元。
Q: 如何处理期货的保证金不足问题?
A: 策略中设置了 g.margin_reserve_ratio。如果市场大幅上涨,期货空单会亏损(需要追加保证金),虽然股票多头会盈利,但股票盈利是未结盈亏,不能直接用于期货保证金。因此必须保留一部分现金(Cash)来应对期货端的浮动亏损。