🚀 AI 一键生成 ptrade 策略代码

PTrade 投资组合管理:如何实现周期性自动再平衡策略?

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

问题描述

如何编写一个多资产投资组合,并定期按目标权重自动调仓的策略?

解决方案

这是一个非常经典的量化交易场景。在PTrade中实现多资产定期调仓策略(Rebalancing Strategy),核心逻辑分为三步:

  1. 定义目标:在 initialize 中设置资产列表和目标权重。
  2. 判断时间:在 handle_databefore_trading_start 中判断是否到达调仓日(例如每月第一个交易日)。
  3. 执行调仓:计算当前总资产,根据目标权重计算每只股票的目标市值,先卖后买以确保资金充足。

以下是完整的策略代码及详细说明。

策略逻辑说明

  1. 资产配置:我们选取了三只具有代表性的股票(示例:贵州茅台、招商银行、中国平安)作为投资组合。
  2. 调仓频率:策略设定为按月调仓。逻辑是通过比较“当前交易日”和“上一交易日”的月份是否发生变化来判断是否为月初。
  3. 资金分配:使用 context.portfolio.portfolio_value 获取当前账户总资产(现金+持仓市值),乘以权重得到每只股票的目标持仓市值。
  4. 交易执行:使用 order_target_value 函数。为了防止资金不足,代码中包含了一个排序逻辑,优先执行卖出操作(降低仓位),释放资金后再执行买入操作。

PTrade 策略代码

def initialize(context):
    """
    初始化函数,设置股票池、权重和运行参数
    """
    # 设定要操作的股票及其目标权重 (权重之和最好为1.0)
    # 示例:茅台40%,招行30%,平安30%
    g.target_portfolio = {
        '600519.SS': 0.4,
        '600036.SS': 0.3,
        '601318.SS': 0.3
    }
    
    # 提取股票列表用于设置股票池
    g.security_list = list(g.target_portfolio.keys())
    
    # 设置股票池
    set_universe(g.security_list)
    
    # 设置基准(可选,这里设为沪深300)
    set_benchmark('000300.SS')
    
    # 设定手续费(可选,这里设为万分之三)
    set_commission(commission_ratio=0.0003, min_commission=5.0, type="STOCK")
    
    log.info("策略初始化完成,投资组合目标: %s" % g.target_portfolio)

def handle_data(context, data):
    """
    盘中运行函数,每日/每分钟调用
    """
    # 获取当前日期和上一交易日日期
    current_date = context.blotter.current_dt.date()
    previous_date = context.previous_date
    
    # 判断是否为月初(调仓日)
    # 逻辑:如果当前月份与上一交易日月份不同,说明今天是本月第一个交易日
    if current_date.month != previous_date.month:
        log.info("检测到月份变化,开始执行定期调仓,日期: %s" % current_date)
        rebalance_portfolio(context, data)

def rebalance_portfolio(context, data):
    """
    执行调仓逻辑的核心函数
    """
    # 获取当前账户的总资产价值(包含现金和持仓市值)
    total_value = context.portfolio.portfolio_value
    
    # 临时列表,用于区分买入和卖出指令
    sell_orders = []
    buy_orders = []
    
    # 遍历目标组合中的每一只股票
    for stock, weight in g.target_portfolio.items():
        # 获取该股票的当前价格
        # 注意:如果股票停牌或无数据,data[stock]可能无法获取价格,需做保护
        if stock not in data or math.isnan(data[stock].price):
            log.warning("股票 %s 无行情数据或停牌,跳过本次调仓" % stock)
            continue
            
        current_price = data[stock].price
        
        # 计算该股票的目标持仓市值
        target_value = total_value * weight
        
        # 获取当前该股票的持仓市值
        current_position = get_position(stock)
        current_holding_value = current_position.amount * current_price
        
        # 判断是买入还是卖出
        # 如果目标市值 < 当前市值,需要卖出
        if target_value < current_holding_value:
            sell_orders.append((stock, target_value))
        # 如果目标市值 > 当前市值,需要买入
        else:
            buy_orders.append((stock, target_value))
    
    # ---------------------------------------------------
    # 关键步骤:先执行卖出,释放资金,再执行买入
    # ---------------------------------------------------
    
    # 1. 执行卖出
    for stock, target_val in sell_orders:
        order_target_value(stock, target_val)
        log.info("卖出调整: %s, 目标市值: %.2f" % (stock, target_val))
    
    # 2. 执行买入
    for stock, target_val in buy_orders:
        order_target_value(stock, target_val)
        log.info("买入调整: %s, 目标市值: %.2f" % (stock, target_val))

# 引入 math 库用于判断 NaN
import math

代码关键点解析

  1. order_target_value(security, value):

    • 这是PTrade中非常强大的接口。你不需要计算要买卖多少股,只需要告诉它“我希望这只股票最终持有多少钱的市值”。
    • 如果当前持有0元,目标是10000元,它会自动买入10000元。
    • 如果当前持有20000元,目标是10000元,它会自动卖出10000元。
  2. 先卖后买逻辑:

    • rebalance_portfolio 函数中,我们将订单分为了 sell_ordersbuy_orders
    • 原因:如果账户接近满仓,直接发出买入指令可能会因为“可用资金不足”而废单。先执行卖出操作可以将仓位过重的资产变现,增加 context.portfolio.cash,从而保证后续的买入操作顺利成交。
  3. 调仓时间判断:

    • current_date.month != previous_date.month 是判断月初最简单有效的方法。这意味着昨天还在上个月,今天已经进入新月份了,因此今天是本月第一个交易日。
  4. Python 版本兼容性:

    • 代码使用了 %s% 进行字符串格式化,完全兼容 Python 3.5 及以上版本(PTrade环境通常支持)。

常见问题 (Q&A)

Q: 如果某只股票停牌了,策略会怎么处理?
A: 代码中增加了 if stock not in data or math.isnan(data[stock].price): 的判断。如果股票停牌,通常无法获取最新价格,策略会跳过对该股票的操作,并在日志中打印警告,防止程序报错中断。

Q: 我可以将调仓频率改为每周吗?
A: 可以。在 handle_data 中,你可以使用 context.blotter.current_dt.isoweekday() 来判断。例如,判断 current_date.isoweekday() == 1 (周一) 且与上一次调仓日期不同,即可实现周度调仓。

Q: 为什么不直接在 initialize 里用 run_daily 定时?
A: 使用 run_daily 也是可以的,例如 run_daily(context, rebalance_portfolio, time='09:35')。但在函数内部依然需要判断日期是否符合“月初”的条件,因为 run_daily 是每天都会触发的。上面的写法直接在 handle_data 中判断日期变化,逻辑上更为直观。

Q: 权重之和必须严格等于1吗?
A: 建议等于1(即100%仓位)。如果小于1(例如0.8),则策略会保留20%的现金不进行投资。如果大于1,会导致资金不足无法完成所有买入操作。