引言
最近大幅扩充了 xalpha 的代码,添加了大量的新功能,应该说基金管理已经不能概括 xalpha 的现状了,xalpha 已经可以说是完整的金融市场数据的封装和量化工具箱。本文就想到哪说到哪,可能有些 API 设计的哲学,有些用法的解释,有些内部程序架构的讨论。
设计哲学
任何一个和金融市场相关的工具绕不开的就是数据,怎么提供数据,谁来维护数据,数据的接口设计的风格,决定了不同的金融产品,软件和平台的差异。首先是数据的提供,数据源根本上就是两类,一类是专门有人负责清洗验证和管理,保证更新质量的中心化数据库,其往往还向外提供 RESTFUL API 或其他形式的封装完好的接口。这类东西当然好,不过有个明显的缺点,就是得花钱,而且通常还不是小数目。第二类数据源就是各种国内外的财经网站和专门类的官网,比如国外的雅虎财经,彭博,FT,标普,国内的新浪财经,网易财经,天天基金,雪球,上交所,深交所,中证官网等等。这些源头数据类型质量和更新快慢参差不齐,想要便捷实用需要在爬虫设计,接口封装,数据简单清洗上花大力气。xalpha 则努力结合这两种数据源的优势。首先当然是优先免费的网络源,而且为了稳定性,后端的网络源越多越好,上述这十几个知名网站的数据源,xalpha 都实现了爬取和封装。使得即使被某些网站反爬或者某些网站数据结构改版,也几乎不会削弱 xalpha 获取数据的能力。同时,毕竟有些数据尤其是历史数据很难在公开免费的财经网站获取,比如历史上指数的成分股权重等。因此 xalpha 还是借助了第一类数据提供商的数据源。目前 xalpha 暂时包括了聚宽的数据源,其好处是聚宽数据可以申请本地免费试用,而同时 xalpha 还完美兼容在聚宽研究环境云平台直接使用,因此可以保证免费的数据获取。最重要的是,完全不理会聚宽的配置和账号,xalpha 还是可以正常使用且大部分数据和功能不受影响。这就是关于数据源 xalpha 的原则:永远使用免费的数据,优先使用不需要鉴权的数据,每个数据都尽量有不同数据源的备份,总是可以不进行任何鉴权的正常使用以避免用户的额外申请和配置。
数据的维护同样是个问题,如果一个项目数据维护需要自己来,比如每天需要跑一定的脚本来保证本地数据,那这样项目的稳定性是很成问题的。或者说,想让这样的项目达到和不需要维护数据的项目一样的稳定性和易用性是成本很高的。一旦选择了自己维护数据的路线,那就不是简单的 crontab 就能胜任的。为了保证稳定性和容错,需要数据库表的设计,每日定时运行的服务,自动爬取和中心化数据清洗入库与常规维护,甚至运行服务器与网络的稳定性和高可用的考量。说白了,这时候维护的不再是一个代码组成的简单项目,而是大量基础设施共同构筑的应用和服务,这需要的成本极其高昂,也是我绝对不会选择的发展路线。而且你维护的这么费劲,一旦换一台电脑程序就不能跑了,是我完全无法接受的。因此每当看到金融类的开源项目教大家怎么每日更新下本地数据,以前数据需要去百度云下载压缩包云云,我就会立马关掉。这绝不是一个金融类开源项目应该有的技术路线图,这种要求本地数据维护的项目,要想稳定可用,需要用户的技术积累和精力投入都是非常巨大的,在性价比上是一定不划算的。因此 xalpha 的实际哲学,一定是只要 pip install xalpha
之后不需要任何设定配置,和操作系统数据库无关,马上就可以用,一行代码就可以拿到想要的任何数据,这才是一个合理的开源金融类项目的自我修养。当然需要指出,有些本地数据的更新维护也是无奈的,比如深交所的基金场内份额,只有当日数据,如果想获取历史数据,那就只能每天去爬取来更新本地数据,但由于我对引入一整套基础设施来保证本地数据完整可用的极端反感,这种时候我宁愿选择 xalpha 不提供该类型数据,也绝不会允许强制的本地数据维护服务的出现。当然我们下面会看到:1. 深交所场内基金历史份额还是有其它方式可以不依赖本地数据每日更新拿到(聚宽),2. 不强制需要本地数据维护就能使用,不代表本地数据不能维护,如果你足够闲,可以轻松的将 xalpha 和本地数据维护结合起来。
提供数据的接口也是一个问题,是 RESTFUL API,还是懒到不封装而直接基于 sqlalchemy 的 query 查表,还是设计一整套的函数来获取不同数据,以至于每次用,不找文档的话,都不知道该用的函数是啥?在我看来,以上的设计都很不完美。特别是对于每种数据设计一个函数的行为,我是完全无法接受的:获取历年 GDP 的函数叫 get_gdp
, 获取每日股价的函数叫 get_stock_price
,获取基金的函数叫 get_fund
,获取美股指数的函数叫做 get_us_index
,然后再配上20页长的数据字典,API 文档,这样的数据接口设计,无疑是完全失败的。事实上,你会发现所有的数据接口其实只有两种类型,一种是系列数据,通常可以以 pandas.DataFrame 的形式呈现,比如每日的某种数据,每周的某种数据等等。另一种是实时数据,通常可以以 dict 的形式呈现,返回结果包括了现价,时间和一些标的元信息。两类返回,两个性质,那么两个函数接口就完全足够了!不同的数据获取,只不过是代码不同而已。GDP 也可以通过 get_stock_price
来获取,只要股票代码传入的是 GDP 就完事了,就这么简单!这才是一个金融数据类的项目应有的接口,而不是绞尽脑汁的为每一种新数据想个新的函数名字和接口。统一接口的好处不只是方便记忆,下面我们会看到一个统一的接口对于缓存等设计也相当的重要和简洁。因此 xalpha 数据获取就两个接口,xa.get_daily()
用来获取任何时间序列历史数据,管你是每日股价还是每周估值还是每季度GDP还是每年度失业率,统统是这一个接口!用户只想知道数据本身,不想知道通过具体哪个函数获取的数据,因此不自己封装好基于输入代码的函数分流,而把两位数的数据函数接口暴露给用户的项目,都是对用户时间的犯罪。至于实时数据,xa.get_rt()
就好了,不管什么金融标的,只要代码格式符合 xalpha 规范,都可以返回有效的实时数据和元信息。(当然,必须承认 xalpha 还有第三个数据接口 xa.get_bar()
用来获取分钟线和小时线等更精细的数据,这一接口之所以和 get_daily 分开,主要是因为缓存上的考虑,get_bar 数据增量缓存的逻辑过于复杂,再加上其背后 API 的能力不同,因此暂时没有缓存策略可用,所以将其和 get_daily 独立开了。xalpha 也不提倡用户大规模的使用 get_bar,除非你知道自己在干什么,并且了解该接口的数据的局限性和多态性。)
总结下 xalpha 不做那种:
- 必须要先去申请账号,每次使用还得设置下 token 才能用的软件(有朝一日中心服务商有倒闭风险)
- 先得从网盘下载一堆数据,之后还得每天更新本地数据才能用的软件(某天忘了更新数据可能很多东西就错乱了)
- 获取不同数据的函数名和接口方法就一张纸写不下的软件(无论用的多熟,还是会离开文档,根本没法用)
对于 xalpha 用户唯一需要做的,就是 pip install xalpha
,之后 import xalpha as xa
,xa.get_daily("SH600000")
就完事了,真正的零配置,零学习成本。
缓存策略
透明的缓存策略,也是 xalpha 全新设计的一个亮点。对于这种数据来源于网络的软件,一个最大的问题就是数据返回的速度。网络的不稳定性往往造成获取数据的延迟,同时高强度的爬取可能会被反爬机制屏蔽掉 IP。而缓存机制一下就解决了这两个问题,如果本地有要请求的数据或数据的一部分,那么只需要增量更新剩余没有部分的数据,这将极大的降低网络请求总数和数据获取的速度。这样一种缓存机制最重要的就是对用户透明。也就是用户什么都不需要做,如果用户需要自己操心数据有没有缓存,如何增量更新等,那就太失败了。相反 xa.get_daily("LK", start="2020-03-03", end="2020-04-04")
后,再 xa.get_daily("LK", start="2020-02-03", end="2020-04-04")
时就只会爬取从 2020/02/03 到 2020/03/02 的数据做增量更新,而 xa.get_daily("LK", start="2020-03-03", end="2020-03-14")
时就会不爬取直接返回数据,这样一种用户完全没有感知的透明缓存,将极大的提升使用体验。这种缓存默认是在内存中,这样下次使用 python 时,之前的数据还是要重新爬取。更好的实践是将数据透明缓存到本地,只需 xa.set_backend(backend="csv", path="./")
即可。这里的后端还可以选数据库的 “sql”。具体用法可以参考文档。设置完这一句其他就都不用管了,全部经过 get_daily 的数据都会妥善处理,增量更新,透明缓存到本地,这同时还维护着内存缓存,这种双层缓存机制极大的减少了需要的硬盘读写,使得速度更快。
读者可能会疑惑这样的本地缓存,不是和上边设计原则的不需要维护本地数据相矛盾吗。这事实上是不矛盾的,因为缓存数据只是写在本地,你根本不需要去管理,同时也可以随便部分或全部删除,不会有任何问题。有这些本地缓存文件或没有都不影响 xalpha 的正常使用。因此这些本地缓存只是不需要用户管理使得 xalpha 可以运行更快的 cache,而不是有意义的数据。与此同时,这些缓存格式良好,如果用户真的有精力进行外部的维护更新甚至是手动改写都是可行的,但只建议专业用户这么做,因为一旦缓存数据被破坏而没有删除,会影响 get_daily 的结果。虽然这对于专业用户,反而可以实现更神奇的效果。
set_backend 还有一些更有趣的扩展,使得用户更好的进行缓存控制。首先是 set_backend 具有 precached 关键字参数,可以传入日期字符串。precached="2017010"
就意味着对于所有的 get_daily 无缓存时都会“预热”数据,也即总是从 2017/01/01 开始爬取指定数据。但是用户不需要担心 xa.get_daily("PDD", start="2019-01-01")
返回的数据从 2017 开始,这种后台的缓存从 2017 开始和前端返回还是按照 get_daily 的规定,是并行不悖的,这就是缓存”透明“的含义,用户永远无需操心到底缓存发生了什么,快就完事了。
还有一些更精细的控制,这段一般用户基本上用不到,可以放心跳过。如果你希望某个数据不在依赖缓存(比如担心缓存出了问题),那么可以 xa.get_daily("HK00700", refresh=True)
,有了 refresh 选项后,缓存中的数据会被强制刷新成新数据。又比如某个数据源出现问题,但本地还有部分数据,比如数据只到三月,你当然可以 xa.get_daily("commodities/crude-oil", end="20200331")
来控制数据范围从而只用缓存,但还有更简单粗暴强制使用缓存数据,不去更新连接网络数据源的方案: xa.get_daily("commodities/crude-oil", fetchonly=True)
,有了 fetchonly,get_daily 就只会求助于缓存了。最后你还可以用 xa.universal.check_cache("LK", start="2020/01/01")
来抽检缓存数据的完整性和正确性。
程序内部
xalpha 的新版非常激进地使用了函数装饰器和python的反射机制,把python动态语言的有点发挥到极致。xa.set_proxy()
可以动态随时设定代理和取消代理,xa.set_backend
可以在程序运行时不断改变缓存的后端,而不需要必须导入之初指定。xa.set_display
则可以控制 DataFrame 在 Jupyter Notebook 进行web级的显示(支持表格的筛选,排序,分页,高亮和搜索)。同时除了 get_daily 完整的透明缓存系统,xalpha 根据具体函数特性,大量启用了 lru_cache 和自己实现的 lru_cache_time 装饰器进行函数内容的缓存和有限时间缓存,更大程度降低不必要的重复爬取。xa.set_handler
则可以进行 get_daily, get_rt 等函数的魔改,使得 xalpha 可以和有现成数据库和部分数据与规范的企业级应用无缝对接。
工具不只是数据
很多金融项目停留在提供各种数据,这从最刚开始就不是 xalpha 的初衷。虽然 xalpha 提供的数据,特别是海外市场标的数据可能比任何市面的免费产品甚至付费产品都要丰富,但数据也只是 xalpha 的副产品而已。xalpha 从设计之初的 xa.fundinfo
, xa.trade
, xa.mul
系列,xalpha 就是以简洁明了的工具而为核心的。数据不经过工具的综合分析和处理,就只是单纯的数字而已。与数据的统一函数接口不同,工具由于其复杂性,xalpha 都采用了面向对象的封装。这种封装的核心就是不暴露原始数据,工具就是工具,我只想看最后结果,而不想了解用到了哪些数据。
一些 xalpha 典型的工具例子包括历史估值系统:xa.PEBHistory
, xa.StockPEBHistory
, xa.SWPEBHistory
。分别对应了指数,个股和行业的历史估值。通过 .summary()
方法可以马上了解到对应标的 PE,PB 的绝对值,历史百分位和可能最大跌幅。通过 .v()
方法可以查看历史估值变化的可视化。尤其值得一提的是 PEBHistory 对应的指数估值是严格按照成分股权重计算的,结果是要比按总市值加权和等权都要准确的,真实的反映了历史估值情况,这一数据在市面上可以说是绝无仅有的!其有效的指出了茅台占15%权重的上证50并没有像其他数据源给出的那么便宜。
还有一些工具专门用来做基金净值的估值,RTPredict
可以做一般基金的盘中实时估值,QDIIPredict
可以做 QDII 基金的 T-1 日净值预测和实时净值分析。由此自然溢价率等套利相关信息可以轻松获取,不谦虚地说关于 QDII 的估值,xalpha 应该也是市面上最准的。那些FOF原油基金都可以估计的比华宝油气的偏差还小。