可转债多因子策略

可转债多因子策略在现代“固收+”投资中扮演着至关重要的角色。通过系统化的因子筛选与量化建模,该策略能够有效平衡转债的“债底保护”与“股性进攻”,帮助投资者在震荡市中捕捉超额收益,实现风险调整后的收益最大化。在中低频量化领域,多因子策略对数据处理精度与组合回撤控制的助益尤为明显。

在投入实盘交易之前,利用全量历史数据对因子有效性与策略稳健性进行严谨的回测评估,是确保投资逻辑成立的关键步骤。DolphinDB 凭借其高性能分布式计算引擎与内置的 Alphalens 因子分析模块,成为可转债多因子策略研究与回测的理想基础设施。在本文中,我们将集成一个涵盖五因子策略实现、因子分析、机器学习的可转债多因子策略案例,详细介绍 DolphinDB 在中低频多因子策略开发中的全流程应用。

1. 背景介绍

可转债作为一种兼具股债特性的混合金融工具,其定价逻辑复杂,单一依赖“双低”或条款博弈的传统主观策略,往往难以适应多变的市场环境。相比之下,可转债多因子策略通过系统化的量化模型,选取估值、动量、波动率等多个维度的因子,对全市场转债进行客观评分与轮动,能够有效捕捉市场定价偏差。量化多因子策略高度重视数据的实证分析,通常利用 IC 分析和分层回测来验证因子的有效性,并据此动态调整因子权重。同时,为了捕捉非线性关系,现代转债策略常引入机器学习(如 XGBoost )模型替代传统线性加权,进一步提升策略的进攻性与防御性。

本文以国金证券的《可转债量化择时、轮动、选券策略年中献礼》研报为研究基础,系统性地完善基于多维度因子的量化回测框架。本篇教程内容将围绕以下三个方面展开:

  1. 五因子策略复现:从数据处理与清洗开始,复现研报中的经典多因子体系,构建基于因子打分的可转债轮动策略,并通过严格的回测验证其在不同市场周期下的表现。

  2. 因子分析和多因子策略实现:借助 Alphalens 评价模块,通过信息系数分析与分层回测对因子进行有效性检验和筛选,筛选出各个维度具备 Alpha 能力的因子,形成可转债多因子策略。

  3. 机器学习赋能:突破传统线性加权模型的局限,引入 XGBoost 机器学习算法,对因子与未来收益的非线性关系进行拟合,在回测中应用机器学习模型进行预测。

2. 基于 DolphinDB 的可转债五因子策略

2.1 策略介绍

该策略为可转债五因子策略,选取能够反映可转债价值的低价、转股溢价率、双低因子历史分位数、隐含波动率、转债价格动量五个在收益相关性和超额稳定性方面均表现较优的因子进行择券。策略核心逻辑为:通过可转债五因子对可转债进行综合打分,选取得分靠前的标的构建投资组合,定期调仓以动态优化持仓:

  1. 因子综合打分:计算每个可转债的因子加权得分。

  2. 标的筛选:选取得分排名前 topPercent(如 25%)的标的作为买入候选池。

  3. 调仓执行:每周一进行调仓,卖出不在买入池中的持仓,买入新入选的标的。

  4. 风险控制:等权分配资金,单只标的持仓市值不超过总权益的 maxPosition(如 2%)。

2.2 策略实现

本章节基于可转债日频数据,利用 DolphinDB 回测插件实现了一个按得分阈值筛选进行日频调仓的回测示例。策略每日基于前一日生成的信号分配仓位,在当日根据目标仓位调整持仓,保持持有因子得分更高的可转债标的,卖出得分排名不在前 topPercent 的标的。策略实现流程如下所示:

  1. 策略输入:准备行情表和付息表

  2. 策略开发:在回调函数编写自定义策略

  3. 策略配置:根据策略设置相应的配置参数

  4. 策略创建与执行:创建回测引擎并插入行情执行回测

  5. 策略输出:调用接口获取回测结果

2.2.1 策略输入:准备付息表和行情表

2.2.1.1 付息表

付息表是可转债持有期间债券固定收益测算的基础,在启动可转债策略回测前,需将付息信息处理成回测引擎要求的数据结构,并通过 stockDividend 参数输入回测引擎,完整付息表数据结构要求可参考:可转债付息表。

表 1. 表 2-1 可转债付息表结构
字段 说明
symbol 可转债代码
recordDate 登记日
payDate 可转债付息日
afterTaxCashDiv 可转债票面利率

需要注意的是,该表登记日和可转债付息日均需预处理成交易日,并要求行情表中有对应的交易日行情,即付息表处理为:

  • 对于付息日,如该日为法定节假日或休息日,则顺延至下一个交易日,对于登记日,取付息日的前一交易日。

  • 根据行情交易日筛选付息表,要求登记日、付息日在行情交易日中。

2.2.1.2 行情表

