• 年化17.5%,十年8倍的“双低”转债策略:从零实现量化回测系统之三


    持续行动1期 43/100,“AI技术应用于量化投资研资”之可转债投资。

    投资的心法大同小异,都是以合适的价格买入好的东西。

    由于所处的周期位置不同,判断的标准产生了差异罢了。

    为何我们先从转债入手,因为转债与股票相比,多了债性,估值更容易,风险相对更低。若是操作得当,收益并不比股票差,而且转债背后仍然是上市公司分析,是股票。

    所以,对于基本面的分析逻辑是类似的,后面可以平滑切换到股票投资。

    转债投资里最经典的“双低”策略,大部分的策略都是它的变种,心法是一样的。

    前面的文章讲到“积木式”的策略搭建,做到了自动选股和权重分配。

    今天我们要把这些信息落实到模拟交易里。

    01 执行“按权重调仓”交易

    前面我们实现了order_by_mv, order_sell_mv这两个方法,是买入某支股票mv,或者卖出某支股票mv。

    投资组合管理中,更通用的操作是按总市值的仓位比例来分配的。

    比如股债平稳策略,股票70%,债券30%,无论你当前市值是多少,都在这个比例来操作,我们并不关心具体市值是多少。

    另外这个方法同样适用于“动态再平稳”,就是定时执行这个操作,把仓位恢复到一个即定的比例。

    在实际操盘过程中,这里涉及到先卖再买(当然存在卖不了或者滑点的情况),出于简化的考虑,我们可以先忽略——投资讲“模糊的正确”

    把当前持仓市值计算出来:

    # 持仓市值,不包括cash
    def _calc_total_holding_mv(self):
        total_mv = 0.0
        for s, mv in self.curr_holding.items():
            total_mv += mv
        return total_mv

    执行按比例调仓:

    # weights的格式 {'symbol1':0.2, 'symbol2':0.7} weights加和需要0<= x <=1,若小于1,则剩余部分按cash算。
    def adjust_weights(self, date, weights):
    
        # 计算当前的总市值
        total_mv = self._calc_total_holding_mv()
        total_mv += self.curr_cash
    
        old_pos = self.curr_holding.copy()
        self.curr_holding.clear()
        # 分配新权重
        for s, w in weights.items():
            self.curr_holding[s] = total_mv * w
    
        self.curr_cash = total_mv - self._calc_total_holding_mv()
        logger.info('发起权重调仓,日期:{}, 旧仓位:{},新仓位:{}'.format(date, old_pos, self.curr_holding))

    在引擎处直接调用即可:

    e = Engine(init_cash=100000, datafeed=feed)
    e.run(algo_list=[RunOnce(), SelectAll(), WeightEqually(), AdjustWeights()])
    logger.info('回测完成!')
    logger.info(e.acc.cache_portfolio_mv[-1])

    茅台就算不复权,从02年买入并持有,也是50多倍呀!

    02 使用qlib数据库

    我们需要对可转债全市场数据做回测,所以一个高性能的数据库是必要的。

    import qlib
    from qlib.constant import REG_CN
    from qlib.data import D
    from qlib.data.dataset.loader import QlibDataLoader
    
    
    class QlibDataFeed:
        def __init__(self):
            self.all_df = None
        def add_data(self, data_dir):
            qlib.init(provider_uri=data_dir, region=REG_CN)
    
        def get_all_df(self, start_date='2010-01-01'):
            if self.all_df:
                return self.all_df
    
            fields = ['$close',
                      '$close/Ref($close,1)-1',
                      '$close+ ($close/(100/$chg_price*$stk_close)-1)*100'
                      ]
            names = ['close','rate', 'double_low']
            data_loader_config = {
                "feature": (fields, names),
                # "label": (labels, label_names)
            }
            data_loader = QlibDataLoader(config=data_loader_config)
            instruments = D.instruments(market='all')
            df = data_loader.load(instruments=instruments, start_time=start_date)
            df = df['feature']
            df.reset_index(level=1, inplace=True)
            df.rename(columns={'instrument':'code'}, inplace=True)
            self.all_df = df
            return self.all_df

    从2010年开始所有转债的日频交易数据,以及它们的“双低值”一次计算出来,一共30万+条数据:

    03 SelectTopK算子

    由于我们是每天执行一次,所以不需要加时间算子。

    策略上需要把“买入并持有”的SelectAll改成SelectTopK即可,也就是选择“双低值”最小的前K个。

    我们的策略是不是很通用?

    Qlib框架有TopK的策略,我们可以借用过来,封装成我们自己的“算子”。

    class SelectTopK:
        def __init__(self, K=10, order_by='pred_score', ascending=False):
            self.K = K
            self.order_by = order_by
            self.ascending = ascending
    
        def __call__(self, context):
            logger.debug(self.__class__.__name__)
            df_bar = context['bar']
    
            # 前面还可以加规则,所以先看有没有选股过程selected
            if 'selected' in context.keys():
                if len(context['selected']) == 0:
                    # print('SelectTopK遇空仓,直接跳过')
                    # 若前面计算过selected,是空仓,那不需要排序,继续下一轮,但不退出——有可能要清仓操作。
                    return False
                to_select = []
                for s in context['selected']:
                    if s in df_bar.index:
                        to_select.append(s)
                #规则选股后,在子集里排序
                df_bar = df_bar.loc[to_select]
    
            df_bar.sort_values(by=self.order_by, ascending=self.ascending, inplace=True)  # 倒序
            symbols = df_bar.index[:self.K]
            logger.debug('选股结果:{}'.format(symbols))
            context['selected'] = symbols

    这里暂未考虑,已经持仓的就不动的,只调仓新增的和卖出的,这个下一步实现。

    算子使用非常简单:

    e = Engine(init_cash=100000, datafeed=feed)
    # e.run(algo_list=[RunOnce(),SelectAll(), WeightEqually(), AdjustWeights()])
    e.run(algo_list=[SelectTopK(ascending=True, K=5, order_by='double_low'), WeightEqually(), AdjustWeights()])

    相比“买入并持有”到“双低"策略我们仅改动一行代码

    一个基础的版本,没有仔细筛选转债背后公司的质量,也没有判断回售期之类的,作为一个benchmark,十年8倍!

    年化17.5%,不过最大回撤有点大,达到45.9%,所以这里还有较大的优化空间,今天主要是检验回测系统。

    从零开始实现一个量化回测系统(一)

    从零开始实现一个量化回测系统(二)

    关于投资的思考

    无论要不要以此为职业、事业,如同写作技能一样,每个人应该掌握一点投资知识。

    投资能力其实反映了对世界的认知

    除去必要的金融知识,比如你要交易转债,你肯定得知道转债的交易规则,以及背后的定价逻辑——这些基础知识都是可以很快学会的。

    投资的天花板,有点像比特币之于李笑来。

    他不是职业投资人,他讲投资也只讲定投。但他的认知结构和”常识“,在他初次见到BTC的时候,敏感的认为这是个机会,并抓住这个时间窗口。

    飞狐,科技公司CTO,用AI技术做量化投资;以投资视角观历史,解时事;专注个人成长与财富自由。

  • 相关阅读:
    【项目管理】Java OCR实现图片文字识别
    34 机器学习(二):数据准备|knn
    JAVA8新特性-Stream
    Makefile中诸多等号“:=, =, ?=和+=”的区别
    Scss--@extend--使用/实例
    AttributeError: module ‘dgl‘ has no attribute ‘batch_hetero‘
    深度学习——day23 class1-week2:二分分类与逻辑回归
    【Axure高保真原型】多图表动态切换
    聊聊使用@RefreshScope与nacos2整合踩到的坑
    C++函数自动生成规则
  • 原文地址:https://blog.csdn.net/weixin_38175458/article/details/126815763