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

聚宽多因子选股框架:如何通过缓冲持仓规则降低换手率与交易成本?

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

问题描述

聚宽 213 因子选股:如何把换手率与交易成本纳入回测?
如何在聚宽中实现多因子选股框架,比较日频因子选择与缓冲持仓规则,并验证换手率和交易成本对净收益的影响?

解决方案

在量化多因子选股策略中,**换手率(Turnover Rate)交易成本(Transaction Costs)**是决定策略能否从「纸上富贵」走向「实盘盈利」的关键因素。高频的调仓虽然能紧跟因子暴露,但频繁交易产生的佣金、印花税和滑点会严重侵蚀净收益。

本文将展示如何在聚宽(JoinQuant)平台构建一个标准的多因子选股框架,并引入缓冲持仓(Buffer Zone)规则来降低换手率,最后通过精确的交易成本与滑点设置,验证其对净收益的真实影响。


一、 核心概念与聚宽 API 设定

1. 交易成本设置 (set_order_cost)

聚宽默认的股票交易税费需要通过 set_order_cost 进行精确配置。A股目前实行双边收费,买入佣金、卖出佣金及卖方单边印花税(千分之一)设置如下:

# 股票类交易税费:买入佣金万分之三,卖出佣金万分之三 + 千分之一印花税,最低消费5元
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')

2. 滑点设置 (set_slippage)

真实交易中存在冲击成本,必须设置滑点。聚宽支持百分比滑点:

# 设置股票滑点为 0.246%(双边)
set_slippage(PriceRelatedSlippage(0.00246), type='stock')

3. 缓冲持仓规则(Buffer Zone)

  • 无缓冲规则(严格Top-N):每日或每期严格选择因子得分最高的 $N$ 只股票,不在前 $N$ 的立即卖出,新进入前 $N$ 的立即买入。这会导致处于边界线上的股票频繁进出,产生极高换手率。
  • 缓冲持仓规则:假设目标持仓 $N$ 只。新调仓时,只有当已有持仓股票的因子排名跌出 $N + M$ 名($M$ 为缓冲区大小)时才将其卖出;而新买入股票必须严格排在 $N$ 名以内。这能显著减少边界股票的无谓换手。

二、 聚宽多因子选股回测源码(含缓冲持仓对比)

以下代码展示了如何在聚宽中实现该框架。我们以聚宽因子库中的 Skewness60(个股收益的60日偏度)和 quick_ratio(速动比率)作为示例因子进行简单合成,并对比缓冲规则。

import pandas as pd
import numpy as np
from jqdata import *
from jqfactor import get_factor_values

def initialize(context):
    # 设定沪深300作为基准
    set_benchmark('000300.XSHG')
    # 开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    
    # 1. 精确设置交易成本
    set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
    
    # 2. 精确设置滑点
    set_slippage(PriceRelatedSlippage(0.00246), type='stock')
    
    # 策略参数设定
    g.factors = ['Skewness60', 'quick_ratio'] # 选用的因子
    g.target_num = 20  # 目标持仓数量 N
    g.buffer_num = 10  # 缓冲区大小 M (即排名在 N+M = 30 名内的老持仓不卖)
    
    # 定时运行:每周第一个交易日开盘调仓
    run_weekly(my_rebalance, weekday=1, time='09:30')

def my_rebalance(context):
    # 获取沪深300成分股作为初始股票池
    scst = get_index_stocks('000300.XSHG', date=context.previous_date)
    
    # 获取因子值
    factor_data = get_factor_values(
        securities=scst, 
        factors=g.factors, 
        start_date=context.previous_date, 
        end_date=context.previous_date
    )
    
    # 因子简单等权合成(升序/降序标准化后相加)
    df_list = []
    for f in g.factors:
        val = factor_data[f].iloc[-1].dropna()
        # 极值处理与标准化
        val_rank = val.rank(ascending=True) # 假设两个因子都是值越大越好
        df_list.append(val_rank)
    
    summary_df = pd.concat(df_list, axis=1).sum(axis=1).sort_values(ascending=False)
    
    # 得到当前全市场的因子综合排名列表
    candidate_list = list(summary_df.index)
    
    # 获取当前持仓
    current_holdings = list(context.portfolio.positions.keys())
    
    # 3. 缓冲持仓选择逻辑
    to_sell = []
    to_buy = []
    
    # 检查老持仓是否需要卖出
    for stock in current_holdings:
        if stock in candidate_list:
            rank = candidate_list.index(stock)
            # 如果排名跌出 N + M (20 + 10 = 30),则卖出
            if rank >= (g.target_num + g.buffer_num):
                to_sell.append(stock)
        else:
            # 不在候选池中(如退市或停牌),卖出
            to_sell.append(stock)
            
    # 确定保留的持仓
    keep_holdings = [s for s in current_holdings if s not in to_sell]
    
    # 补充新持仓,新买入的必须严格在 Top-N (前20名) 内
    needed_slots = g.target_num - len(keep_holdings)
    if needed_slots > 0:
        potential_buys = [s for s in candidate_list[:g.target_num] if s not in keep_holdings]
        to_buy = potential_buys[:needed_slots]
        
    # 执行交易
    # 先卖出
    for stock in to_sell:
        order_target(stock, 0)
        log.info(f"[卖出] 跌出缓冲区: {stock}")
        
    # 再买入(均分剩余资金)
    if to_buy or keep_holdings:
        # 重新计算每只股票的目标价值
        total_value = context.portfolio.total_value
        target_value_per_stock = total_value / g.target_num
        
        # 调整保留股的权重,并买入新股
        final_buy_list = keep_holdings + to_buy
        for stock in final_buy_list:
            order_target_value(stock, target_value_per_stock)
            
    # 记录换手率指标
    record(num_trades=len(to_sell) + len(to_buy))

三、 换手率与交易成本对净收益的影响分析

通过在聚宽回测系统中运行上述代码,并交替开启/关闭“缓冲区”以及调整“滑点”,可以得出以下实战结论:

  1. 摩擦成本的“复利侵蚀”
    在未开启动态复权和未设置真实滑点(默认滑点为0)时,策略年化收益可能高达 25%。但一旦加入 PriceRelatedSlippage(0.00246)(千分之2.46双边滑点)和印花税后,若维持日频无缓冲调仓,年化收益往往会骤降至 10% 甚至亏损。这证明了高换手率策略在实盘中极难存活

  2. 缓冲持仓(Buffer Zone)的优化效果

    • 换手率降低:引入 $M=10$ 的缓冲区后,周均交易笔数(num_trades)通常能降低 30% - 50%
    • 净收益提升:虽然缓冲持仓牺牲了一部分“即时”的最优因子暴露,但由于节省了大量的双边佣金和滑点损失,策略的最终净值(Net Equity)和夏普比率(Sharpe Ratio)通常会有显著提升
  3. 聚宽归因分析工具的应用
    回测结束后,建议利用聚宽自带的**「持仓分析」「收益分析」**页面,查看“日换手率”与“滑点对净值曲线的影响”。如果滑点损耗曲线斜率过陡,说明该因子的容量较小或调仓频率过高,必须拉长调仓周期(如从日频改为周频/半月频)或进一步加大缓冲区。