Repository: rainx/inside-zipline Branch: master Commit: 31f143418f74 Files: 14 Total size: 33.2 KB Directory structure: gitextract_7_lqrnmx/ ├── .gitignore ├── README.md ├── SUMMARY.md ├── arch/ │ ├── appendix.md │ ├── arch.md │ ├── entry.md │ ├── overview.md │ └── restriction.md ├── benchmark/ │ └── benchmark.md ├── data/ │ ├── hq.md │ └── ingestissue.md ├── group/ │ └── group.md ├── risks/ │ └── empyrical.md └── tradingcalendar/ └── tc.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ _book ================================================ FILE: README.md ================================================ # 深入了解zipline 由于机缘巧合的机会,我离开了从事多年的互联网/移动互联网行业,进入了金融领域,就职与一家金融IT公司,虽然我毕业后第一份工作就在雅虎中国的财经频道从事开发工作(雅虎中国被雅虎收购之后,我就转到其它的部门),但是之前对于金融领域还是涉猎不深,这次加入后认真的补了这方面的知识,为了更加深入的了解量化相关的技术领域的问题,我准备从一些优秀的开源量化框架入手,从我熟悉的代码角度了解这个行业的业务和技术细节。 经过简单的调研,从交易频度方面,现有的量化框架大概有两类: * 高频量化交易框架 (大部分是event base的) * 中低频量化交易框架 (大部分是bar base的) 高频和中低频现在在实际的生产领域各有侧重,此外中低频框架由于逻辑相对简单,在很多教学和研究领域使用也较多,比如我当下在研究的`zipline` 。 除此之外,以后如果有时间的话还想研究一下`apama`,但由于`apama`不是开源软件,考虑只了解一下它的机制之后看看能不能基于`Apache Spark Streaming`或`Storm`来实现。好像说的有点远了,那我们回来继续来谈`zipilne`。 [zipline](http://zipline.io)是美国[Quantopian](https://quantopian.com) 公司开源的量化交易回测引擎,它使用`Python`语言开发,部分代码使用`cython`融合了部分c语言代码。`Quantopian` 在它的网站上的回测系统就是基于`zipline`的,经过生产环境的长期使用,已经比完善,并且在持续的改进中。 目前我研究的对象基于当前`zipline`的最新版本是 [1.0.2](https://github.com/quantopian/zipline/releases/tag/1.0.2) ,由于`zipline`的版本更新较快,后面可能会有些变化。请知晓。 `zipline`默认是不支持国内市场的股票数据的,我的研究过程是讲`zipline`引入国内股票市场的数据,时期可以进行国内市场的回测,在这个过程中进一步了解整个框架。 这篇文章不是一篇入门文章,所以想要了解`zipline`的基本使用方法,请参考`zipline`和`quantopian`的官方文档。 http://www.zipline.io/beginner-tutorial.html 后面的这些文档是我再研究过程中逐步整理而成,更像一个笔记的形式而不是系统化的介绍。 如果文档中出现了错误信息需要更正,欢迎到github上来发`issue` https://github.com/rainx/inside-zipline ================================================ FILE: SUMMARY.md ================================================ # Summary * [深入了解zipline](README.md) * [zipline的目录结构](arch/arch.md) * [zipline命令的入口](arch/entry.md) * [zipline的整体架构](arch/overview.md) * [zipline应用在国内市场的限制](arch/restriction.md) * [了解TradingCalendar](tradingcalendar/tc.md) * [国内股票的行情信息](data/hq.md) - [zipline ingest的时候遇到的坑(随手记的)](data/ingestissue.md) * [基准信息和国债收益率曲线](benchmark/benchmark.md) * [整合在一起](group/group.md) * [附图,完整架构图](arch/appendix.md) * [风险指标的计算](risks/empyrical.md) ================================================ FILE: arch/appendix.md ================================================ # 附图 真不该把他们放到一张图上.. 害的我后期还要用gimp切图用在文章里,这里是完整的图 T_T ![附图](../images/zipline_arch.png) ================================================ FILE: arch/arch.md ================================================ # 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 相关的需要重点关注下 ``` ## ================================================ FILE: arch/entry.md ================================================ # zipline的命令行入口 ## 先上个图 ![入口](../images/zipline_entry.png) ## 几种使用方式 首先`zipline`主要有三种启动的方式: 1. 使用`zipline`命令行工具 2. 使用`jupyter notebook` 的`zipline`集成`magic` 3. 直接自己组装和调用`TradingAlgorithm` 我们主要以`zipline`命令行工具为研究对象,来看一下它的结构,其它情况类似 ## zipline 命令行 首先从`setup.py`中可以看到代码的entry_point ```python 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`函数来进行拉取。 ================================================ FILE: arch/overview.md ================================================ # zipline的整体架构 ## 先看图 ![整体架构](../images/zipline_all_arch.png) 在这里可以看出,`zipline`由下面几个主要的部分构成 | 名称 | 说明 | | ------------------ | ---------------------------------------- | | TradingAlgorithm | 量化策略的抽象,既可以通过初始化传入构造上参数的方式,也可以通过继承的方式构造,其中`zipline`命令行主要的运行入口逻辑 `run` 方法也在这个类中 | | TradingCalendar | 交易日历的抽象,这个类非常重要,无论是在构建数据的过程还是运行的过程,都可以用到 | | DataPortal | 数据中心的抽象,可以通过这个入口获取很多不同类型的数据 | | AlgorithmSimulator | 使用`generator`的方式,表述了策略运行过程的主循环。如果说`TradingAlgorithm`更像是代表了策略本身,那么`AlgorithmSimulator`更像是策略的执行器,尤其要关注的是他的`transform`方法 | | TradingEnvirioment | 构造运行环境,主要是`benchmark`和`国债利率曲线`等信息,对于美国的市场,这个类基本上不太需要关注,但是对于国内的市场,我么需要构建自己的`TradingEnvironment` | ================================================ FILE: arch/restriction.md ================================================ # zipline应用在国内市场的限制 `zipline`可以很好的支持美国股票市场的应用,但是却无法直接使用在国内市场,主要有如下几个方面的限制 ## 数据方面 `zipline` 自带的几个`bundle` 都无法支持国内市场,其中`quandl`只包含美国的市场数据, `yahoo`的经过配置勉强可以下载到国内的股票信息。但是由于雅虎的国内行情的数据质量不敢恭维,历史数据经常会有缺失,所以感觉也不是特别靠谱的选择 ## 交易日历 ( TradingCalendar ) 交易日历是zipline系统里非常重要的部分,通过上一文章的稿子可以看到,很多其他的组件都和它有关联,给其他组件提供时间维度的索引,不管是ingest data bundle 还是运行算法,如果日历不匹配的话,那回测的结果肯定是不正确的,zipline默认使用的是美国纽交所的交易日历 (NYSE),这个显然是不能匹配国内股市的,除此之外,它还提供了 CME ([芝加哥商品交易所 ](http://www.baidu.com/link?url=Ysd27unYdDJZcKSxnEKAt8GjrM5XgNR_ZQ_yR5qWUQSQ0rdB-50pEQjn9ZJ63wgm)) ,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`, `数据`方面分别研究一下。 ================================================ FILE: benchmark/benchmark.md ================================================ # 基准信息和国债收益率曲线 基准信息和国债收益率曲线用于计算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的代码,我这里通过从 [中债信息网](http://www.chinabond.com.cn/)获取数据,并进行整合,由于网站上的数据是通过excel的文件提供的,并且格式和最终zipline所需要的格式并不相同,我这里做了一个解析其内容的包: https://github.com/rainx/cn_treasury_curve 关于数据的推导和整理过程,可以参考**[这个jupyter notebook文档](https://github.com/rainx/cn_treasury_curve/blob/master/notebooks/%E4%B8%AD%E5%80%BA%E6%95%B0%E6%8D%AE%E6%95%B4%E7%90%86.ipynb)** 这部分内容,也同样通过上面的loader整合到zipline中。 ================================================ FILE: data/hq.md ================================================ # 国内股票行情信息 ## 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`函数的参数如下: ```python 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`](http://www.zipline.io/appendix.html#zipline.assets.AssetDBWriter)的实例,通过它的write函数可以把一个证券(如股票)的基础信息,主要是码表,名称,起止日期等信息写入到数据库中,并且为每个证券分配一个sid作为唯一标识,这个sid在系统的其它地方也会成为股票的主要索引方式。默认保存在sqlite数据库中 | | daily_bar_writer | 写入每日的行情信息  [`BcolzDailyBarWriter`](http://www.zipline.io/appendix.html#zipline.data.us_equity_pricing.BcolzDailyBarWriter)的实例,通过调用write方法写入股票的开高低收和成交量等信息(OHLCV),这里的信息也需要使用sid与基本信息进行关联。默认使用bcolz的格式保存 | | minute_bar_writer | 写入每分钟行情的... | | adjustment_writer | 处理一些拆分,合并,送股,分红等事件的信息。默认使用sqlite数据库保存 | | calendar | 你当前使用的`交易日历`,数据的获取是以交易日历作为索引的,也就是说,如果你的交易日历里那一天存在,可是你无法读取行情数据,很有可能会出现错误,所以`calendar`和你的`行情信息`的匹配是很关键的 | | start_session/end_session | 获取数据的起止日期 | | cache | [`dataframe_cache`](http://www.zipline.io/appendix.html#zipline.utils.cache.dataframe_cache)的实例,你可以使用它来缓存在获取过程中的原始信息,在多次ingest的时候起到加速的作用 | | show_progress | 一个布尔值,是否显示ingest的过程,如果你的获取数据时间较长,可以判断show_progress变量来显示进度。 | | output_dir | data bundle的输出目录,如果你的data bundle 是类似 `quantopian-quandl`这种通过下载远程已经写好的数据源,并且解压到本地的,可以直接使用这个变量获取最终解压目录 | 一般来说,获取一个行情的数据源,主要需要三方面的信息 1. 使用 `asset_db_writer` 获取基础信息 2. 使用 `daily_bar_writer`/`minute_bar_writer` 写入行情信息 3. 使用 `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`调用),所以对时间和空间上都可以做到比较优化的状态。 内部的索引结构大概抽象为: ![zipline](../images/zipline_bcolzwriter.png) ================================================ FILE: data/ingestissue.md ================================================ # zipline ingest的时候遇到的坑 这个是之前做程序的时候随手记录的,本来不想写出来,但是想到也许有人也遇到类似的问题,就记一下,99.999%的人应该可以略过了。 在dailywriter 写入的时候 传入的 sid, data tuple 里的 data 是一个df ,这个df 必须是有唯一类型的, 不能是组合类型,否则ingest 的时候会报如下错误,原因是 winsorise_uint32 会将所有大于uint32的内容过滤掉,其中用到 df[boolean_df] = 0 这个不允许用混合类型的df ```python Traceback (most recent call last): File "/usr/local/bin/zipline", line 11, in load_entry_point('zipline==1.0.2+103.g9d7049a.dirty', 'console_scripts', 'zipline')() File "/usr/local/lib/python3.4/site-packages/click/core.py", line 716, in __call__ return self.main(*args, **kwargs) File "/usr/local/lib/python3.4/site-packages/click/core.py", line 696, in main rv = self.invoke(ctx) File "/usr/local/lib/python3.4/site-packages/click/core.py", line 1060, in invoke return _process_result(sub_ctx.command.invoke(sub_ctx)) File "/usr/local/lib/python3.4/site-packages/click/core.py", line 889, in invoke return ctx.invoke(self.callback, **ctx.params) File "/usr/local/lib/python3.4/site-packages/click/core.py", line 534, in invoke return callback(*args, **kwargs) File "/usr/local/lib/python3.4/site-packages/zipline-1.0.2+103.g9d7049a.dirty-py3.4-macosx-10.10-x86_64.egg/zipline/__main__.py", line 306, in ingest show_progress, File "/usr/local/lib/python3.4/site-packages/zipline-1.0.2+103.g9d7049a.dirty-py3.4-macosx-10.10-x86_64.egg/zipline/data/bundles/core.py", line 451, in ingest pth.data_path([name, timestr], environ=environ), File "/usr/local/lib/python3.4/site-packages/zipline_cn_databundle-0.2-py3.4.egg/zipline_cn_databundle/squant_source.py", line 108, in squant_bundle File "/usr/local/lib/python3.4/site-packages/zipline-1.0.2+103.g9d7049a.dirty-py3.4-macosx-10.10-x86_64.egg/zipline/data/us_equity_pricing.py", line 265, in write return self._write_internal(it, assets) File "/usr/local/lib/python3.4/site-packages/zipline-1.0.2+103.g9d7049a.dirty-py3.4-macosx-10.10-x86_64.egg/zipline/data/us_equity_pricing.py", line 327, in _write_internal for asset_id, table in iterator: File "/usr/local/lib/python3.4/site-packages/click/_termui_impl.py", line 259, in next rv = next(self.iter) File "/usr/local/lib/python3.4/site-packages/zipline-1.0.2+103.g9d7049a.dirty-py3.4-macosx-10.10-x86_64.egg/zipline/data/us_equity_pricing.py", line 258, in ((sid, to_ctable(df, invalid_data_behavior)) for sid, df in data), File "/usr/local/lib/python3.4/site-packages/zipline-1.0.2+103.g9d7049a.dirty-py3.4-macosx-10.10-x86_64.egg/zipline/data/us_equity_pricing.py", line 166, in to_ctable # we already have a ctable so do nothing File "/usr/local/lib/python3.4/site-packages/zipline-1.0.2+103.g9d7049a.dirty-py3.4-macosx-10.10-x86_64.egg/zipline/data/us_equity_pricing.py", line 169, in to_ctable winsorise_uint32(raw_data, invalid_data_behavior, 'volume', *OHLC) File "/usr/local/lib/python3.4/site-packages/zipline-1.0.2+103.g9d7049a.dirty-py3.4-macosx-10.10-x86_64.egg/zipline/data/us_equity_pricing.py", line 117, in winsorise_uint32 File "/usr/local/lib/python3.4/site-packages/zipline-1.0.2+103.g9d7049a.dirty-py3.4-macosx-10.10-x86_64.egg/zipline/data/us_equity_pricing.py", line 159, in winsorise_uint32 df[mask] = 0 File "/usr/local/lib/python3.4/site-packages/pandas/core/frame.py", line 2354, in __setitem__ self._setitem_frame(key, value) File "/usr/local/lib/python3.4/site-packages/pandas/core/frame.py", line 2390, in _setitem_frame self._check_inplace_setting(value) File "/usr/local/lib/python3.4/site-packages/pandas/core/generic.py", line 2781, in _check_inplace_setting raise TypeError('Cannot do inplace boolean setting on ' TypeError: Cannot do inplace boolean setting on mixed-types with a non np.nan value ``` 部分数据缺失问题 如果信息在`tradingcalendar`有效地日期没有数据,则`ingest`之后会造成错乱 如 深发展 `000001.sz` 在通达信的数据中,查询 ``` 2010-12-16  16.62  16.72  16.43  16.45  22055606 2010-12-17  16.45  16.50  16.29  16.35  17302092 2010-12-20  16.39  16.44  15.99  16.13  25620545 2010-12-21  16.17  16.65  16.07  16.57  30685363 2010-12-22  16.61  16.63  16.20  16.23  22703588 2010-12-24  16.20  16.60  16.15  16.44  28612724 2010-12-27  16.62  16.73  16.02  16.07  30865798 ``` 无法发现 12-23 日的数据, 导致使用zipline的BcolzReader的时候,日期会错乱 ```python In [32]: reader2.get_value(1202, '2010-12-21', 'close') Out[32]: 16.57 In [33]: for i in range(1,9): ...: date = '2010-12-2' + str(i) ...: v = (date, reader2.get_value(1202, date, 'close')) ...: print(v) ...: ('2010-12-21', 16.57) ('2010-12-22', 16.23) ('2010-12-23', 16.44) ('2010-12-24', 16.07) ``` 在网易上查看,也无此信息 [http://quotes.money.163.com/trade/lsjysj_000001.html?year=2010&season=4](http://quotes.money.163.com/trade/lsjysj_000001.html?year=2010&season=4) 奇怪的是,雅虎有对应的数据,可能是前向或者后向填充的数据 Bcolz Data的meta信息内容 ```python { "first_trading_day":1292889600, "end_session_ns":1477872000000000000, "calendar_name":"SHSZ", "start_session_ns":1292889600000000000, "first_row":{}, "calendar_offset":{}, "last_row":{} } ``` 其中  `calendar_name` 对应日历的名字 `first_row`` last_row` , `calendar_offset` 都是 以字符串格式定义的sid为key的字典 `table`里面是针对每个column每个股票前后连接起来的一个结构 `first_row` 定义了该sid对应股票在table里的起始位置 `last_row` 定义了该sid对应股票在table里的起始结束位置 `calendar_offset` 是股票起始数据日期对于当前 `start_session_ns` 的偏移量 这里有一个坑是first_row和last_row之间的日期不能有空的数据,及时股票停牌,也要填充上数据,否则数据索引的时候会出错 研究zipline的处理方式 在quandl模块里 ```python sessions = calendar.sessions_in_range(start_session, end_session) raw_data = raw_data.reindex( sessions.tz_localize(None), copy=False, ).fillna(0.0) ``` reindex 并fillna(0.0) 修改之后,调用  ```python In [151]: reader2.get_value(1202, '2010-12-23', 'close') Out[151]: -1 ``` 查看历史信息 ```python def handle_daily_data(context, data): sym = symbol('000001.SZ') # Save values for later inspection record(price=data.current(sym, 'price'),) In [23]: df.price Out[23]: 2010-12-21 07:00:00 16.57 2010-12-22 07:00:00 16.23 2010-12-23 07:00:00 16.23 2010-12-24 07:00:00 16.44 2010-12-27 07:00:00 16.07 2010-12-28 07:00:00 15.95 2010-12-29 07:00:00 15.77 2010-12-30 07:00:00 15.67 2010-12-31 07:00:00 15.79 Name: price, dtype: float64 ``` ================================================ FILE: group/group.md ================================================ # 整合在一起 接下来,我们就把我们之前做的整合在一起,看看如何进行国内股票的回测 (注意,下面的软件包中部分依赖的内容并未发布到github上,下面的说明只是示意,如果要让系统运行起来,需要自行开发部分代码,主要是数据获取部分的代码) 首先,安装`cn-stock-holidays` `cn-treasury-curve` `zipline-cn-databundle` 等软件包 ## ingest 过程 配置 `~/.zipline/extension.py`注册bundle ```python 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 ```python zipline ingest cn_squant ``` ## 运行策略 由于zipline默认的运行过程没有办法支持自定义的loader ,所以这里我们自己来实现 `TradingAlgorithm`的`run` 过程。 ```python 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`包,尽量使用国内公开数据,是大家可以直接使用它进行回测。 ================================================ FILE: risks/empyrical.md ================================================ # 风险指标的计算 (empyrical模块) ## 概述 量化中,我们经常会遇到各种量化指标的计算,对于zipline来说,也会对这部分计算进行处理,由于指标计算的通用性比较强,所以,zipline单独封装了 ```empyrical``` 这个模块,可以处理类似的计算,由于这个模块并不依赖其它zipline模块,我们可以在我么的项目中单独使用它。 ## 安装 ```python pip install empyrical ``` 它会依赖安装 numpy, scipy, pandas 等模块 ## 使用 ### 导入 ```python 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 ... ``` 索引为一个时间序列, 值为每日的收益率,为一个百分比,并且是非累积的,也就是每天相对于上一个交易日的收益比率。 下面我们构造一个初始数据 ```python 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) ) ``` ```python 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 ```python returns.plot() ``` ![png](output_5_1.png) #### 基准收益率 和策略收益一样,我们可以构造一个模拟的基准收益 ```python 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) ) ``` ### 计算累计收益 ```python creturns = cum_returns(returns) ``` ```python 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 ```python creturns.plot() ``` ![png](output_11_1.png) ### 计算最大回撤 ```python max_drawdown(returns) ``` -0.02103000000000009 ### 计算年化收益 ```python annual_return(returns) ``` 4.3554273608590925 ### 年化Volatility (策略波动率) 用来测量策略的风险性,波动越大代表策略风险越高。 ```python annual_volatility(returns, period='daily') ``` 0.3055933840036463 ### Calmar比率 Calmar比率描述的是收益和最大回撤之间的关系。计算方式为年化收益率与历史最大回撤之间的比率。Calmar比率数值越大,基金的业绩表现越好。反之,基金的业绩表现越差。 ```python calmar_ratio(returns) ``` 207.10543798664165 ### Omega比率 介绍: Omega函数是用来分析收益分布的一种方法,它是一种天然的业绩指标。基于Omega的分析是在下跌,下偏矩和损益文献的精神上进行的。Omega函数捕捉到在收益分布上的所有高阶矩信息并且影响收益水平的敏感性。 公式意义:Omega越高越好,它是对偏度和峰值的一个调整。 ```python omega_ratio(returns=returns, risk_free=0.0001) ``` 3.0015132184078577 ### Sharpe比率 核心思想:理性的投资者将选择并持有有效的投资组合. 公式意义:夏普指数代表投资人每多承担一分风险,可以拿到几分收益;若为正值,代表基金收益率高过波动风险;若为负值,代表基金操作风险大过于收益率。每个投资组合都可以计算Sharpe ratio,即投资回报与多冒风险的比例,这个比例越高,投资组合越佳。 ```python sharpe_ratio(returns=returns) ``` 5.6451366106126715 ### sortino比率 介绍: Sortino ratio是一个能够评价投资资产、组合或者策略收益的指标。它是夏普比率的修正,它只对收益低于某个值的波动性进行衡量,这个值可能是持有者规定的目标收益或者是要求收益,而夏普比率是同时对上涨的和下降的波动进行衡量。尽管这两个比率都衡量的是一个调整后的投资风险,但它们的意义却不同,这导致投资的收益的结果不同。 核心思想: 公式及其解释:R是资产或组合的预期收益,T是投资策略的目标或要求的收益,起源于最小可接受收益。DR是目标方差的平方根。也就是二阶低偏矩。 Sharpe and Omega-Sharpe ratio的一个自然扩展就是由Sortino在1991年提出的,他使用的是downside risk作为分母,downside risk就是二阶下偏矩。 总风险用下降风险所代替,因为投资组合经理不会被上涨的变化而惩罚,但会被低于最小目标收益的变化而惩罚。 用下降标准差而不是总标准差,以区别不利和有利的波动。 ```python sortino_ratio(returns=returns) ``` 14.150708210667487 ### 下降风险 ```python downside_risk(returns=returns) ``` 0.12191025172150209 ### 信息比率(Information Ratio) 信息比率主要是用来衡量某一投资组合优于一个特定指数的风险调整超额报酬,或者说是用来衡量超额风险所带来的超额收益。它表示单位主动风险所带来的超额收益。 Information Ratio = α∕ω (α为组合的超额收益,ω为主动风险) 计算信息比率时,可以将基金报酬率减去同类基金或者是大盘报酬率(剩下的值为超额报酬),再除以该超额报酬的标准差。信息比率越高,该基金表现持续优于大盘的程度越高。 ```python information_ratio(returns=returns, factor_returns=benchmark_returns) ``` 0.43383172638699696 ### Alpha 投资中面临着系统性风险(即Beta)和非系统性风险(即Alpha),Alpha是投资者获得与市场波动无关的回报。比如投资者获得了15%的回报,其基准获得了10%的回报,那么Alpha或者价值增值的部分就是5%。 ```python 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%,反之亦然。 ```python beta(returns=returns, factor_returns=benchmark_returns, risk_free=0.01) ``` 0.56157656832313008 ### Tail Ratio ```python tail_ratio(returns=returns) ``` 2.2825137911495892 ## 其他说明 上面很多参数都涉及到年华指标,他们都会涉及到两个可选的参数, ```period```, ```annualization``` , 其中,如果设置了 annualization ,则 period会被忽略, 他们是用来设置策略的年化频率的,对于period 设置为一个字符串,可以设置的参数为 'monthly', 'weekly' , 'daily', 后面是默认的值 , ```python {'monthly':12 'weekly': 52 'daily': 252} ``` 如果需要覆盖默认的值, 则可以通过 ```annualization``` 参数来设定。 ================================================ FILE: tradingcalendar/tc.md ================================================ # 了解TradingCalendar 本文大部分内容都是我在研究`TradingCalendar`时所记得笔记,这个图是我后期整理的, ![tc](../images/zipline_tradingcalendar.png) TraderCalendar的解释: ``` 一个TraderCalendar代表一个单独交易市场的时间信息,时间信息由两部分组成 sessions 和 opens/closes session代表一个连续的分钟时间段, 使用UTC的午夜时间来定义, 注意它代表的不是一个时间点,使用UTC午夜时间只是为了方便 ``` 里面有很多函数是处理minute 和 session转换以及获取开收盘时间时间 对应文档在 [http://www.zipline.io/appendix.html#trading-calendar-api](http://www.zipline.io/appendix.html#trading-calendar-api) [http://www.zipline.io/_modules/zipline/utils/calendars/trading_calendar.html](http://www.zipline.io/_modules/zipline/utils/calendars/trading_calendar.html) 系统自带的calendars 可以通过``zipline.utils.calendars.get_calendar`获取,如: ```python from zipline.utils.calendars import get_calendar c = get_calendar("NASDAQ") ``` 获取了纳斯达克的calendar, 注意,这里的calendar是存在别名的,不同的交易所可能对应同样的别名,只要它们的交易时间相同,如 `NASDAQ` 交易所的别名就和`NYSE`相同, 这部分定义可以查看`zipline.utils.calendars.calendar_utils`模块下看到定义 ```python _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时区的时间,具体的时间不变,如  ```python 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日开收盘信息 ```python 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时间,对应美国的时间为 ```python 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时间应该为: ```python 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` 的实现保存着节假日的信息 ```python In [120]: c.regular_holidays Out[120]: In [121]: c.regular_holidays.holidays Out[121]: > In [122]: c.regular_holidays.holidays() ``` 在美国有一个`special_close`, `special_open`的概念,在国内好像没有,好像是在指定的日期会提早收盘,这个信息通过一个数组保存,里面可能有多个提早收盘的时间点和收盘的列表。 有两种不同的表示, 一种是可以用常规的holiday对象表示,还有一种则直接写明日期 ```python (datetime.time(13, 0), ), (datetime.time(14, 0), )] 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`方法来注册。 这里有一个我写的例子 https://github.com/rainx/cn_stock_holidays/blob/master/cn_stock_holidays/zipline/exchange_calendar_shsz.py 由于国内股市的节假日时间信息无法通过类似美国的有明确的假期规则,所以这里使用了它的`adhoc_holidays`的方式,也就是类似枚举的方式获得,这里我通过 `cn_stock_holidays` (https://github.com/rainx/cn_stock_holidays) 项目维护了一个节假日列表,历史节假日我是根据历史指数的非周六日的无开盘记录来获取的,未来的可预期的节假日我也会保持更新,如果你在其他项目里用到该数据,可以直接安装使用。 ```bash pip install git+https://github.com/rainx/cn_stock_holidays.git ``` 具体的使用说明,可以参考github上的文档 具体的使用方式为 ```python from cn_stock_holidays.zipline.exchange_calendar_shsz import SHSZExchangeCalendar register_calendar("SHSZ", SHSZExchangeCalendar(), force=True) c=get_calendar("SHSZ") ``` 或者你也可以直接使用我这边已经注册的单例 ```python from cn_stock_holidays.zipline.default_calendar import shsz_calendar ``` 直接获取到 shsz_calendar的实例。 除了休市时间外,对于国内股市还有一点区别是日内的午休时间,这个在国外是没有的,这个应该对分钟数据有影响,所以在`SHSZExchangeCalendar`的实现里,我重写了 `all_minutes` 使其支持国内的`lunch break`