持续行动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技术做量化投资;以投资视角观历史,解时事;专注个人成长与财富自由。