专栏名称: 中金固定收益研究
中金公司固定收益研究团队倾情奉献!”专业+深度+及时+准确“,涵盖宏观利率走势研判、信用评级全覆盖、信用策略紧跟踪、转债市场精细研究。
目录
相关文章推荐
武汉本地宝  ·  湖北春节返程省内易拥堵路段汇总! ·  2 天前  
武汉本地宝  ·  2025武汉元宵节灯会活动来了!每个都想去! ·  昨天  
武汉本地宝  ·  春节假期结束!武汉本周上班时间有变! ·  昨天  
武汉本地宝  ·  中国互免签证国家将再+萨摩亚 ·  3 天前  
湖北省人民政府网  ·  今晚,武汉地铁临时调整! ·  昨天  
湖北省人民政府网  ·  今晚,武汉地铁临时调整! ·  昨天  
51好读  ›  专栏  ›  中金固定收益研究

中金转债|左右开弓,择时策略的逻辑闭环与Python建议

中金固定收益研究  · 公众号  ·  · 2024-08-03 23:09

正文


这是关于转债择时体系报告的第四篇,我们希望在这里能有一个"可堪一用"的闭环。在此前的三篇报告中,我们分别介绍了择时指标基础(估值为主,偏左侧)、关于趋势分析的工具(主要目的是判定右侧)与强化学习框架。这里,我们准备结合左侧、右侧与强化学习,谈谈如何克服实践中的障碍,建立统一模型。


图表1:转债择时策略的整合思路

资料来源:中金公司研究部


左侧,我们要做的是精简化。我们介绍过的估值指标确实足够多,也各自有一些作用,但如果希望再结合“趋势”来补齐估值过于左侧化的问题——实践中容易出现YTM越高越买,但越买越高的窘境——我们需要用一些更简单而长效的数据。反例是百元溢价率,这个指标我们自2018年提出后经常使用,投资者接受度也逐渐提高,但在更早期的市场里,这个指标存在缺失值。最终,我们选择了最简单的三个指标进入模型:价格中位数、隐含波动率中位数、债底溢价率的20日偏离度,这些数据自2010年以来均不存在缺失,意味着我们可用较长的数据进行训练。


处理方式相对简单,仅注意尽可能矢量化避免进入耗费计算量的循环即可。此外在2019年以来,市场常出现“双高”个券对估值数据影响较大,我们直接剔除。


图表2:剔除双高的转债估值计算逻辑


def stateMatrix(obj):
    mat01 = obj.matNormal # 剔除双高
    close = (obj.Close * mat01).median(axis=1).fillna(method='pad')
    impv = (obj.ImpliedVol * mat01).median(axis=1).fillna(method="pad")
    srsStrbPrem = (obj.StrbPrem *
               mat01).mean(axis=1).fillna(method="pad")

    srsStrbPrem_bias = srsStrbPrem - srsStrbPrem.rolling(20).mean()
    
    dfMat = pd.DataFrame({'median_price': close,
    'impv': impv,
    'srsStrbPrem_bias': srsStrbPrem_bias})

    return dfMat


资料来源:中金公司研究部


