深入了解zipline
由于机缘巧合的机会,我离开了从事多年的互联网/移动互联网行业,进入了金融领域,就职与一家金融IT公司,虽然我毕业后第一份工作就在雅虎中国的财经频道从事开发工作(雅虎中国被雅虎收购之后,我就转到其它的部门),但是之前对于金融领域还是涉猎不深,这次加入后认真的补了这方面的知识,为了更加深入的了解量化相关的技术领域的问题,我准备从一些优秀的开源量化框架入手,从我熟悉的代码角度了解这个行业的业务和技术细节。
经过简单的调研,从交易频度方面,现有的量化框架大概有两类:
- 高频量化交易框架 (大部分是event base的)
- 中低频量化交易框架 (大部分是bar base的)
高频和中低频现在在实际的生产领域各有侧重,此外中低频框架由于逻辑相对简单,在很多教学和研究领域使用也较多,比如我当下在研究的zipline
。 除此之外,以后如果有时间的话还想研究一下apama
,但由于apama
不是开源软件,考虑只了解一下它的机制之后看看能不能基于Apache Spark Streaming
或Storm
来实现。好像说的有点远了,那我们回来继续来谈zipilne
。
zipline是美国Quantopian 公司开源的量化交易回测引擎,它使用Python
语言开发,部分代码使用cython
融合了部分c语言代码。Quantopian
在它的网站上的回测系统就是基于zipline
的,经过生产环境的长期使用,已经比完善,并且在持续的改进中。
目前我研究的对象基于当前zipline
的最新版本是 1.0.2 ,由于zipline
的版本更新较快,后面可能会有些变化。请知晓。
zipline
默认是不支持国内市场的股票数据的,我的研究过程是讲zipline
引入国内股票市场的数据,时期可以进行国内市场的回测,在这个过程中进一步了解整个框架。
这篇文章不是一篇入门文章,所以想要了解zipline
的基本使用方法,请参考zipline
和quantopian
的官方文档。
http://www.zipline.io/beginner-tutorial.html
zipline的目录结构
下面列出了zipline主要的目录和文件结构和它的说明
├── ci - 持续集成相关
├── conda - 生成conda 包先关
├── docs - 文档
│ ├── notebooks - notebook代码
│ └── source - 教程和what’s new
├── etc - 依赖配置和一些 hook shell 脚本
├── tests - 测试代码
├── zipline - 代码主目录
│ ├── __init__.py - 集中引入包内容
│ ├── __main__.py - 主程序入口
│ ├── _protocol.pyx - current, history, can_trade 之类的一些数据操作接口的实现
│ ├── _version.py - 版本管理相关的
│ ├── algorithm.py - 策略算法的主逻辑抽象,算法最后会被实例化为TradingAlgorithm的实例或者继承它, 并且里面进行了主要的api的定义,zipine的 cli会调用它的run方法启动回测
│ ├── api.py 常用api
│ ├── api.pyi 常用api说明
│ ├── assets - 资产类抽象 里面封装了常用的资产如股票Equity,期货Funtrue, 作为Asset的子类,并且封装了其数据库操作(这里是sqlite)
│ ├── data - 数据相关,所有的数据操作封装为dataportal
│ │ ├── __init__.py
│ │ ├── _adjustments.pyx - 除权除息等信息的读取
│ │ ├── _equities.pyx - 从bcolz里面获取行情的索引的抽象
│ │ ├── _minute_bar_internal.pyx - 分钟bar相关的索引
│ │ ├── bar_reader.py - BarReader接口定义
│ │ ├── benchmarks.py - 从雅虎获取基准数据
│ │ ├── bundles - 官方的提供的data bundle
│ │ ├── continuous_future_reader.py
│ │ ├── data_portal.py DataPortal定义,整合了所有的reader,writer,等,是biplane获取数据的入口,提供reader,writer数据的简单高层封装
│ │ ├── dispatch_bar_reader.py - 结合trading calendar 读取asset的bar信息
│ │ ├── history_loader.py - asset 历史信息的获取, 包括附加复权信息
│ │ ├── loader.py - loader 封装了基准信息和国债收益率曲线
│ │ ├── minute_bars.py - 分钟线reader/writer相关的抽象
│ │ ├── resample.py - 把分钟线数据resample为日线数据
│ │ ├── session_bars.py - SessionBarReader
│ │ ├── treasuries.py - 国债收益率曲线
│ │ ├── treasuries_can.py - 加拿大国债收益率曲线
│ │ └── us_equity_pricing.py - 主要是针对Equity的日线读取,adjustment数据读取,
│ ├── dispatch.py - 分发逻辑
│ ├── errors.py - 异常的抽象
│ ├── examples - 一些例子
│ ├── finance - 主要抽象了交易和财务相关的逻辑,这些接口大多会出现在zipline或者quantopian的代码策略代码里,可以进行import
│ │ ├── __init__.py
│ │ ├── asset_restrictions.py - 资产交易限制
│ │ ├── blotter.py - 账号?
│ │ ├── cancel_policy.py - 取消策略
│ │ ├── commission.py - 佣金
│ │ ├── constants.py - 一些常亮定义
│ │ ├── controls.py - 分控相关
│ │ ├── execution.py - 订单类型
│ │ ├── order.py - 订单逻辑
│ │ ├── performance - 收益
│ │ ├── risk - 风险相关
│ │ ├── slippage.py - 滑点
│ │ ├── trading.py - TradingEnvironment, SimulationParameters的抽象,如果使用自己的loader, TradingCalendar 则需要自己初始化这个对象
│ │ └── transaction.py - Transaction - 交易的抽象
│ ├── gens - 应该是集合了大部分的generator , 主要是回测过程的generator
│ │ └── tradesimulation.py - 回测主要过程的generator
│ ├── lib - 一些主要用的的数据结构和算法
│ ├── pipeline - pipeline相关的逻辑
│ ├── protocol.py - Order Account, Portfolio, Position等的抽象
│ ├── resources - etc相关的一些资源
│ ├── sources - 基准数据源等
│ ├── test_algorithms.py - 测试策略...
│ ├── testing - 测试
│ ├── utils - 一些工具类, 其中 run_algo.py, tradingcalendar.py 相关的需要重点关注下
zipline的命令行入口
先上个图
几种使用方式
首先zipline
主要有三种启动的方式:
- 使用
zipline
命令行工具 - 使用
jupyter notebook
的zipline
集成magic
- 直接自己组装和调用
TradingAlgorithm
我们主要以zipline
命令行工具为研究对象,来看一下它的结构,其它情况类似
zipline 命令行
首先从setup.py
中可以看到代码的entry_point
entry_points={ 'console_scripts': [ 'zipline = zipline.__main__:main', ], .... }
研究 __main__.py
文件,发现其石宏了 click
(http://click.pocoo.org/)包来做命令行接口的路由,其中可以分为4个子命令
- run : 负责执行策略
- ingest : 负责拉取策略所需的数据包(bundle)
- bundles : 查看所有数据包
- clean: 清除数据包
后两个命令都比较简单,前两个里面的逻辑相对复杂一些。
其中 在main
的入口中,除了者几个子命令,它还是用了load_extensions
来载入所有的扩展,-extension
可以指定扩展的列表。
run
命令在一些初始化和装载过程之后,会调用TradingAlgorithm的run方法。
ingest
命令会调用data.bundles.core
的ingest
函数来进行拉取。
zipline的整体架构
先看图
在这里可以看出,zipline
由下面几个主要的部分构成
名称 | 说明 |
---|---|
TradingAlgorithm | 量化策略的抽象,既可以通过初始化传入构造上参数的方式,也可以通过继承的方式构造,其中zipline 命令行主要的运行入口逻辑 run 方法也在这个类中 |
TradingCalendar | 交易日历的抽象,这个类非常重要,无论是在构建数据的过程还是运行的过程,都可以用到 |
DataPortal | 数据中心的抽象,可以通过这个入口获取很多不同类型的数据 |
AlgorithmSimulator | 使用generator 的方式,表述了策略运行过程的主循环。如果说TradingAlgorithm 更像是代表了策略本身,那么AlgorithmSimulator 更像是策略的执行器,尤其要关注的是他的transform 方法 |
TradingEnvirioment | 构造运行环境,主要是benchmark 和国债利率曲线 等信息,对于美国的市场,这个类基本上不太需要关注,但是对于国内的市场,我么需要构建自己的TradingEnvironment |
zipline应用在国内市场的限制
zipline
可以很好的支持美国股票市场的应用,但是却无法直接使用在国内市场,主要有如下几个方面的限制
数据方面
zipline
自带的几个bundle
都无法支持国内市场,其中quandl
只包含美国的市场数据, yahoo
的经过配置勉强可以下载到国内的股票信息。但是由于雅虎的国内行情的数据质量不敢恭维,历史数据经常会有缺失,所以感觉也不是特别靠谱的选择
交易日历 ( TradingCalendar )
交易日历是zipline系统里非常重要的部分,通过上一文章的稿子可以看到,很多其他的组件都和它有关联,给其他组件提供时间维度的索引,不管是ingest data bundle 还是运行算法,如果日历不匹配的话,那回测的结果肯定是不正确的,zipline默认使用的是美国纽交所的交易日历 (NYSE),这个显然是不能匹配国内股市的,除此之外,它还提供了 CME (芝加哥商品交易所 ) ,ICE(洲际交易所),us_futures (美国期货),等不同交易市场的交易日历,同样也不太适合国内的市场。
基准数据和国债利率曲线
基准数据
在回测的时候,如果没有特别指定,zipline使用的美国的标普500
作为基准,显然也无法适用于国内的市场,我们可以通过两种方式来改变默认的行为
- 一种是在初始化
TradingEnvironment
的时候使用bm_symbol
参数来指定 - 另外一种是在策略代码里调用
set_benchmark
api 来设置
注意,两种方法在数据源的获取上有所不同 , TradingEnvironment
的方式通过雅虎网站上的csv数据源获取的,而 set_benchmark
的方式是直接使用你本地的data bundle数据。
无论哪种方式,现有的zipline平台都不是特别合适,其中TradingEnvironment
的方式由于是用yahoo上获取的数据源,经测试,雅虎的数据源的数据在国内的指数上,如上证综指
部分时间点的数据有缺失,由于zipline在运行时对于数据的要求比较严格,会导致运行时抛出异常失效。对于set_benchmark
的模式,由于它默认是不提供国内的行情数据的,所以也无法使用。
所以如果要讲zipline应用到国内的市场,需要做一些定制的开发。
所以后面我们会分别从TradingCalendar
, 数据
方面分别研究一下。
了解TradingCalendar
本文大部分内容都是我在研究TradingCalendar
时所记得笔记,这个图是我后期整理的,
TraderCalendar的解释:
一个TraderCalendar代表一个单独交易市场的时间信息,时间信息由两部分组成 sessions 和 opens/closes
session代表一个连续的分钟时间段, 使用UTC的午夜时间来定义, 注意它代表的不是一个时间点,使用UTC午夜时间只是为了方便
里面有很多函数是处理minute 和 session转换以及获取开收盘时间时间
对应文档在
http://www.zipline.io/appendix.html#trading-calendar-api
http://www.zipline.io/_modules/zipline/utils/calendars/trading_calendar.html
系统自带的calendars 可以通过``zipline.utils.calendars.get_calendar`获取,如:
from zipline.utils.calendars import get_calendar c = get_calendar("NASDAQ")
获取了纳斯达克的calendar, 注意,这里的calendar是存在别名的,不同的交易所可能对应同样的别名,只要它们的交易时间相同,如 NASDAQ
交易所的别名就和NYSE
相同, 这部分定义可以查看zipline.utils.calendars.calendar_utils
模块下看到定义
_default_calendar_factories = { 'NYSE': NYSEExchangeCalendar, 'CME': CMEExchangeCalendar, 'ICE': ICEExchangeCalendar, 'CFE': CFEExchangeCalendar, 'BMF': BMFExchangeCalendar, 'LSE': LSEExchangeCalendar, 'TSX': TSXExchangeCalendar, 'us_futures': QuantopianUSFuturesCalendar, } _default_calendar_aliases = { 'NASDAQ': 'NYSE', 'BATS': 'NYSE', 'CBOT': 'CME', 'COMEX': 'CME', 'NYMEX': 'CME', 'ICEUS': 'ICE', 'NYFE': 'ICE', }
需要注意的是:这里所有的开收盘时间都被转化为UTC时间,但是在函数里面传入的大多数时间pd.Timestamp,都会去掉时区信息,并设置为utc时间,这里不是用 astimezone转化为utc时间,而是直接变为utc时区的时间,具体的时间不变,如
Timestamp('2016-09-27 09:30:00+0800', tz='Asia/Shanghai') 会直接变为 Timestamp('2016-09-27 09:30:00+0800', tz=‘UTC')
我们举个使用TradingCalendar的例子
我们看一下美国的2016年9月27日开收盘信息
In [115]: c.open_and_close_for_session(pd.Timestamp('2016-09-27 00:00', tz=pytz.UTC)) Out[115]: (Timestamp('2016-09-27 13:31:00+0000', tz='UTC'), Timestamp('2016-09-27 20:00:00+0000', tz='UTC'))
这里的 13:31:00 和 20:00:00 都是UTC时间,对应美国的时间为
In [118]: [d.astimezone(pytz.timezone('America/New_York')) for d in c.open_and_close_for_session(pd.Timestamp('2016-09-27 00:00', tz=pytz.UTC))] Out[118]: [Timestamp('2016-09-27 09:31:00-0400', tz='America/New_York'), Timestamp('2016-09-27 16:00:00-0400', tz='America/New_York')]
以此类推,国内的开收盘时间对应的UTC时间应该为:
In [110]: shanghaiopen = pd.Timestamp('2016-09-27 09:30:00', tz=pytz.timezone('Asia/Shanghai')); In [111]: shanghaiopen.astimezone(tz=pytz.UTC) Out[111]: Timestamp('2016-09-27 01:30:00+0000', tz='UTC') In [112]: shanghaiclose = pd.Timestamp('2016-09-27 15:00:00', tz=pytz.timezone('Asia/Shanghai')); In [113]: shanghaiclose.astimezone(tz=pytz.UTC) Out[113]: Timestamp('2016-09-27 07:00:00+0000', tz='UTC')
谢天谢地,还是在 UTC的同一天的,估计类似欧洲一些国家的股市就悬了
regular_holidays
对应的 AbstractHolidayCalendar
的实现保存着节假日的信息
In [120]: c.regular_holidays Out[120]: <zipline.utils.calendars.trading_calendar.HolidayCalendar at 0x118c75588> In [121]: c.regular_holidays.holidays Out[121]: <bound method HolidayCalendar.holidays of <zipline.utils.calendars.trading_calendar.HolidayCalendar object at 0x118c954e0>> In [122]: c.regular_holidays.holidays()
在美国有一个special_close
, special_open
的概念,在国内好像没有,好像是在指定的日期会提早收盘,这个信息通过一个数组保存,里面可能有多个提早收盘的时间点和收盘的列表。
有两种不同的表示, 一种是可以用常规的holiday对象表示,还有一种则直接写明日期
(datetime.time(13, 0), <zipline.utils.calendars.trading_calendar.HolidayCalendar at 0x118cecb00>), (datetime.time(14, 0), <zipline.utils.calendars.trading_calendar.HolidayCalendar at 0x118cec8d0>)] In [131]: c.special_closes[0][1].holidays() Out[131]: DatetimeIndex(['1993-11-26', '1994-11-25', '1995-07-03', '1995-11-24', '1996-07-05', '1996-11-29', '1996-12-24', '1997-07-03', '1997-11-28', '1997-12-24', '1998-11-27', '1998-12-24', '1999-11-26', '2000-07-03', '2000-11-24', '2001-07-03', '2001-11-23', '2001-12-24', '2002-07-05', '2002-11-29', '2002-12-24', '2003-07-03', '2003-11-28', '2003-12-24', '2004-11-26', '2005-11-25', '2006-07-03', '2006-11-24', '2007-07-03', '2007-11-23', '2007-12-24', '2008-07-03', '2008-11-28', '2008-12-24', '2009-11-27', '2009-12-24', '2010-11-26', '2011-11-25', '2012-07-03', '2012-11-23', '2012-12-24', '2013-11-29', '2013-12-24', '2014-07-03', '2014-11-28', '2014-12-24', '2015-11-27', '2015-12-24', '2016-11-25', '2017-07-03', '2017-11-24', '2018-07-03', '2018-11-23', '2018-12-24', '2019-07-05', '2019-11-29', '2019-12-24', '2020-11-27', '2020-12-24', '2021-11-26', '2022-11-25', '2023-07-03', '2023-11-24', '2024-07-05', '2024-11-29', '2024-12-24', '2025-07-03', '2025-11-28', '2025-12-24', '2026-11-27', '2026-12-24', '2027-11-26', '2028-07-03', '2028-11-24', '2029-07-03', '2029-11-23', '2029-12-24', '2030-07-05', '2030-11-29', '2030-12-24'], dtype='datetime64[ns]', freq=None) In [133]: c.special_closes_adhoc Out[133]: [(datetime.time(13, 0), ['1997-12-26', '1999-12-31', '2003-12-26', '2013-07-03'])]
如果是自己实现的TradingCalendar
,可以继承 TradingCalendar
类,然后通过register_calendar
方法来注册。
这里有一个我写的例子
由于国内股市的节假日时间信息无法通过类似美国的有明确的假期规则,所以这里使用了它的adhoc_holidays
的方式,也就是类似枚举的方式获得,这里我通过 cn_stock_holidays
(https://github.com/rainx/cn_stock_holidays) 项目维护了一个节假日列表,历史节假日我是根据历史指数的非周六日的无开盘记录来获取的,未来的可预期的节假日我也会保持更新,如果你在其他项目里用到该数据,可以直接安装使用。
pip install git+https://github.com/rainx/cn_stock_holidays.git
具体的使用说明,可以参考github上的文档
具体的使用方式为
from cn_stock_holidays.zipline.exchange_calendar_shsz import SHSZExchangeCalendar register_calendar("SHSZ", SHSZExchangeCalendar(), force=True) c=get_calendar("SHSZ")
或者你也可以直接使用我这边已经注册的单例
from cn_stock_holidays.zipline.default_calendar import shsz_calendar
直接获取到 shsz_calendar的实例。
除了休市时间外,对于国内股市还有一点区别是日内的午休时间,这个在国外是没有的,这个应该对分钟数据有影响,所以在SHSZExchangeCalendar
的实现里,我重写了 all_minutes
使其支持国内的lunch break
国内股票行情信息
Databundle
zipline 缺省提供了一些行情的data bundle , 可以通过 zipline bundles
查看
其中 quandl
数据源是从 https://www.quandl.com/ 网站的WIKI dataset获取数据的,不过通过该api数据较慢,因为逐批获取之后还要处理后才放到本地。quantopian-quandl
一份备份数据,相当于将处理后的数据打包之后提供,下载下来解压到 ~/.zipline/data
目录,所以相对较快,如果有研究美国股市的量化交易,可以使用这些数据源,还有yahoo数据源,它的好处是可以自己定制要抓取的数据集,如果你只需要配置好然后通过 ipline.data.bundles.register
进行注册即可。
如果列位想引入国内的数据源进行回测,那么恐怕要下点功夫啦,下面可以给出如下几种方案:
- 不ingest数据源,直接在构建
TradingAlgorithm
的时候引入如从csv
或者数据库
里读取出来的pd.Dataframe
信息作为DataPortal
的datasource
(当然最终会转化为pd.Panel
) - 自己定制
data bundle
,然后ingest
如果你要测试的数据集比较小,股票数量也比较少,那么第一种方案是比较方便的,如果需要大量的数据,还是自己写data bundle 比较方便(并且貌似使用Dataframe也很难处理有 split或者dividend的股票)
那么如何编写一个新的bundle的扩展呢?
其实比较简单,自己实现一个ingest
函数即可
该ingest
函数的参数如下:
ingest(environ, asset_db_writer, minute_bar_writer, daily_bar_writer, adjustment_writer, calendar, start_session, end_session, cache, show_progress, output_dir)
其中这个函数是被environ
回调的,所以参数列表无法自己指定,下面简单介绍一下几个参数的作用
参数 | 作用 |
---|---|
environ | 代表环境变量的一映射,如果你需要一些额外的参数引入,可以在这里通过环境变量指定,如quandl 的API key |
asset_db_writer | AssetDBWriter 的实例,通过它的write函数可以把一个证券(如股票)的基础信息,主要是码表,名称,起止日期等信息写入到数据库中,并且为每个证券分配一个sid作为唯一标识,这个sid在系统的其它地方也会成为股票的主要索引方式。默认保存在sqlite数据库中 |
daily_bar_writer | 写入每日的行情信息 BcolzDailyBarWriter 的实例,通过调用write方法写入股票的开高低收和成交量等信息(OHLCV),这里的信息也需要使用sid与基本信息进行关联。默认使用bcolz的格式保存 |
minute_bar_writer | 写入每分钟行情的... |
adjustment_writer | 处理一些拆分,合并,送股,分红等事件的信息。默认使用sqlite数据库保存 |
calendar | 你当前使用的交易日历 ,数据的获取是以交易日历作为索引的,也就是说,如果你的交易日历里那一天存在,可是你无法读取行情数据,很有可能会出现错误,所以calendar 和你的行情信息 的匹配是很关键的 |
start_session/end_session | 获取数据的起止日期 |
cache | dataframe_cache 的实例,你可以使用它来缓存在获取过程中的原始信息,在多次ingest的时候起到加速的作用 |
show_progress | 一个布尔值,是否显示ingest的过程,如果你的获取数据时间较长,可以判断show_progress变量来显示进度。 |
output_dir | data bundle的输出目录,如果你的data bundle 是类似 quantopian-quandl 这种通过下载远程已经写好的数据源,并且解压到本地的,可以直接使用这个变量获取最终解压目录 |
一般来说,获取一个行情的数据源,主要需要三方面的信息
- 使用
asset_db_writer
获取基础信息 - 使用
daily_bar_writer
/minute_bar_writer
写入行情信息 - 使用
adjustment_writer
写入split, dividend信息。
话说这里坑不少,我建议多看看官方的例子,大部分都要讲数据处理为pd.Dataframe的结构再进行的。另外我自己也写了一些简单的demo,可以参考:
https://github.com/rainx/zipline_cn_databundle
里面代码比较凌乱,有很多无用代码,主要参考一下squant_source
模块
https://github.com/rainx/zipline_cn_databundle/blob/master/zipline_cn_databundle/squant_source.py
不过代码里用到了我自己写的一个squant包,是一个私有数据包,因为应用了很多内部数据,不便公开,所以大家恐怕无法直接使用。主要是asset和adjuestment的部分,对于行情,我使用的是通达信的客户端的本地数据(木有windows , 从别的机器拷贝的 T_T),大家应该可以直接使用,参考里面的TdxReader
(https://github.com/rainx/zipline_cn_databundle/blob/master/zipline_cn_databundle/tdx/reader.py)
Bcolz
zipline的本地行情是写入到bcolz
的格式的,它是底层使用Blosc
库的基于列的数据库,至于为什么使用基于列的数据库,应该是与行情信息的特质有关,因为行情信息可以通过TradingCalendar和Bcolz的元信息进行索引,并且以时间顺序排列,而且是相同的类型,所以非常适合类似数组结构的存储方式,加之以Blosc的变态级别的压缩解压算法(使用CPU L1/L2缓存进行压缩/解压,平均速度超过了memcpy
调用),所以对时间和空间上都可以做到比较优化的状态。
内部的索引结构大概抽象为:
基准信息和国债收益率曲线
基准信息和国债收益率曲线用于计算Alpha,Beta,Shape, Sortino等风险指标的时候使用。默认情况下zipline使用标普500
作为基准数据,并且使用美国的国债收益率曲线,这显然是有问题的,基于这个信息计算出来的数值也会存在较大偏差,所以我们需要引入国内的数据。
基准信息(benchmark)
之前我们提到,切换benchmark信息有两种方式:
- 一种是在初始化
TradingEnvironment
的时候使用bm_symbol
参数来指定 - 另外一种是在策略代码里调用
set_benchmark
api 来设置
对于方式2,如果你需要引入某个指数的数据作为基准, 需要我们再ingest信息的时候讲指数的信息也作为行情导入进来,如果是方式1,那么需要注意的是,zipline默认的loader只能通过雅虎网站上下载指数的行情信息,而我们之前也提到过,雅虎的行情并不靠谱 (虽然国内的行情有一阵子是我再Yahoo China的时候维护的 T_T),所以如果需要用这种方式导入,需要实现我们自己的 loader并在TradingEnvionment
初始化的时候传递进去。
下面就是一个例子
https://github.com/rainx/zipline_cn_databundle/blob/master/zipline_cn_databundle/loader.py
对于可以使用的指数信息,我维护了一个项目来生成这部分数据,这个项目会保持每个交易日晚上更新
https://github.com/rainx/cn_index_benchmark_for_zipline
国债收益率曲线
国库收益率曲线也可以通过loader进行定制,具体可以参考之前loader的代码,我这里通过从 中债信息网获取数据,并进行整合,由于网站上的数据是通过excel的文件提供的,并且格式和最终zipline所需要的格式并不相同,我这里做了一个解析其内容的包:
https://github.com/rainx/cn_treasury_curve
关于数据的推导和整理过程,可以参考**这个jupyter notebook文档**
这部分内容,也同样通过上面的loader整合到zipline中。
整合在一起
接下来,我们就把我们之前做的整合在一起,看看如何进行国内股票的回测 (注意,下面的软件包中部分依赖的内容并未发布到github上,下面的说明只是示意,如果要让系统运行起来,需要自行开发部分代码,主要是数据获取部分的代码)
首先,安装cn-stock-holidays
cn-treasury-curve
zipline-cn-databundle
等软件包
ingest 过程
配置 ~/.zipline/extension.py
注册bundle
from zipline.data.bundles import register from zipline_cn_databundle.squant_source import squant_bundle import pandas as pd from cn_stock_holidays.zipline.default_calendar import shsz_calendar register('cn_squant', squant_bundle, 'SHSZ', pd.Timestamp('2008-12-19', tz='utc'), pd.Timestamp('2016-10-31', tz='utc') )
由于我的程序里面使用了通达信的数据,在通达信的客户端下载数据,复制通达信目录下vipdoc
目录下 sh
和 sz
目录到某个位置,并在环境变量里设置,adjustment和assets meta 信息,由于使用了公司的私有数据库,无法对外公开,所以大家自行解决吧。
运行ingest
zipline ingest cn_squant
运行策略
由于zipline默认的运行过程没有办法支持自定义的loader ,所以这里我们自己来实现 TradingAlgorithm
的run
过程。
from zipline.data.bundles import register from zipline_cn_databundle.squant_source import squant_bundle import pandas as pd import os from zipline.api import ( schedule_function, symbol, order_target_percent, date_rules, record ) import re from zipline.algorithm import TradingAlgorithm from zipline.finance.trading import TradingEnvironment from zipline.utils.calendars import get_calendar, register_calendar from zipline.finance import trading from zipline.utils.factory import create_simulation_parameters from zipline.data.bundles.core import load from zipline.data.data_portal import DataPortal from zipline_cn_databundle.loader import load_market_data # register SHSZ from cn_stock_holidays.zipline.default_calendar import shsz_calendar bundle = 'cn_squant' start_session_str = '2011-01-05' register( bundle, squant_bundle, "SHSZ", pd.Timestamp(start_session_str, tz='utc'), pd.Timestamp('2016-10-31', tz='utc') ) bundle_data = load( bundle, os.environ, None, ) prefix, connstr = re.split( r'sqlite:///', str(bundle_data.asset_finder.engine.url), maxsplit=1, ) env = trading.environment = TradingEnvironment(asset_db_path=connstr, trading_calendar=shsz_calendar, bm_symbol='000001.SS', load=load_market_data) first_trading_day = \ bundle_data.equity_minute_bar_reader.first_trading_day data = DataPortal( env.asset_finder, shsz_calendar, first_trading_day=first_trading_day, equity_minute_reader=bundle_data.equity_minute_bar_reader, equity_daily_reader=bundle_data.equity_daily_bar_reader, adjustment_reader=bundle_data.adjustment_reader, ) def initialize(context): schedule_function(handle_daily_data, date_rules.every_day()) def handle_daily_data(context, data): sym = symbol('000001.SZ') # 计算均线 short_mavg = data.history(sym, 'close', 5, '1d').mean() long_mavg = data.history(sym, 'close', 10, '1d').mean() # 交易逻辑 if short_mavg > long_mavg: # 满仓 order_target_percent(sym, 1) elif short_mavg < long_mavg: # 清仓 order_target_percent(sym, 0) # Save values for later inspection record(价格=data.current(sym, 'price'), short_mavg=short_mavg, long_mavg=long_mavg) if __name__ == '__main__': sim_params = create_simulation_parameters( start=pd.to_datetime(start_session_str + " 00:00:00").tz_localize("Asia/Shanghai"), end=pd.to_datetime("2012-01-01 00:00:00").tz_localize("Asia/Shanghai"), data_frequency="daily", emission_rate="daily", trading_calendar=shsz_calendar) algor_obj = TradingAlgorithm(initialize=initialize, handle_data=None, sim_params=sim_params, env=trading.environment, trading_calendar=shsz_calendar) # not use run method of TradingAlgorithm #perf_manual = algor_obj.run(data) #perf_manual.to_pickle('/tmp/perf.pickle') algor_obj.data_portal = data algor_obj._assets_from_source = \ algor_obj.trading_environment.asset_finder.retrieve_all( algor_obj.trading_environment.asset_finder.sids ) algor_obj.perf_tracker = None try: perfs = [] for perf in algor_obj.get_generator(): perfs.append(perf) daily_stats = algor_obj._create_daily_stats(perfs) daily_stats.to_pickle('/tmp/perf.pickle') finally: algor_obj.data_portal = None
其中 initialize
和 handle_daily_data
是我们策略的主要代码,我们将运行结果保存在 perf.pickle
文件中,后续如果要进一步进行分析,可以直接载入到Dataframe中进行分析,这里的输出结果和zipline 的 -o
选项输出的内容是一致的。
至此,一个相对完整的运行使用zipline运行国内市场数据的过程也完成了,同事估计大家也能对zipline
有一个简单的了解,后面有时间的话,我会完善zipline-cn-databundle
包,尽量使用国内公开数据,是大家可以直接使用它进行回测。
附图
真不该把他们放到一张图上.. 害的我后期还要用gimp切图用在文章里,这里是完整的图 T_T
风险指标的计算 (empyrical模块)
概述
量化中,我们经常会遇到各种量化指标的计算,对于zipline来说,也会对这部分计算进行处理,由于指标计算的通用性比较强,所以,zipline单独封装了 empyrical
这个模块,可以处理类似的计算,由于这个模块并不依赖其它zipline模块,我们可以在我么的项目中单独使用它。
安装
pip install empyrical
它会依赖安装 numpy, scipy, pandas 等模块
使用
导入
from empyrical import ( alpha, beta, alpha_beta_aligned, annual_volatility, cum_returns, annual_return, downside_risk, information_ratio, max_drawdown, sharpe_ratio, sortino_ratio, calmar_ratio, omega_ratio, tail_ratio )
初始数据
策略收益率
大多数函数的参数都需要策略的收益率列表,格式为 pandas.Series 结构,如下:
...
2015-07-16 -0.012143
2015-07-17 0.045350
2015-07-20 0.030957
2015-07-21 0.004902
...
索引为一个时间序列, 值为每日的收益率,为一个百分比,并且是非累积的,也就是每天相对于上一个交易日的收益比率。
下面我们构造一个初始数据
import pandas as pd returns = pd.Series( index=pd.date_range('2017-03-10', '2017-03-19'), data=(-0.012143, 0.045350, 0.030957, 0.004902, 0.002341, -0.02103, 0.00148, 0.004820, -0.00023, 0.01201) )
returns
2017-03-10 -0.012143
2017-03-11 0.045350
2017-03-12 0.030957
2017-03-13 0.004902
2017-03-14 0.002341
2017-03-15 -0.021030
2017-03-16 0.001480
2017-03-17 0.004820
2017-03-18 -0.000230
2017-03-19 0.012010
Freq: D, dtype: float64
returns.plot()
<matplotlib.axes._subplots.AxesSubplot at 0x7f3bd6fecda0>
基准收益率
和策略收益一样,我们可以构造一个模拟的基准收益
benchmark_returns = pd.Series( index=pd.date_range('2017-03-10', '2017-03-19'), data=(-0.031940, 0.025350, -0.020957, -0.000902, 0.007341, -0.01103, 0.00248, 0.008820, -0.00123, 0.01091) )
计算累计收益
creturns = cum_returns(returns)
creturns
2017-03-10 -0.012143
2017-03-11 0.032656
2017-03-12 0.064624
2017-03-13 0.069843
2017-03-14 0.072348
2017-03-15 0.049796
2017-03-16 0.051350
2017-03-17 0.056417
2017-03-18 0.056174
2017-03-19 0.068859
Freq: D, dtype: float64
creturns.plot()
<matplotlib.axes._subplots.AxesSubplot at 0x7f3bd65eb0b8>
计算最大回撤
max_drawdown(returns)
-0.02103000000000009
计算年化收益
annual_return(returns)
4.3554273608590925
年化Volatility (策略波动率)
用来测量策略的风险性,波动越大代表策略风险越高。
annual_volatility(returns, period='daily')
0.3055933840036463
Calmar比率
Calmar比率描述的是收益和最大回撤之间的关系。计算方式为年化收益率与历史最大回撤之间的比率。Calmar比率数值越大,基金的业绩表现越好。反之,基金的业绩表现越差。
calmar_ratio(returns)
207.10543798664165
Omega比率
介绍: Omega函数是用来分析收益分布的一种方法,它是一种天然的业绩指标。基于Omega的分析是在下跌,下偏矩和损益文献的精神上进行的。Omega函数捕捉到在收益分布上的所有高阶矩信息并且影响收益水平的敏感性。
公式意义:Omega越高越好,它是对偏度和峰值的一个调整。
omega_ratio(returns=returns, risk_free=0.0001)
3.0015132184078577
Sharpe比率
核心思想:理性的投资者将选择并持有有效的投资组合.
公式意义:夏普指数代表投资人每多承担一分风险,可以拿到几分收益;若为正值,代表基金收益率高过波动风险;若为负值,代表基金操作风险大过于收益率。每个投资组合都可以计算Sharpe ratio,即投资回报与多冒风险的比例,这个比例越高,投资组合越佳。
sharpe_ratio(returns=returns)
5.6451366106126715
sortino比率
介绍: Sortino ratio是一个能够评价投资资产、组合或者策略收益的指标。它是夏普比率的修正,它只对收益低于某个值的波动性进行衡量,这个值可能是持有者规定的目标收益或者是要求收益,而夏普比率是同时对上涨的和下降的波动进行衡量。尽管这两个比率都衡量的是一个调整后的投资风险,但它们的意义却不同,这导致投资的收益的结果不同。
核心思想: 公式及其解释:R是资产或组合的预期收益,T是投资策略的目标或要求的收益,起源于最小可接受收益。DR是目标方差的平方根。也就是二阶低偏矩。 Sharpe and Omega-Sharpe ratio的一个自然扩展就是由Sortino在1991年提出的,他使用的是downside risk作为分母,downside risk就是二阶下偏矩。 总风险用下降风险所代替,因为投资组合经理不会被上涨的变化而惩罚,但会被低于最小目标收益的变化而惩罚。 用下降标准差而不是总标准差,以区别不利和有利的波动。
sortino_ratio(returns=returns)
14.150708210667487
下降风险
downside_risk(returns=returns)
0.12191025172150209
信息比率(Information Ratio)
信息比率主要是用来衡量某一投资组合优于一个特定指数的风险调整超额报酬,或者说是用来衡量超额风险所带来的超额收益。它表示单位主动风险所带来的超额收益。
Information Ratio = α∕ω (α为组合的超额收益,ω为主动风险)
计算信息比率时,可以将基金报酬率减去同类基金或者是大盘报酬率(剩下的值为超额报酬),再除以该超额报酬的标准差。信息比率越高,该基金表现持续优于大盘的程度越高。
information_ratio(returns=returns, factor_returns=benchmark_returns)
0.43383172638699696
Alpha
投资中面临着系统性风险(即Beta)和非系统性风险(即Alpha),Alpha是投资者获得与市场波动无关的回报。比如投资者获得了15%的回报,其基准获得了10%的回报,那么Alpha或者价值增值的部分就是5%。
alpha(returns=returns, factor_returns=benchmark_returns, risk_free=0.01)
0.7781943501778946
Beta
表示投资的系统性风险,反映了策略对大盘变化的敏感性。例如一个策略的Beta为1.5,则大盘涨1%的时候,策略可能涨1.5%,反之亦然;如果一个策略的Beta为-1.5,说明大盘涨1%的时候,策略可能跌1.5%,反之亦然。
beta(returns=returns, factor_returns=benchmark_returns, risk_free=0.01)
0.56157656832313008
Tail Ratio
tail_ratio(returns=returns)
2.2825137911495892
其他说明
上面很多参数都涉及到年华指标,他们都会涉及到两个可选的参数, period
, annualization
, 其中,如果设置了 annualization ,则 period会被忽略,
他们是用来设置策略的年化频率的,对于period 设置为一个字符串,可以设置的参数为 'monthly', 'weekly' , 'daily', 后面是默认的值 ,
{'monthly':12 'weekly': 52 'daily': 252}
如果需要覆盖默认的值, 则可以通过 annualization
参数来设定。