该策略为基于因子选债的可转债日频策略,需要准备可转债日频行情和因子数据合并表,作为策略行情输入,通过 appendQuotationMsg 接口向回测引擎注入数据,完整行情数据结构可参考行情数据结构说明。本策略行情表结构如下:

表 2. 表 2-2 可转债行情表结构
字段 类型 说明
symbol SYMBOL 可转债代码
tradeTime TIMESTAMP 交易时间
open DOUBLE 开盘价
low DOUBLE 最低价
high DOUBLE 最高价
close DOUBLE 收盘价
volume LONG 成交量
upLimitPrice DOUBLE 涨停价
downLimitPrice DOUBLE 跌停价
cb_over_rate DOUBLE 转股溢价率
double_low_percentile_3m DOUBLE 双低历史分位数
iv DOUBLE 隐含波动率
cb_Mom_20D DOUBLE 20 日动量

2.2.2 策略开发:在回调函数编写自定义策略

2.2.2.1 策略回调函数

通过 onBar 行情回调函数编写策略主逻辑,在行情到来时执行相应策略操作。同时,用户可根据实际需要运用不同回调函数开发策略逻辑,策略回调函数用法见策略回调函数说明 。本策略行情回调函数主要处理如下:

  1. 首先,对前一天生成的仓位信号执行调仓,对卖单列表标的执行卖出,对买单列表标的补充到目标仓位。

  2. 其次,基于当天的行情和因子信息,计算所有有效标的得分,写入因子得分字典。

  3. 最后,根据得分字典,对于不在得分排名前 topPercent 的标的,记录为卖单,获取当前持仓,目标卖出仓位即为当前持仓,对于在得分排名前 topPercent 的标的,记录为买单,对入选标的等权分配资金,并根据当前持仓市值和目标持仓市值比较,确定补仓量。