即便这些指标相对简单,其也有较强的代表性,对多数市场条件下的“该做”与“不该做”有了基本认识。例如下面是价格中位数在不同时间窗口下,对后续行情的预测效果。(更多结果请参考该系列的第一篇《择时体系1:这些指标怎么看,及Python实现》


图表3:不同价位下转债指数胜率

注:数值截至2024年7月17日 

资料来源:Wind,中金公司研究部


就右侧(正股趋势)而言,我们要放进怎样的代理变量?上期报告中我们给出了部分答案,对于股价走势我们能做的是划分结构、确认趋势,我们不期待右侧模型做太多预判,大体上做到明辨趋势即可。


图表4:上证指数的趋势划分(摘自:《打造趋势分析的工具》

资料来源:Wind,中金公司研究部


而在上期报告最后,我们给出了一个学习模型。数值如下所示。这里,我们直接用上一章模型产生的结果,作为“趋势”的代理变量。


图表5:趋势模型值 vs 多空趋势

资料来源:Wind,中金公司研究部


综上,进入模型的只有四个维度的数据:价格中位数、隐波中位数、债底溢价率20日偏离度、趋势模型值。追求更高精度的投资者自然可以使用更多有效的指标,和更好的模型结构,但对多数投资者来说,现实环境原本就比模型表征的更为复杂:例如资金端可能总是顺势申赎,例如买卖还要面临反向交易的限制,再如投资者本身的沟通问题——我们介绍的模型在此仍力求简单明确。


综合训练,可能遇到的几个弯路:


1.  不要做“收益率预测”,而是做“应对建议”。即便是对转债而言,对收益率给出“预测”,也非常困难。虽然直观上能有一个“收益率”预期非常吸引人,但这并不显示,一般模型会尽量给出一个接近0的数值——这是一个很接近最小二乘的结果。因此我们也不做最小二乘预期模型,而是沿用该系列报告第二篇提出的强化学习模型。简单理解下,强化模型给出买卖建议,而非收益预测。买入并不是因为收益预期高,而往往是盈利空间大,情况不对亦可在合适的时机,根据同一个模型迎来卖出信号,反之亦然。总而言之,既然我们考虑了左右侧信号,逻辑理当存在闭环,买入后有对应的卖出信号,反之亦然,就是这个维度下相对基础的闭环。


图表6:典型Agent模型结构(训练方式请见《强化学习的逐步实现》)


class Agent:
    def __init__(self, input_size, hidden_size, action_size, lr=0.001, gamma=0.9,
                 weight_decay=0.0005)
:
        self.net = DQN_InOne(input_size, hidden_size, action_size)
        self.optimizer = optim.Adam(self.net.parameters(), lr=lr, weight_decay=weight_decay)
        self.gamma = gamma # discount factor
        self .loss_fn = nn.MSELoss()

    def get_action(self, state, action_space, epsilon=0.1):
        if np.random.random() < epsilon:  # exploration
            return np.random.choice(action_space), True
        else:  
            with torch.no_grad():
                q_values = self.net(state)
                return torch.argmax(q_values).item(), False

    def update(self, state, action, reward, next_state):
        q_values = self.net(state)
        with torch.no_grad():
            q_next = self.net(next_state)
        q_target = reward + self.gamma * torch.max(q_next)

        loss = self.loss_fn(q_values[action], q_target)
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()


资料来源:中金公司研究部


2.      即便是强化学习模型,2015年的权重可能也较高。鉴于那年发生了太多难以复制的情况——转债的个数一度只有四个(甚至一度只有2支在交易),这时统计数据的有效性受到严重影响,因此,不如直接跳过2015年,而将训练集分为2010~2014以及2016~2019两段(为避免过拟合,2020年后的行情是样本外)。


图为训练3000次后,模型在2015年后的实证。有趣的是,其超额回报反而来自样本外——实际这意味着,2018~2020年间整体上行趋势和低估值下,都应保持多头,重在择券,而择时的作用反而体现在时多时空的2021年后。(此外值得注意在2020和2023年间这个测试有很多频繁切换的时点,这是因为结果数值很接近0.5,模型出现犹豫不定的情况)


图表7:强化学习模型的净值表现

注:数据自2016年至2024年7月17日 

资料来源:Wind,中金公司研究部


3.  但如何吸纳更新的经验呢:一个路线是预训练模型的微调。吸纳“最新经验”的难点在于,无法验证那是学习到了“新知识”还是“过拟合”或者“未来数据”。一个解决思路是对“新知识”也分割为训练阶段与测试阶段,而为了避免重新学习全部参数,我们可以在已有模型的基础上做“微调”——即固定大部分参数,仅在有正则化约束的情况下优化一小部分参数。


对于我们的择时模型而言,如果采取了两层或多层模型结构,那么最后一层相当于直观意义上的“综合信息后决策”,而第一层一般接近于“对高低的认知”,例如“均价为110元算高还是低”。实践中,最后一层的基础框架很少变化,那么我们可以尽量只微调第一层,以下是一个示例。


图表8:模型微调:假设只调整第一个全连接层


def train_finetune(model, dataloader): 
    param_need_update = ['fc1.weight', 'fc1.bias']
    # 冻结模型不需要调整的参数
    for name, param in model.parameters():
        if not name in param_need_update:
            param.requires_grad = False
    
    # 只有存在梯度的参数,即第一层
    optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
    # 定义损失函数,以交叉熵为例
    criterion = nn.CrossEntropyLoss()
    # 假设我们有一个数据加载器 dataloader
    for epoch in range(5): # 训练5个epoch
        for inputs, labels in dataloader:
            optimizer.zero_grad()
            outputs = model(inputs)
            loss = criterion(outputs, labels)
            
            # 反向传播和优化





请到「今天看啥」查看全文