🚀 AI 一键生成 ptrade 策略代码

PTrade 策略开发:如何构建自定义权重的指数投资组合?

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

问题描述

如何构建一个自定义权重的指数组合?

解决方案

构建一个自定义权重的指数组合(或投资组合)在 PTrade 中主要涉及以下几个核心步骤:

  1. 定义股票池与权重:确定你要持有的股票列表以及每只股票的目标权重(例如:固定权重、市值加权或等权重)。
  2. 计算目标持仓市值:根据当前账户的总资产和目标权重,计算每只股票应该持有的金额。
  3. 定期调仓(Rebalance):设置定时任务(如每月或每周),将实际持仓调整至目标权重。
  4. 交易执行:使用 order_target_value 函数来买入或卖出,使持仓达到预期金额。

下面是一个完整的 PTrade 策略示例。该策略演示了如何构建一个包含 3 只股票的自定义组合,并设定了不同的权重(例如:股票A 50%,股票B 30%,股票C 20%),且每月第一个交易日进行调仓。

策略代码实现

def initialize(context):
    """
    初始化函数,设置策略参数
    """
    # 1. 定义自定义权重的组合
    # 格式:{'股票代码': 目标权重}
    # 注意:权重之和最好为 1.0 (100%),如果不足 1.0 则会有现金剩余
    g.target_portfolio = {
        '600519.SS': 0.5,  # 贵州茅台 50%
        '000858.SZ': 0.3,  # 五粮液 30%
        '600036.SS': 0.2   # 招商银行 20%
    }
    
    # 设置股票池(用于回测数据预加载)
    set_universe(list(g.target_portfolio.keys()))
    
    # 2. 设置调仓周期
    # 这里设置为每天检查,但在函数内部判断是否为月初
    # time='10:00' 表示在交易日 10:00 执行调仓逻辑,避开开盘剧烈波动
    run_daily(context, rebalance_portfolio, time='10:00')

def before_trading_start(context, data):
    """
    盘前处理
    """
    # 可以在这里根据动态逻辑更新 g.target_portfolio
    # 如果是固定权重,则不需要操作
    pass

def rebalance_portfolio(context):
    """
    调仓核心逻辑
    """
    # --- 1. 判断调仓时机 ---
    # 获取当前日期
    current_date = context.blotter.current_dt.date()
    # 获取当月第一个交易日
    # get_trade_days 返回的是字符串列表或 numpy array,需要处理
    trade_days = get_trade_days(start_date=current_date.strftime("%Y%m01"), end_date=current_date.strftime("%Y%m%d"))
    
    # 如果当前日期不是当月第一个交易日,则跳过(实现月度调仓)
    # 注意:trade_days[0] 是当月第一天,如果当前日期等于它,说明是月初
    # 为了演示简单,这里假设只要运行了就是调仓日。
    # 实际生产中,建议使用 context.blotter.current_dt.month != context.previous_date.month 来判断换月
    
    is_month_start = False
    if context.previous_date.month != context.blotter.current_dt.month:
        is_month_start = True
        
    # 如果不是换月的第一天,且不是回测第一天,则不调仓
    if not is_month_start and context.blotter.current_dt != get_trade_days(count=1)[0]:
        return

    log.info("开始执行月度调仓...")

    # --- 2. 获取账户总资产 ---
    # portfolio_value 包含 现金 + 持仓市值
    total_value = context.portfolio.portfolio_value
    
    # --- 3. 卖出不在目标组合中的股票 ---
    current_positions = list(context.portfolio.positions.keys())
    for stock in current_positions:
        if stock not in g.target_portfolio:
            # 如果当前持仓的股票不在目标清单里,清仓
            order_target_value(stock, 0)
            log.info("卖出非目标股票: %s" % stock)

    # --- 4. 买入/调整目标股票 ---
    for stock, weight in g.target_portfolio.items():
        # 检查股票是否停牌或退市,避免下单失败
        if not is_stock_tradable(stock):
            log.warning("股票 %s 当前不可交易,跳过" % stock)
            continue
            
        # 计算该股票的目标持仓市值
        target_value = total_value * weight
        
        # 使用 order_target_value 直接调整持仓到目标金额
        # 该函数会自动计算需要买入或卖出的数量
        order_target_value(stock, target_value)
        log.info("调整股票 %s 持仓至目标市值: %.2f (权重: %.2f)" % (stock, target_value, weight))