def onBar(mutable context, msg, indicator){
    // t日,根据t-1日盘后记录的委托单下单撮合
    // 执行卖出
    if (size(context['sellList']) >0){
        for(isbond in context['sellList']){ // 遍历卖单列表
            sellVolume = exec Volume[0] from context['sellOrders'] where symbol = isbond // 从委托单信息获取卖单量
            price = exec price[0] from context['sellOrders'] where symbol = isbond 
            tradeTime = msg[isbond]["tradeTime"]  
            if(sellVolume> 0 && !isNull(price) && !isNull(tradeTime)) {  //执行卖出
                Backtest::submitOrder(context["engine"],(isbond, tradeTime, 5, price, sellVolume, 3), "sell")  
                // submitOrder接口用于下单,对于可转债,参数依次为(回测引擎,(证券代码,交易时间,订单类型,价格,数量,买卖方向),买卖标签)
                // 订单类型:
                // 0:市价单中最优五档即时成交剩余撤销委托订单
                // 1:市价单中即时成交剩余撤销委托订单
                // 2:市价单中本方最优价格委托订单
                // 3: 市价单中对手方最优价格委托订单
                // 4:市价单中全额成交或撤销委托订单
                // 5:限价单
                // 买卖方向:1-买开,2-卖开,3-卖平,4-买平
            }  
        }  
    }
    
    // 执行买入
    if(size(context['buyList']) >0) {  
        for(isbond in context['buyList']){  // 遍历买单列表
            buyVolume = exec Volume[0] from context['buyOrders'] where symbol = isbond // 从委托单信息获取买单量
            price = exec price[0] from context['buyOrders'] where symbol = isbond 
            tradeTime = msg[isbond]["tradeTime"]  
            if(buyVolume >0 &&!isNull(price) && !isNull(tradeTime)) {// 执行买入
                Backtest::submitOrder(context["engine"], (isbond, tradeTime, 5, price, buyVolume, 1), "buy")  
            }      
        }  
    }

    // 初始化因子得分字典和有效债券列表
    factor_convPremium_Dict = dict(STRING, DOUBLE)  
    validBonds = array(STRING) 

    // 计算标的得分字典与validBonds
    keys = msg.keys()
    for(sym in keys){
        // 获取策略参数(即策略配置中的用户自定义参数)
        maxPosition = context["maxPosition"]
        w_close = context["close_weight"]
        w_cb_over_rate = context["cb_over_rate_weight"]
        w_price_premium_3m = context["price_premium_percentile_3m_weight"]
        w_iv = context["iv_weight"]
        w_mom20d = context["CB_Mom_20D_weight"]
        
        // 获取因子值(因子与行情连接得到,即行情中的字段)
        f_close = msg[sym]["close"]
        f_cb_over_rate = msg[sym]["cb_over_rate"]
        f_double_low_percentile_3m = msg[sym]["double_low_percentile_3m"]
        f_iv = msg[sym]["iv"]
        f_cb_Mom_20D = msg[sym]["cb_Mom_20D"]
        dt = msg[sym]["tradeTime"]

        // 过滤有效标的,计算得分并更新字典与 validBonds
        if(!isNull(f_close) && !isNull(f_cb_over_rate) && !isNull(f_double_low_percentile_3m) && !isNull(f_iv) && !isNull(f_cb_Mom_20D)){
            score = (w_close*f_close + w_cb_over_rate*f_cb_over_rate +
                w_price_premium_3m*f_double_low_percentile_3m +
                w_iv*f_iv + w_mom20d*f_cb_Mom_20D) * (-1)
            factor_convPremium_Dict[sym] = score
            validBonds.append!(sym)
        }
    }

    // t日,根据得分结果,生成委托单列表,用于t+1日交易
    // 初始化委托相关信息
    buyList = array(STRING)  
    sellList = array(STRING)
    buyOrders = table(1:0, `symbol`direction`tradeTime`price`Volume`score,[SYMBOL,INT,TIMESTAMP,DOUBLE,DOUBLE,DOUBLE])
    sellOrders = table(1:0, `symbol`direction`tradeTime`price`Volume`score,[SYMBOL,INT,TIMESTAMP,DOUBLE,DOUBLE,DOUBLE])

    // 计算阈值
    sortedFactors = sort(factor_convPremium_Dict.values(), false)
    topThreshold = sortedFactors[int(size(sortedFactors) * context["topPercent"])] 

    // 获取当前资金
    cash = Backtest::getAvailableCash(context.engine) 
    // 标记第二天下单时间
    tradeTime = temporalAdd(context['tradeTime'],1,'SZSE') 

    // 生成卖单委托
    for(isbond in validBonds){  
        price = msg[isbond]["close"] 
        factor = factor_convPremium_Dict[isbond]
        pos = Backtest::getPosition(context["engine"], isbond)  
        longPos = pos.longPosition[0] // 获取现有持仓量
        if(longPos > 0 and factor < topThreshold){  // 不在目标阈值的债券卖出
            sellList.append!(isbond)  
            sellOrders.tableInsert(isbond,2,tradeTime,price,longPos,factor)
            cash + = price*longPos // 更新可用资金
        } 
    } 

    // 生成买单池子
    for(isbond in validBonds){  
        factor = factor_convPremium_Dict[isbond]
        if(factor >= topThreshold) {  
            buyList.append!(isbond)  
        }  
    }

    // 根据当前资金情况,计算买单目标持仓
    k = size(buyList)
    if(k > 0){
        // 等权分配资金
        perCash = cash / k   
        maxValue = cash * context["maxPosition"]
        for(isbond in buyList){ 
            // 获取当前持仓量
            pos = Backtest::getPosition(context["engine"], isbond)  
            longPos = 0
            if(!isNull(pos.longPosition) && size(pos.longPosition) > 0){
                longPos = pos.longPosition[0]
            }
            // 计算目标持仓
            factor = factor_convPremium_Dict[isbond]
            price = msg[isbond]["close"] 
            currentValue = longPos * price  // 当前持仓市值
            if(currentValue < maxValue) {    
                addValue = min(maxValue-currentValue, perCash) *0.9
                buyVolume = int(addValue / price / 100) * 100 
                if(!isNull(price) && !isNull(tradeTime)) {
                    buyOrders.tableInsert(isbond,1,tradeTime,price,buyVolume,factor)
                }  
            }   
        }
    }
    // 记录t+1日买卖债券列表,委托信息
    context['buyList'] = buyList
    context['sellList'] = sellList
    context['buyOrders'] = buyOrders
    context['sellOrders'] = sellOrders
    context['allOrders'] = unionAll(buyOrders,sellOrders)   
}
2.2.2.2 策略回调函数参数
  1. context

    策略全局变量 context 为一个字典,用于设置策略的所有自定义变量,在所有回调函数均可调用。通过 context["字段名称"] 或 context.字段名称 调用。若要使用 JIT 优化,context 中用户定义的全局变量不支持表,其他所有 DolphinDB 的数据类型均支持。引擎内部维护了 4 个变量:

    • context.tradeTime 获取行情的最新时间

    • context.tradeDate 获取当前日期

    • context.BarTime 快照降频为低频行情时的当前 Bar 时间戳

    • context.engine 获取回测引擎实例

    其他 context 变量根据用户输入参数确定,可参见 2.2.3.2 用户自定义参数小节。

  2. msg

    回测引擎的策略事件函数行情消息 msg 为字典,在 onBar 等策略行情回调函数调用。onBar 行情回调函数中的 msg 为嵌套字典,表示多条消息。第一层的字典的 key 是标的代码(sym),value 值为该标的的行情,例如可以通过 msg.sym.close 获取最新收盘价。该字典部分内容示例如下:

    123045.SZ->symbol->123045.SZ
    tradeTime->2021.03.01T00:00:00.000
    open->101.599999999999994
    low->101.469999999999998
    high->103.89
    close->103.310000000000002
    volume->22536
    upLimitPrice->121.680000000000006
    downLimitPrice->81.120000000000004
    cb_over_rate->12.289999999999999
    double_low_percentile_3m->-1.155022796336155
    iv->-0.131868259038364
    cb_Mom_20D->-0.33366397320168
    
    123046.SZ->symbol->123046.SZ
    tradeTime->2021.03.01T00:00:00.000
    open->136.879999999999995
    low->136.050000000000011
    high->143.5
    close->141.060000000000002
    volume->66437
    upLimitPrice->163.199999999999988
    downLimitPrice->108.800000000000011
    cb_over_rate->6.77
    double_low_percentile_3m->-0.250412235413919
    iv->0.714654335556472
    cb_Mom_20D->-0.976648195453703
  3. indicator

    indicator 表示策略中订阅的指标字典,行情指标 indicator 的数据类型同相应行情的 msg 的数据类型一致。在 initialize 策略初始化函数中通过 subscribeIndicator 接口订阅行情指标后,可在 onBar 等策略行情回调函数调用。onBar 行情回调函数中的 indicator 为嵌套字典,表示多条消息。第一层的字典的 key 是标的代码(sym),value 值为该标的的行情,可以通过类似 indicator.sym.mavg 调用。本策略暂未用到 indicator 参数。

