🚀 AI 一键生成 ptrade 策略代码

如何在 PTrade 中构建一个市场中性的 Alpha 对冲策略?

ptrade | 发布时间: 2025/12/19
以下内容由 EasyQuant 生成。

问题描述

请帮我构建一个用股指期货对冲的市场中性策略

解决方案

这是一个基于PTrade平台的股指期货对冲市场中性策略(Market Neutral Strategy)。

策略逻辑说明

  1. 多头端(Alpha)

    • 选股范围:沪深300成分股(000300.SS)。
    • 选股因子:低市盈率(PE)策略。选取PE(TTM)最小的前20只股票。
    • 权重:等权重买入。
    • 目的:获取超越指数的超额收益(Alpha)。
  2. 空头端(Beta对冲)

    • 标的:沪深300股指期货(IF)。
    • 合约选择:自动计算当月主力合约(简单的逻辑:每月第三个周五前使用当月合约,之后切换至下月合约)。
    • 对冲比例:1:1 完全对冲。即:空头期货合约价值 $\approx$ 多头股票持仓市值。
    • 目的:剥离市场风险(Beta),无论大盘涨跌,只要选出的股票表现优于大盘即可获利。
  3. 资金管理

    • 预留20%资金作为期货保证金及应对波动,80%资金用于股票投资。
  4. 调仓频率

    • 每日进行监测,根据持仓市值变化调整期货空单数量。
    • 每月定期(或每日)调整股票持仓。

策略代码

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)

关键点解析

  1. 合约代码处理 (get_current_future_contract)

    • PTrade中的中金所期货代码后缀为 .CCFX(例如 IF2309.CCFX)。
    • 代码中实现了一个简单的逻辑来判断何时移仓换月(每月第三个周五)。这是实盘和回测中非常重要的一环,否则会导致持有到期被强制交割或流动性枯竭。
  2. 对冲计算

    • total_stock_value:遍历持仓,累加股票市值。
    • target_short_count:$\text{股票总市值} / (\text{期货价格} \times 300)$。这里采用四舍五入取整,尽可能使净敞口接近0。
  3. 交易指令

    • 股票使用 order_target_value,方便直接调整到目标金额。
    • 期货开空单使用 sell_open(卖出开仓)。
    • 期货平空单使用 buy_close(买入平仓)。
  4. 兼容性

    • 代码遵循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 获取分钟线收盘价作为替代。