def is_stock_tradable(stock):
    """
    辅助函数:判断股票是否可交易(非停牌、非退市)
    """
    # 获取股票状态
    # get_stock_status 返回字典 {code: bool}
    # ST: 是否ST, HALT: 是否停牌, DELISTING: 是否退市
    statuses = get_stock_status([stock], filter_type=["HALT", "DELISTING"])
    
    # 如果返回 True,说明处于停牌或退市状态,不可交易
    if statuses[stock]:
        return False
    return True

def handle_data(context, data):
    """
    盘中运行函数(本策略主要逻辑在 run_daily 中,此处留空)
    """
    pass

代码关键点解析

  1. g.target_portfolio (全局变量):

    • 我们使用字典来存储股票代码和对应的权重。这种结构非常灵活,你可以很容易地将其修改为动态生成的字典(例如在 before_trading_start 中根据市值或因子计算权重)。
  2. order_target_value(security, value):

    • 这是构建指数组合最关键的 API。
    • 它的作用是:无论当前持有多少股,系统会自动计算差额,买入或卖出,使得该股票的最终持仓市值等于 value
    • 例如:总资产 100万,茅台目标权重 50%。调用 order_target_value('600519.SS', 500000)。如果当前持有 40万,系统会自动买入 10万;如果当前持有 60万,系统会自动卖出 10万。
  3. 调仓频率控制:

    • 示例代码中使用了 if context.previous_date.month != context.blotter.current_dt.month: 这一逻辑来判断是否刚刚跨月。这是实现月度调仓的常用技巧。
    • 如果不加这个判断,策略会每天都进行调仓,这会导致极高的手续费和滑点损耗。
  4. 异常处理 (is_stock_tradable):

    • 在实际交易或回测中,股票可能会停牌。如果对停牌的股票下单,订单会失败或一直挂单。因此,使用 get_stock_status 检查股票状态是一个良好的编程习惯。

如何扩展为动态权重?

如果你不想使用固定的权重,而是想根据流通市值来加权(类似沪深300指数的编制方法),你可以修改 rebalance_portfolio 中的逻辑:

def get_market_cap_weights(stock_list):
    # 获取基本面数据:流通市值 (float_value)
    # 注意:get_fundamentals 返回的是 DataFrame
    q = get_fundamentals(stock_list, 'valuation', fields=['float_value'])
    
    if q is None or len(q) == 0:
        return {}
        
    # 计算总流通市值
    total_cap = q['float_value'].sum()
    
    weights = {}
    # 遍历 DataFrame 计算权重
    # 注意:PTrade 返回的 DataFrame 索引通常是股票代码
    for stock in stock_list:
        if stock in q.index:
            # 单只股票市值 / 总市值
            w = q.loc[stock, 'float_value'] / total_cap
            weights[stock] = w
        else:
            weights[stock] = 0.0
            
    return weights

你可以在调仓前调用这个函数来动态更新 g.target_portfolio


Q&A

Q: 为什么使用 order_target_value 而不是 order_target (按数量)?
A: 构建指数组合时,我们通常关注的是资产配置比例(即金额占比)。股票价格各不相同,如果按数量分配(例如每只买100股),高价股的权重会远超低价股,导致组合偏离预期。使用 order_target_value 可以直接根据资金量进行配置,更加准确。

Q: 如果权重加起来超过 1.0 (100%) 会怎样?
A: 如果 total_value * weight 的总和超过了账户里的 portfolio_value(包含现金),在执行最后的买入操作时,可能会因为可用资金不足而导致下单失败或部分成交。建议在计算权重时进行归一化处理(即所有权重除以权重之和),确保总和不超过 1.0。

Q: 如何处理停牌的股票?
A: 在示例代码中,我们使用了 is_stock_tradable 函数跳过了停牌股票。对于指数组合来说,如果某只成分股停牌,通常的做法是保持该股票持仓不变,只调整其他可交易股票的仓位,或者将该股票的目标资金暂时保留为现金。