2.2.3 策略配置:根据策略设置相应的参数

2.2.3.1 策略参数

设置回测配置参数,例如开始与结束日期、初始资金、手续费等,详细回测参数配置要求可参考引擎配置说明文档。策略初始参数配置如下所示,可根据实际需要进行调整。

表 3. 表 2-3 策略配置参数
字段 说明 备注
startDate 开始日期 DATE 类型,设为 2021.01.01
endDate 结束日期 DATE 类型,设为 2025.12.31
strategyGroup 策略类型

STRING 类型,"stock" ,

可转债按 "stock" 接口处理

cash 初始资金 DOUBLE 类型,设为 300000000
commission 手续费 DOUBLE 类型,设为 0.00015
tax 印花税 DOUBLE 类型,设为 0.0
dataType 行情类型 INT 类型,4 代表日频行情
msgAsTable 行情的数据格式 BOOL 类型,默认 false ,字典
outputOrderInfo 是否输出风控日志 BOOL 类型,设为 true ,代表输出风控日志
matchingMode 订单撮合模式 INT 类型,设为 3:以委托价格成交。
stockDividend 付息表

DICT 类型,如包含可转债必须使用 DICT 类型:

dividend=dict(STRING,ANY)
dividend["convertibleBonds"]=couponData
userConfig["stockDividend"]=dividend
2.2.3.2 用户自定义参数

除引擎配置参数外,用户还可通过 userConfig['context'] 接口配置自定义参数,本策略用户自定义参数如下表所示。

表 4. 表 2-4 用户自定义参数
字段 说明 备注
topPercent 选券阈值 DOUBLE 类型,筛选前 topPercent 标的
maxPosition 每券最大持仓比例 DOUBLE 类型,控制单券最大持仓
close_weight 收盘价因子权重 DOUBLE 类型,用于因子加权
cb_over_rate_weight 转股溢价率因子权重 DOUBLE 类型,用于因子加权
price_premium_percentile_3m_weight 双低历史分位数因子权重 DOUBLE 类型,用于因子加权
CB_Mom_20D_weight 20日动量因子权重 DOUBLE 类型,用于因子加权
iv_weight 隐含波动率因子权重 DOUBLE 类型,用于因子加权
buyList 买标的列表 STRING VECTOR ,用于存放当日买标的
sellList 卖标的列表 STRING VECTOR ,用于存放当日卖标的
buyOrders 委托买单表 TABLE ,用于存放委托买单量价信息
sellOrders 委托卖单表 TABLE ,用于存放委托卖单量价信息

2.2.4 策略创建与执行:创建回测引擎并插入行情执行回测

调用 Backtest::createBacktester 创建回测引擎,参数为引擎名称、配置参数、事件函数。详情可参考文档 subscribeIndicator。

// 策略名称和回调函数
strategyName="Backtest_cbond"
callbacks=dict(STRING,ANY)
callbacks["initialize"]=initialize
callbacks["onBar"]=onBar
// 创建回测
try{Backtest::dropBacktestEngine(strategyName)}catch(ex){print ex}
engine = Backtest::createBacktester(strategyName, userConfig, callbacks,, )

插入处理好的行情数据执行回测,result 为对应的可转债日频行情数据,其中包含日频因子数据。

timer Backtest::appendQuotationMsg(engine,result);

2.2.5 策略输出:调用接口获取回测结果

回测运行结束之后,可以通过相应的接口获取每日持仓、每日权益、收益概述、成交明细和策略中用户自定义的逻辑上下文,如下所示。回测插件提供的完整回测结果接口可以参阅策略回调函数说明。

2.2.5.1 交易明细表

通过 Backtest::getTradeDetails(long(engine)) 获取每日交易明细表,输出字段详细说明见 getTradeDetails。下表为部分交易明细展示:

