🚀 AI 一键生成 joinquant 策略代码
立即体验

聚宽 order_target_value 调仓实战:如何通过先卖后买与备选股票递补实现完美再平衡?

joinquant | 发布时间: 2026/8/7
以下内容由 EasyQuant 生成。

问题描述

聚宽调仓实战:order_target_value 仓位不足与先卖后买如何处理?
使用聚宽 order_target_value 调仓时出现目标仓位无法打满,如何通过先卖出回收现金、再买入并用备选股票递补来完成组合再平衡?

解决方案

在聚宽(JoinQuant)平台进行多股组合调仓时,直接遍历股票池并调用 order_target_value 极易触发**资金不足(Insufficient Cash)**的警告。这是因为默认的执行顺序可能是先尝试买入新股票,而此时旧股票尚未卖出,导致可用资金(available_cash)不足。此外,若某些目标股因停牌、涨跌停无法买入,会导致整体仓位无法打满。

为了解决这一痛点,实战中必须采用**“先卖后买、动态计算、备选递补”**的精细化调仓控制流。


一、 核心调仓逻辑设计

  1. 第一步:识别并执行卖出(回收资金)
    比较当前持仓与目标持仓。对于需要减仓或清仓的股票,优先执行卖出操作。这样可以最大化回收可用现金。
  2. 第二步:计算实际可分配资金
    在卖出订单撮合或执行后,获取当前账户的最新总资产(total_value)和可用资金(available_cash),重新计算每个买入标的的目标绝对价值。
  3. 第三步:按优先级买入与备选递补
    依次买入首选股票。若遇到停牌、涨停等无法买入的情况,或者资金仍有富余,则自动从备选股票池(Backup List)中提取标的进行递补,直至仓位达到设定目标。

二、 聚宽实战策略源码

以下是一个完整的、可直接在聚宽回测运行的策略示例。该策略展示了如何在每月初进行再平衡,采用“先卖后买”并引入备选股票池递补的机制:

import pandas as pd
from jqdata import *

def initialize(context):
    # 设定沪深300作为基准
    set_benchmark('000300.XSHG')
    # 开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    
    # 设定交易税费
    set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
    
    # 策略参数
    g.target_num = 5  # 目标持仓股票数量
    
    # 每月第一个交易日开盘运行调仓
    run_monthly(rebalance_portfolio, monthday=1, time='09:30')

def rebalance_portfolio(context):
    log.info("====== 开始执行组合再平衡 ======")
    
    # 1. 获取候选股票池与备选股票池
    # 假设我们以中证500成分股作为选股池,按某种规则排序(此处简化为取前15只)
    all_candidates = get_index_stocks('000905.XSHG')[:15]
    
    # 过滤掉停牌、ST的股票
    current_data = get_current_data()
    valid_candidates = [s for s in all_candidates if not current_data[s].paused and not current_data[s].is_st]
    
    # 首选目标买入列表(前5只)
    preferred_targets = valid_candidates[:g.target_num]
    # 备选递补列表(其余的作为备选)
    backup_targets = valid_candidates[g.target_num:]
    
    log.info("首选目标: %s" % preferred_targets)
    log.info("备选目标: %s" % backup_targets[:5])
    
    # 2. 区分“卖出”与“买入”队列
    current_positions = list(context.portfolio.positions.keys())
    
    # 需要完全清仓的股票
    to_sell_completely = set(current_positions) - set(preferred_targets)
    
    # 执行完全清仓(先卖)
    for stock in to_sell_completely:
        log.info("清仓卖出: %s" % stock)
        order_target_value(stock, 0)
        
    # 3. 重新评估可用资金并分配买入
    # 此时卖出单已提交(回测中市价单会立即撮合回收资金)
    total_value = context.portfolio.total_value
    single_target_value = total_value / g.target_num  # 每只股票计划分配的价值
    
    active_holdings = []  # 记录最终成功建仓的股票
    
    # 4. 尝试买入首选股票
    for stock in preferred_targets:
        # 检查是否可以交易
        if current_data[stock].paused or current_data[stock].last_price == 0:
            continue
        
        # 计算当前持仓价值
        current_val = context.portfolio.positions[stock].value
        # 如果当前价值与目标价值差异较大,进行调仓
        if abs(current_val - single_target_value) > (single_target_value * 0.1):
            order_target_value(stock, single_target_value)
            log.info("调仓/买入首选股: %s, 目标价值: %.2f" % (stock, single_target_value))
        
        active_holdings.append(stock)
        
    # 5. 备选递补机制
    # 如果首选股有因停牌等原因未买入的,用备选股补齐至 g.target_num 只
    needed_count = g.target_num - len(active_holdings)
    if needed_count > 0:
        log.info("首选股未满,启动备选递补,需递补数量: %d" % needed_count)
        for backup_stock in backup_targets:
            if needed_count <= 0:
                break
            
            if backup_stock in context.portfolio.positions:
                continue  # 已持有的不重复买入
                
            if not current_data[backup_stock].paused:
                order_target_value(backup_stock, single_target_value)
                log.info("递补买入备选股: %s, 目标价值: %.2f" % (backup_stock, single_target_value))
                active_holdings.append(backup_stock)
                needed_count -= 1

    log.info("====== 调仓结束,当前持仓: %s ======" % list(context.portfolio.positions.keys()))

三、 关键点解析与避坑指南

  1. 回测与模拟盘的撮合差异
    在聚宽回测中,order_target_value(stock, 0) 市价单会立即同步撮合,资金瞬间到账,因此紧接着的买入操作可以直接使用这部分资金。但在实盘或高仿真模拟盘中,订单提交到撮合成交有时间差,建议在卖出后,通过事件回调或在下一个分钟 Bar 再进行买入操作,以防资金未及时到账。
  2. 避免跨日缓存 get_current_data()
    在调仓时,务必使用最新的 get_current_data() 获取当日的 paused(停牌)和 is_st(ST)状态,防止将资金浪费在无法交易的标的上。
  3. 滑点与交易税费预留
    代码中将总资产均分为 g.target_num 份。在实际极端情况下,由于印花税和佣金扣除,最后一只股票可能会因差几块钱而无法完全买入。可以适当将 single_target_value 乘以 0.99(预留 1% 的安全资金缓冲)。