持续行动1期 41/100,“AI技术应用于量化投资研资”之可转债投资。
今天是中秋节和教师节,祝大家中秋快乐,教师节快乐!
今天开始一个主题——“从零实现一个量化回测系统”。
暂时不用考虑接入实盘,所以我会把订单、交易所,执行器等很多模拟交易的细节省掉,连事件驱动都省略,直接按bar来做循环。
这样代码看起来更加简洁,直奔核心而去,出了问题定位也相对容易。
一句话,该简化的简化,而且我们会兼容qlib的数据格式(其实等同于pandas的dataframe)。
01 账本account
我们就以自己做投资的视角来看这件回测,假设我们有一个账本叫Account。
Account有:
初始资金init_cash=10万。
维护当前还有多少现金余额:curr_cash。
当前持仓curr_holding = {"茅台": 20000.0} ,注意是 股票代码:市值。
cache_dates=['2010-01-01','2010-01-02'...]就是走过的bar的日期。
cache_porfilio_mv=[100000,,,,]每天的总市值(含现金)
class Account: def __init__(self, init_cash=100000.0): self.init_cash = init_cash self.curr_cash = self.init_cash self.curr_holding = defaultdict(float) # 当前持仓{symbol:mv} self.cache_dates = [] self.cache_portfolio_mv = []
总共这5个变量就够了。
Account每天要做的事情:
一、更新持仓 curr_holding的市值,按持仓更新每支股票的市值,这个好理解,市值mv*涨跌幅。
二、把总市值和日期cache起来。
# 要更新一次df_bar及市值,再进行交易。 def update_bar(self, date, df_bar): # 所有有持仓的,按收益率更新mv total_mv = 0.0 # 当前已经持仓中标的,使用收盘后的收益率更新 for s, mv in self.curr_holding.items(): rate = 0.0 # 这里不同市场,比如海外市场,可能不存在的,不存在变化率就是0.0, 即不变 if s in df_bar.index: symbol_bar = df_bar.loc[s] rate = symbol_bar['rate'] new_mv = mv * (1 + rate) self.curr_holding[s] = new_mv total_mv += new_mv self.cache_portfolio_mv.append(total_mv + self.curr_cash) self.cache_dates.append(date)
02 引擎engine
engine除了init_cash之外,需要一个datafeed。
datafeed里可以得到所有股票池的df数据,然后取出排重后的交易日期列表,对列表进行逐个bar的循环:
class Engine: def __init__(self, init_cash, datafeed): self.acc = Account(init_cash=init_cash) # 从feed得到df,并从index取出日期列表 self.df = datafeed.get_all_df() self.dates = self.df.index.unique() def run(self): for index, date in enumerate(self.dates): self.step(index, date) def step(self, index, date): df_bar = self.df.loc[date] if type(df_bar) is pd.Series: df_bar = df_bar.to_frame().T df_bar.index = df_bar['code'] df_bar.sort_index(ascending=True, inplace=True) self.acc.update_bar(date, df_bar)
03 支持交易:
想想,最简单的一个策略叫“买入并持有”,只有一个买入动作。
买入可以按“比例”买,也可以按“市值”买。从真实交易的角度,就是要买多少钱。
比例也可以换算成市值。
def order_buy_mv(self, instrument, mv): logger.debug('买入{},mv:{}'.format(instrument, mv)) if mv <= 0.0: logger.warning('mv需要>0') return if mv > self.curr_cash: logger.warning('资金不够,只能买:{}'.format(self.curr_cash)) mv = self.curr_cash self.curr_holding[instrument] += mv self.curr_cash -= mv
要买的时候,需要检查现金够不够,如果不够,有多少就买多少,有一些系统是如果不行,完全不买,这个看策略。
然后更新curr_holding持仓市值,现金减少相应的市值。
卖出的逻辑类似:
def order_sell_mv(self, instrument, mv): logger.debug('卖出{},mv:{}'.format(instrument, mv)) if mv <= 0.0: logger.warning('mv需要>0') return mv_holding = self.curr_holding.get(instrument, 0.0) if mv_holding == 0: logger.warning('当前未持仓,无法卖出') return else: if mv_holding < mv: logger.warning('实际持仓:{},不够:{}'.format(mv_holding, mv)) mv = mv_holding self.curr_holding[instrument] -= mv self.curr_cash += mv
卖出的市值必须为正,同时如果未持仓,无法卖出。
如果仓位不够,只卖出当前持有的(也是清仓了)
然后更新curr_holding和curr_cash即可。
04 投资的可行性及意义
要把一件事当成“长期”事业来做,我们要问两个问题,
一是这件事行不行,如果逻辑上走通的概率低,但不值得当长线的事情来做;
二是这件事的意义,能不能给我们带来满足感,人还是讲自我实现的,尤其是当遇到困难的时候。
先说可行性,投资能不能当长线事情做。
也许你会说,有很多人把投资当成事业或者职业啊。比如职业股评师,基金经理,私募基金管理人等等,甚至很多失业的大龄中年都“沦”为职业股民。
但这么说我们心里还是没谱的,毕竟投资是一个概率游戏,西蒙斯花了十年时间才实现盈利,普通人能撑多久呢?
真正让我相信这条路可以当作B计划,因为它可以同时走三条路,一是互联网产品平台模式,比如集思录,理杏仁,乌龟量化等;二是IP模式,如螺丝钉,力哥,甚至做可转债的阿秋;三才是投资本身。
后者与前两者,如同淘金和送水。淘金具备不确定性,但送水具备稳定性。送水的同时自己同时可以研究淘金,一举多得。
三者叠加起来,让投资这条路的方向变得清晰,不用再担心打井有没有水的问题。
另外一个问题,是关于意义。
尽管前途光明,但道路是曲折的。
当道理曲折时,能不能坚持下去,除了内心相信能不能赢的必胜信心之外,还有就是对于这件事的认同。
金融有没有意义,做投资如果纯粹是为了赚钱,客观讲这条路并不好走,冒的险并不少。
也许我们会说金融是为了资源跨时空的优化配置,投机有利于市场价格发现,这此理由高大上但不接地气。
西蒙斯从知名数学家进入资本市场,他的信念就是像解决数学难题一样去看待资本市场的“预测”,之于他证明定理无异。都是为了发现这个世界未知的规律,“顺便”实现了财富自由,带给自己更多的精神自由。
这一点给我不少启发。
其实,我们不是刻意做投资,我们是为了认知这个世界,社会,你如何得到相应的反馈,投资和资本市场是一个好的角度。否则你就像出租车司机般,大谈特谈对这个世界的偏见,除了夸夸其谈,并不能带来任何效果。
以投资的视角看待历史,现实,未来,去探索这个世界,“顺便”把投资做好。
这就是一个“系统”,所以,不必以短期投资效果作为衡量标准,而是持续改进我们的认知系统。
小结:
你仔细看看,一个回测引擎的核心其实就干这几件事,你就以你交易账本的视角来看就好,别管什么order, event。
就是买什么,买多少,卖什么,卖多少,每天根据市场涨跌更新一下市值。
只是后续由“量化策略”来决定 买或卖以及仓位的动作。
然后还有一个部分就是回测完成之后,做分析,比如风险收益特性,各种比率以及可视化等等。
后面的篇幅再展开。
投资是为了认知世界,投资可以做为一项长线的事业来积累和经营。
飞狐,科技公司CTO,用AI技术做量化投资;以投资视角观历史,解时事;专注个人成长与财富自由。