图 1. 图 2-1 部分交易明细结果
2.2.5.2 账户每日持仓表

通过 Backtest::getDailyPosition(long(engine)) 接口获取账户每日持仓,输出字段详细说明见 getPosition。下表为部分持仓明细展示:

图 2. 图 2-2 部分持仓明细结果
2.2.5.3 账户每日权益表

通过 Backtest::getDailyTotalPortfolios(long(engine)) 接口获取账户每日权益信息,输出字段详细说明见 getDailyTotalPortfolios。下表为部分每日权益展示:

图 3. 图 2-3 部分每日权益结果
2.2.5.4 账户收益表

通过 Backtest::getReturnSummary(long(engine)) 接口获取策略收益概述,输出字段详细说明见 getReturnSummary。下表为可转债五因子策略的收益概述:

图 4. 图 2-5 账户收益结果
2.2.5.5 用户自定义参数

除了上述回测引擎相关输出外,用户还可根据需要调用 Backtest::getContextDict(long(engine)) 接口输出自定义参数,例如本策略中的委托单信息,如下所示为委托买单信息:

图 5. 图 2-6 自定义参数——委托买单结果

3. 基于 Alphalens 评价模块的可转债因子分析与策略优化

3.1 因子分析

可转债五因子策略所用的因子根据研报经验确定,未与当前数据和回测区间对齐,故本部分基于可转债微观因子框架的 20 个因子建立待选因子池,对待选因子进行单因子分析和多因子分析,剔除无效因子,保留剩余因子,建立可转债多因子模型,进行策略回测。基于因子分析的策略回测具体步骤如下:

  1. 建立待选因子池

  2. 根据待分析因子池,进行单因子分析,选择 IC 和分层回测表现较好因子;

  3. 根据单因子分析有效的因子,进行多因子相关性分析,剔除相关性较高的因子,得到最终有效因子列表;

  4. 根据最终有效因子列表,按 IC 加权得到权重,作为策略回测打分依据;

  5. 进行策略回测。

按上述步骤,附录脚本执行链路为:2-1.因子分析/21.factorAnalysis.dos→3.策略回测/32.analysisBacktest.dos。

首先,建立可转债因子池,待选因子池如下:

表 5. 表 3-1 可转债待选因子池
因子类型 因子名称 因子定义
债券属性 转换价值 conv_value 转化成正股后对应的价格,与股价相关
纯债价值 bond_value 又称之为债底
到期时间 cb_ptm -
票面利率 coupon_rate -
剩余额度 remain_size -
到期收益率 cb_ytm 转债的到期收益率
溢价率 转股溢价率 cb_over_rate 转债价格/转股价值-1
纯债溢价率 bond_over_rate 转债价格/纯债价值-1
平底溢价率 bond_premium_rate 转股价值/纯债价值-1
双低因子 double_low_factor 转股溢价率与价格等权合成
双低分位 double_low_percentile_3m 双低因子处于过去区间内的分位数
波动率 隐含波动率 iv BS 公式反推所得
隐波差 volatility_20diff 隐含波动率-正股波动率
波动率 cb_volatility_20d 转债涨跌幅过去 20 日标准差
量价类 日均换手率 cb_turnover_rate 成交量/余额
转债正股换手率差异 turnover_diff 转债换手-正股换手
价格偏度 cb_skew_60d 过去 60 个交易日可转债市价偏度
转债动量 cb_Mom_20D 过去 20 日价格涨跌幅
振幅差 amplitude_diff 转债振幅-正股振幅
涨幅差 pct_chg_diff 转债涨跌幅-正股涨跌幅
成交量 cb_vol 代表转债的交易热度
收益成交比 cb_vol abs(日收益率)/日成交额,代表转债的市场热度

3.1.1 单因子分析

运用 Alphalens 模块进行单因子分析,通过 use alphalens 加载后即可使用,可参阅 Alphalens 在 DolphinDB 中的应用:因子分析建模实践。下面自定义用 Alphalens 进行单因子分析的函数,返回 IC 分析和分层累计收益结果:

use alphalens // 加载alphalens模块,用于单因子分析
go 

// 定义单因子分析函数
def singleFactorTest(factorName,factorData){
    factorPt = sql(select=[sqlColAlias(<date(tradeTime)>,"date"), sqlColAlias(<symbol>,"asset"),sqlColAlias(sqlCol(factorName),"factor")],from=factorData).eval()
    dayClose = select close from factorData pivot by date(tradeTime) as date,symbol as securityId

    cleanFactorAndForwardReturns = get_clean_factor_and_forward_returns(
        factor=factorPt,
        prices=dayClose,
        groupby=NULL,
        binning_by_group=false, 
        quantiles=5,  // 分5组
        bins=NULL,  
        periods=[1, 5, 10],  // 持仓周期
        filter_zscore=NULL,  // 异常值过滤
        groupby_labels=NULL,
        max_loss=0.35,
        zero_aware=false,
        cumulative_returns=true)

        fullTearSheet = plot_create_full_tear_sheet(
        factor_data=cleanFactorAndForwardReturns, 
        long_short=true,  
        group_neutral=false,
        by_group=false)

        Information_Analysis =fullTearSheet['plot_information_tear_sheet'].Information_Analysis
        cum_ret = fullTearSheet['plot_returns_tear_sheet'].cum_ret
        return Information_Analysis,cum_ret
}

