Loading... > 自己在写量化框架的时候, 对于课题<<回测与投研是自己写还是避免造轮子集成现有的成熟框架>>我前前后后研究了许多, 总结来看, 把时间成本当成主要考虑因素的话, 如果你达到了一定的编程水平就没必要自己再去实现了, 一个开源成熟的如backtarder这样的框架(速度快, 运行稳定, 扩展性很强, 功能最丰富), 不会出问题, 想怎么改自己可以在源码基础上就怎么改, 轻松的集成到自己的量化交易框架中, 而且bt经过了长时间的打磨, 非常稳定, 哪怕是踩到坑自己也能优化掉; > > 后续 -> 我打算写一个系列的文章用来记录集成bt时所遇到的小问题与采坑, 分享出来留给需要的人; > 本次分享内容: > 我在一开始决定自己写一个量化框架时就已经做好了定位(异步快速, 分布式, 可扩展, 可实盘交易, 海量数据处理), 所以数据库的选型上我使用了ck, 那么在bt集成时就需要参考源码的datafeed实现来自己实现一个clickhousefeed类作为数据源, 达到最快的数据加载与策略指标的计算; 本次内容分别是引入ck作为数据源时踩的坑和完整的测试代码, 废话不多说直接上代码: ## 采坑(self.fromdate的tzinfo信息为None) * 如果从数据库取出的数据中, datetime对象自带tzinfo信息, 会导致preload后的数据self.lines.datetime[0]与bt框架自己的执行main代码中feed对象实例化参数fromdate(该dt带tzinfo信息)不对应,从而时间列错乱, 无法加载该条数据; * 进入preload(), 执行feed类 -> self.load()方法, 该方法内部是加载数据的主要逻辑  * 由于dt和self.fromdate的tzinfo信息不一致(None和Asia/Shanghai)导致date2num后的数值错乱, 本应该是dt>self.fromdate, 所以这里continue了本次循环, 导致本应该加载的数据因为系统判断时间不在你规定的回测区间中而被pass掉;  * 解决: * 方案1: 为Feed实例化时的参数fromdate添加对应的tzinfo 简单, 不贴图了; * 方案2: 将Clickhousefeed类(自己写的数据源) 从数据库取出的timestamp数据的tzinfo设置为None  ## 数据源: ClickhouseFeed类代码 ```python # author: Michael # email: yangowen@126.com import datetime from backtrader import TimeFrame from backtrader import date2num, num2date from backtrader.feed import DataBase class ClickhouseFeed(DataBase): params = ( # object of class clickhouse_driver.Client ('client', None), # Note: # set fields order as u want, cause the field has been ordered before selecting from clickhouse # 1. default order t0,o1,h2,l3,c4,v5,op-1 # 2. < 0 column not present # 3. >=0 numeric index to the column after selected from clickhouse ('datetime', 0), ('open', 1), ('high', 2), ('low', 3), ('close', 4), ('volume', 5), ('openinterest', -1), ) def __init__(self): super(ClickhouseFeed, self).__init__() def get_client(self): self.p.client = Client(host="localhost", port=19090, user="default", compression="lz4", settings={"use_numpy": False, 'max_block_size': 100000}) return self.p.client def start(self): self.p.client: Client if not self.p.client: self.get_client() table_name = self.p.dataname # ck: short name for clickhouse self.ckfield = list(filter( lambda x: getattr(self.params, x) is not None and getattr(self.params, x) >= 0, self.getlinealiases())) def sort_key(field): return getattr(self.params, field) self.ckfield.sort(key=sort_key) # Note: 'timestamp' key field cannot be renamed in ck, # deal with it here by replacing with 'datetime', u may not do that in ur case # ['timestamp', 'open', 'high', 'low', 'close', 'volume', 'turnover', 'b1_p'...] self.ckfield[getattr(self.params, 'datetime')] = 'timestamp' self.sql = f"select {','.join(self.ckfield)} " \ f"from {table_name} " \ f"where symbol like 'sh%' and " \ f"timestamp between '{self.p.fromdate}' and '{self.p.todate}' " self.rows_gen = self.p.client.execute_iter(self.sql) def _load(self): if not self.rows_gen: return False try: row = next(self.rows_gen) except StopIteration: return False # Note: 取出来的datetime时间戳不能带有tzinfo信息(如果带有则需要删除), # 否则date2num后时间不正确 self.lines.datetime[0] = date2num(row[self.p.datetime].replace(tzinfo=None)) for ck_field in (x for x in self.ckfield if x != 'timestamp'): line_index = getattr(self.params, ck_field) line = getattr(self.lines, ck_field) line[0] = row[line_index] return True def stop(self): print('stop') self.p.client: Client self.p.client.disconnect() ``` © 允许规范转载