将因子数据转换为窄表,调用单因子分析函数,对候选因子进行单因子分析:

// 筛选出分层数>5的日期,保证分层回测的有效性
CB_factors_narrow = CB_factors.unpivot(keyColNames=`tradeTime`symbol,valueColNames=CB_factors.schema().colDefs.name[4:]).rename!(`valueType`value,`factor_name`factor_value)
CB_factors_narrow = select distinct(factor_value) as unique_count from CB_factors_narrow group by date(tradeTime) as trade_date, factor_name
invalid_date = exec distinct(trade_date)  from CB_factors_narrow where rowCount(unique_count)<5

CB_factors = select * from CB_factors where date(tradeTime) not in invalid_date

// 进行因子分析
Informations = dict(STRING,ANY)
rets = dict(STRING,ANY)
for (factorName in factorNames){
    print(factorName)
    Information_Analysis,cum_ret = singleFactorTest(factorName,CB_factors)
    Informations[factorName] = Information_Analysis
    rets[factorName] = cum_ret
}

下表为隐含波动率 iv 的 IC 分析的结果示例:

图 6. 图 3-1 隐含波动率因子 IC 分析结果

下图为为隐含波动率 iv 的分层回测结果示例:

图 7. 图 3-2 隐含波动率因子分层回测结果

基于上述单因子分析,在日频数据和日频调仓情况下,以 1 日 IC 均值为筛选参考,结合分层回测结果,剔除 1 日 IC 均值小于 0.03 且分层回测不显著的因子,剩余有效因子如下:

图 8. 图 3-3 单因子分析有效因子

3.1.2 多因子分析

计算因子间相关系数矩阵,剔除强相关因子,相关系数矩阵计算脚本如下:

// 计算相关系数矩阵
valid_factorNames_sort = sort(valid_factorNames) // 对因子名称进行排序,以满足索引矩阵要求
factors = <select _$$valid_factorNames_sort from CB_factors>.eval()
factors_m = matrix(factors)
factors_corr = corrMatrix(factors_m)
factors_corr.rename!(valid_factorNames_sort,valid_factorNames_sort) 
factors_corr.setIndexedMatrix!()

根据相关系数矩阵结果,bond_over_rate 纯债溢价率因子和 bond_premium_rate 平底溢价率因子高度正相关,cb_volatility_20d 波动率因子与cb_turnover_rate 日均换手率因子高度正相关,剔除 IC 值更小的因子,最终因子列表如下:

图 9. 图 3-4 最终有效因子

根据 IC 均值,计算最终因子权重,结果如下表所示:

图 10. 图 3-5 有效因子 IC 均值及权重结果

3.2 策略回测

选取上述因子实现可转债多因子策略,保持策略其他配置不变,完整代码见附录。

在行情回调函数 onBar 中修改因子得分计算:

for(sym in keys){
        // 获取因子值
        f_bond_over_rate = msg[sym]["bond_over_rate"] 
        f_cb_volatility_20d = msg[sym]["cb_volatility_20d"] 
        f_cb_vol = msg[sym]["cb_vol"] 
        f_iv = msg[sym]["iv"] 
        f_conv_value = msg[sym]["conv_value"] 
        f_double_low_percentile_3m = msg[sym]["double_low_percentile_3m"] 
        f_cb_Mom_20D = msg[sym]["cb_Mom_20D"] 

        // 计算得分并更新字典与 validBonds
        if(!isNull(f_bond_over_rate) && !isNull(f_cb_volatility_20d)  && !isNull(f_cb_vol)  && !isNull(f_iv) && 
                !isNull(f_conv_value) && !isNull(f_double_low_percentile_3m) && !isNull(f_cb_Mom_20D)){
                score = (context["bond_over_rate_weight"]*f_bond_over_rate +
                        context["cb_volatility_20d_weight"]*f_cb_volatility_20d+
                        context["cb_vol_weight"]*f_cb_vol+
                        context["iv_weight"]*f_iv+
                        context["conv_value_weight"]*f_conv_value+
                        context["double_low_percentile_3m_weight"]*f_double_low_percentile_3m+
                        context["cb_Mom_20D_weight"]*f_cb_Mom_20D)
                factor_convPremium_Dict[sym] = score
                validBonds.append!(sym)
        }
    }

在用户自定义参数中修改因子权重:

// 用户自定义参数
Context = dict(STRING, ANY)
Context["topPercent"]=0.25  // 选股筛选因子的比例
Context["maxPosition"]=0.02 // 每股最大持仓比例
// 从因子分析结果final_factor表获取因子权重
Context["bond_over_rate_weight"] = exec factor_weights from final_factor where factorName ="bond_over_rate" // -0.195231
Context["cb_volatility_20d_weight"] = exec factor_weights from final_factor where factorName ="cb_volatility_20d" // -0.186958  
Context["cb_vol_weight"] = exec factor_weights from final_factor where factorName ="cb_vol" // -0.164259   
Context["conv_value_weight"] =exec factor_weights from final_factor where factorName ="conv_value"  // -0.126126
Context["double_low_percentile_3m_weight"] = exec factor_weights from final_factor where factorName ="double_low_percentile_3m" // -0.116861 
Context["iv_weight"] = exec factor_weights from final_factor where factorName ="iv" // -0.108875
Context["cb_Mom_20D_weight"] = exec factor_weights from final_factor where factorName ="cb_Mom_20D" // -0.101689
userConfig['context'] = Context

执行回测并获取回测结果:

timer Backtest::appendQuotationMsg(engine,result);

4. 基于 XGBoost 插件的可转债预测与策略优化

本章节说明如何利用 DolphinDB 高效的 XGBoost 插件,将因子从传统的线性组合升级为预测模型,并将模型预测结果转化为直观的交易信号,应用于策略回测。

4.1 模型训练

首先基于前期因子分析筛选出的有效因子,选择 XGBoost 模型以可转债微观因子为特征,1 日后“涨”、 “跌”结果为标签进行模型训练。

4.1.1 加载XGBoost插件

使用 XGBoost 进行模型训练,需要先安装和加载插件,可参考文档 xgboost 。

/* ======================================= 0.加载插件 ======================================= */
try {loadPlugin("xgboost")}catch(ex){print ex}
go

4.1.2 定义相关函数

XGBoost 模型训练需要进行数据打标、训练集测试集划分等,故预先定义相关函数,用于后续过程,如下为二分类打标函数,其他辅助函数见附录。

/* ======================================= 1.函数定义 ======================================= */
// 构建二分类标签(基于未来 period 日收益)
/*
*  @FunctionName: get_label
*  @Brief: 按 (ffill(move(close,-period))-close)/close 计算未来收益 pct,并生成标签列 label
*  @Param: data (table) - 输入表,需包含 symbol、tradeTime、close 列
*  @Param: period (int) - 前瞻期(日),例如 1、5
*  @Param: threshold (double) - 标签阈值,pct > threshold 记为1,否则记为0
*  @Return: table - 在原表基础上增加 pct 和 label 列
*  @SampleUsage: data_label = get_label(factorData, 5, 0.0)
*/
def get_label(data,period,threshold){ // 获取未来窗口的收益率
    data_pct = SELECT (ffill(move(close,-period))-close)\close as pct,* FROM data context by symbol csort  tradeTime
    // 计算未来涨跌幅的两分类情况
    data_label = select iif(pct>threshold,1,0) as label,* from data_pct 
    return data_label
}

4.1.3 参数配置与数据准备

根据需要配置开始时间、结束时间、预测区间、分类阈值、训练集比例、训练集划分时间窗口、待选因子列名相关参数,准备训练数据和测试数据:

/* ======================================= 2.参数配置 ======================================= */
startDate = 2021.01.01
endDate = 2025.12.31
period = 1
threshold = 0
trainRatio = 0.8
splitDay = 2022.01.01
featureCols = `bond_over_rate`cb_volatility_20d`cb_vol`conv_value`double_low_percentile_3m`iv`cb_Mom_20D

/* ======================================= 3.数据准备 ======================================= */
// 因子数据
factorData = select * from loadTable("dfs://cbond_factors", "kline_factors")
    where date(tradeTime) between startDate:endDate

// 获取标签
factorData = get_label(factorData,period,threshold)

// 划分预测值和训练集
data_train, data_test = trainSplit(factorData,trainRatio)
data_train, data_test = trainSplitByDay(factorData,splitDay)

4.1.4 模型训练

设置模型参数,进行模型训练,模型参数及含义可参考 XGBoost Parameters,用户可根据实际调整模型参数以提高模型效果。

/* ======================================= 4.模型训练和预测 ======================================= */
// 模型参数设置1:以softprob为目标,mlogloss为评估
params = {
    objective: 'multi:softprob',  // 多分类目标
    max_depth: 8,                // 树的最大深度
    eta: 0.1,                    // 学习率
    min_child_weight: 1,         // 最小子节点权重
    subsample: 0.5,              // 样本采样比例
    colsample_bytree: 1,         // 特征采样比例
    num_class: 2,                // 类别数量
    num_parallel_tree: 1,        // 并行树的数量
    eval_metric: 'mlogloss'      // 评估指标使用多分类错误率
}
numBoostRound=100

// 模型训练
modelSoftprob = xgboost::train(data_train["label"], table(data_train[featureCols]), params, numBoostRound)

调用 saveModel ,将模型保存至指定路径,用于后续策略中加载模型进行预测。

modelSavePath_prob = getHomeDir()+"/uploads/pct_predict_Model_factors_prob.bin"
xgboost::saveModel(modelSoftprob, modelSavePath_prob)

4.2 模型预测与策略回测

在策略初始化函数中加载保存好的模型:

def initialize(mutable context){      
    // 加载模型
    modelSavePath = getHomeDir()+"/uploads/pct_predict_Model_factors_prob.bin"
    context['model'] = xgboost::loadModel(modelSavePath)

    // 买卖委托列表
    context['buyList'] = array(STRING) 
    context['sellList'] = array(STRING)
    context['buyOrders'] = table(1:0, `symbol`direction`tradeTime`price`Volume`score,[SYMBOL,INT,TIMESTAMP,DOUBLE,DOUBLE,DOUBLE])
    context['sellOrders'] = table(1:0, `symbol`direction`tradeTime`price`Volume`score,[SYMBOL,INT,TIMESTAMP,DOUBLE,DOUBLE,DOUBLE])
}

在行情回调函数中调用模型进行买卖得分预测,以买入得分作为 score 进行可转债筛选。在每一个行情触发时刻,策略会实时提取当前可转债的因子数据,构造为模型接受的输入格式。通过调用 xgboost::predict 接口,策略能够实时输出每只转债未来上涨的概率。如下为行情回调函数中模型预测相关脚本:

for(sym in keys){
        // 获取因子值
        f_bond_over_rate = msg[sym]["bond_over_rate"]
        f_cb_volatility_20d = msg[sym]["cb_volatility_20d"]
        f_cb_vol = msg[sym]["cb_vol"]
        f_conv_value = msg[sym]["conv_value"]
        f_double_low_percentile_3m = msg[sym]["double_low_percentile_3m"]
        f_iv = msg[sym]["iv"]
        f_cb_Mom_20D = msg[sym]["cb_Mom_20D"]

        closePrice = msg[sym]["close"]

        // 筛选有效债券拼因子表
        if(!isNull(f_bond_over_rate) && !isNull(f_cb_volatility_20d) && !isNull(f_cb_vol) &&
            !isNull(f_conv_value) && !isNull(f_double_low_percentile_3m) &&
            !isNull(f_iv) && !isNull(f_cb_Mom_20D) && !isNull(closePrice)){

            validBonds.append!(sym)

            factortb.tableInsert(
                f_bond_over_rate,
                f_cb_volatility_20d,
                f_cb_vol,
                f_conv_value,
                f_double_low_percentile_3m,
                f_iv,
                f_cb_Mom_20D
            )
        }
    }

    // xgb模型预测
    if(size(validBonds) > 0){
        predict_xg = xgboost::predict(context['model'], factortb)
        n = predict_xg.size() / 2
        predictProb = predict_xg.reshape(2:n).transpose()
        probBuy = predictProb[1]

        for(i in 0:(size(validBonds) - 1)){
            factorDict[validBonds[i]] = probBuy[i]
        }
    }

保持仓位管理、交易成本等其他回测参数与前述策略完全一致,将选股逻辑替换为 XGBoost 预测信号。回测引擎会调用训练好的 XGBoost 模型生成可转债得分,并执行相关交易,返回回测结果。下表为结合 XGBoost 模型的可转债多因子策略的收益结果,相比可转债五因子模型,策略收益显著提升。

图 11. 图 4-1 结合XGBoost模型的策略收益统计结果

5. 总结

本文系统性地展示了基于 DolphinDB 的可转债多因子研究框架:从复现国金证券研报的经典五因子体系出发,利用 Alphalens 模块进行 IC 分析与分层回测,筛选出具备稳定 Alpha 能力的核心因子;进而引入 XGBoost 机器学习插件,突破传统线性模型的局限,对因子与未来收益之间的复杂映射关系进行拟合与预测。通过本教程的案例,可以直观地展示 DolphinDB 在处理海量可转债数据、执行因子计算以及实现量化策略方面的全面工具支撑,DolphinDB 为“固收+”领域的策略开发与实证研究提供了一套高效、可靠且可扩展的量化基础设施。

6. 附录

6.1 数据获取说明

本文所用的可转债行情数据和因子原始数据来自 tushare 数据接口,用户可从 Tushare数据获取 token 后,接入相关数据。数据接入流程如下:

  1. 上传 tushare 模块文件 tushare.dos 到 DolphinDB 模块目录,默认保存在 [home]/modules 目录,home 目录可在 DolphinDB 通过 getHomeDir() 函数查看。

  2. 建立 tushare 库表,参考“6.2完整脚本”的“0.建库建表-01.createTushareDBsTBs.dos”。

  3. 获取 token 并接入数据,参考“6.2完整脚本的“1.因子数据-11.getTushareData.dos”。