给abu增加Binance api接口获取加密货币价格数据

引言

之前的文章中,我使用优矿平台完成了一个简单的策略分析。在那篇文章的末尾,我提到一个提高的方向就是自己实现数据获取和回测。我个人并不喜欢看不到源码的框架,并且优矿想要获取加密货币的数据必须付费,所以这里想基于abu(一个开源的量化系统)进行一个开发。不过值得说明的是,像我这样的个人研究者,受迫于资金和工具的落后,相对来说,使用优矿这样的工具平台其实是一个更好的选择,可以帮助研究人员更专注于策略。我这么做只是自己的偏好而已。

本项目地址

https://github.com/qiushui777/Qsabu

abu简介

abu是由阿布开发的一个量化交易系统,已经公布源码,官方网站在https://blog.abuquant.com/。其上有丰富的教程。此处就不再多做介绍了。

abu数据获取源码分析

为了增加币安的api,我们需要先分析清楚abu的相关源码。

(1)判断是从本地还是网络获取数据

获取数据首先从ABuSymbolPd.py文件开始,它自身并不是一个类,而是写了几个重要的函数。在示例中,代码通过make_kl_df函数获取数据。该函数有以下参数
1
2
3
4
5
6
7
8
9
10
:param data_mode: EMarketDataSplitMode对象
:param symbol: list or Series or str or Symbol
e.g :['TSLA','SFUN'] or 'TSLA' or Symbol(MType.US,'TSLA')
:param n_folds: 请求几年的历史回测数据int
:param start: 请求的开始日期 str对象
:param end: 请求的结束日期 str对象
:param benchmark: 资金回测时间标尺,AbuBenchmark实例对象
:param show_progress: 是否显示进度条
:param parallel: 是否并行获取
:param parallel_save: 是否并行后进行统一批量保存

如果这里设定了要使用并行的话,将会调用kl_df_dict_parallel函数,其他情况下,调用的是_make_kl_df。而后者进一步调用了kline_pd函数。这个函数在ABuDataSource.py中,并且会设置数据源source_dict。此处函数关键的部分如下。

1
2
3
4
5
6
7
8
9
if ABuEnv.g_data_fetch_mode != EMarketDataFetchMode.E_DATA_FETCH_FORCE_NET:
# 如果env中设置并非强制从网络获取数据,就从本地数据尝试读取df, df_req_start
df, df_req_start, df_req_end = load_kline_df(temp_symbol.value)


if ABuEnv.g_data_fetch_mode == EMarketDataFetchMode.E_DATA_FETCH_FORCE_NET:
# 如果是强制走网络,直接请求使用load_kline_df_net
return load_kline_df_net(source, temp_symbol, n_folds=n_folds, start=start, end=end, start_int=start_int,
end_int=end_int, save=save), save_kl_key

分别是从本地和从网络来获取数据。这两个关键的函数都在ABuDataCache.py中。
kline_pd这个函数还有一个比较重要的地方是,它最初会设置一个symbol,这对后面的很多东西都会有影响。这部分逻辑在code_to_symbol函数中提现。

(2)本地数据获取

在load_kline_df中,会先通过load_kline_key判断对应的数据集是否存在,如果存在就会进一步调用load_kline_func函数也就是_load_kline_csv。load_df_csv会返回一个Dataframe也就是那个csv的内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
date_key = None
try:
# 首先通过symbol_key查询对应的金融时间序列是否存在索引date_key
date_key = load_kline_key(symbol_key)
except HDF5ExtError as e:
# r_s = False的话,hdf5物理性错误就删除了,重来,所以重要的hdf5需要手动备份.
r_s = True
raise RuntimeError('hdf5 load error!! err={} '.format(e)) if r_s else os.remove(ABuEnv.g_project_kl_df_data)

if date_key is not None:
# 在索引date_key存在的情况下,继续查询实体金融时间序列对象
df = load_kline_func(date_key[0])
if df is not None:
df['key'] = list(range(0, len(df)))
# 索引date_key中转换df_req_start
df_req_start = int(date_key[0][-17: -9])
# 索引date_key中转换df_req_end
df_req_end = int(date_key[0][-8:])
return df, df_req_start, df_req_end

(3网络数据的获取

从网络获取数据的关键就是先实例化对象,然后调用对象的kline函数来获取数据。
1
2
3
4
5
6
# 实例化数据源对象
data_source = source(temp_symbol)

if data_source.check_support():
# 通过数据源混入的SupportMixin类检测数据源是否支持temp_symbol对应的市场数据
df = data_source.kline(n_folds=n_folds, start=start, end=end)

这里的source最初是在ABuDataSource.py 中被定义,而后一路传递过来。

1
2
3
4
5
6
7
source_dict = {EMarketSourceType.E_MARKET_SOURCE_bd.value: BDApi,
EMarketSourceType.E_MARKET_SOURCE_tx.value: TXApi,
EMarketSourceType.E_MARKET_SOURCE_nt.value: NTApi,
EMarketSourceType.E_MARKET_SOURCE_sn_us.value: SNUSApi,
EMarketSourceType.E_MARKET_SOURCE_sn_futures.value: SNFuturesApi,
EMarketSourceType.E_MARKET_SOURCE_sn_futures_gb.value: SNFuturesGBApi,
EMarketSourceType.E_MARKET_SOURCE_hb_tc.value: HBApi}

对应的每个值为ABuEnv.py中EMarketSourceType类被定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class EMarketSourceType(Enum):
"""
数据源,当数据获取不可靠时,可尝试切换数据源,更可连接私有的数据源
"""
"""百度 a股,美股,港股"""
E_MARKET_SOURCE_bd = 0
"""腾讯 a股,美股,港股"""
E_MARKET_SOURCE_tx = 1
"""网易 a股,美股,港股"""
E_MARKET_SOURCE_nt = 2
"""新浪 美股"""
E_MARKET_SOURCE_sn_us = 3

"""新浪 国内期货"""
E_MARKET_SOURCE_sn_futures = 100
"""新浪 国际期货"""
E_MARKET_SOURCE_sn_futures_gb = 101

"""火币 比特币,莱特币"""
E_MARKET_SOURCE_hb_tc = 200

而这里的HBApi类则在ABuDataFeed.py中被定义。在这个datafeed中,我们看到它有调用到ABuDataParser.py中的HBTCParser来进行数据的解析。

(4)数据存储

在load_kline_df_net函数中,获取网络的数据后,会调用dump_kline_df函数。
1
dump_kline_df(df, temp_symbol.value, df_key)

这个函数中,如果发现本地没有存储过相关有的数据,会去调用

1
dump_kline_func(symbol_key, date_key, dump_df)

一般情况下,我们用csv的格式进行存储,这里的话也就是_dump_kline_csv。

增加币安api

通过以上的分析,我们已经可以开始修改源码增加api了。

(1)全局变量设置

首先在ABuEnv.py的类EMarketSourceType中加入
1
E_MARKET_SOURCE_binance= 201

在ABuDataSource.py中添加SourceDict的对应项,并记得在import的时候导入

1
EMarketSourceType.E_MARKET_SOURCE_binance: BNApi

在ABuDataFeed.py中加入对应的BNApi类。由于BNApi类需要继承SuppportMixin,这个类会检查自身是否支持传入的这个交易对。在加入这个类的过程中,可以发现kline_pd的code_to_symbol函数也需要做相应的修改,否则无法返回我们需要的symbol对象。所以我们需要在这个函数中加入如下的代码,专门为我们自定义的数据请求服务。

1
2
3
4
5
qs_flag = code[:2]
if(qs_flag == "Qs"):
market = EMarketTargetType.E_MARKET_TARGET_TC
sub_market = EMarketSubType.COIN
return Symbol(market, sub_market, code)

并且,我们在SupportMixin类的check_support函数中也添加对应的代码

1
2
if(symbol.symbol_code[:2] == "Qs"):
return True

(2)数据处理

对于币安返回的数据,我们需要设置一个parser进行处理。这里需要解决的问题是,币安返回的date是毫秒级别的unix time,所以我们在ABuDateUtil.py中加入这样的一个函数进行时间的转换。
1
2
3
4
5
6
7
8
def fmt_epoch(epoch_time):
"""
conver the unix time with milliseconds to datetime
https://stackoverflow.com/questions/21787496/converting-epoch-time-with-milliseconds-to-datetime
"""
s, ms = divmod(epoch_time, 1000)
fmt_time = time.strftime('%Y-%m-%d', time.gmtime(s))
return fmt_time

在此之后,我们就可以写出针对币安的数据解析类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@AbuDataParseWrap()
class BIANParser(object):
"""示例币类市场数据源解析类,被类装饰器AbuDataParseWrap装饰
专门为BNApi而写的数据解析类
"""

# noinspection PyUnusedLocal
def __init__(self, symbol, json_dict):
"""
:param symbol: 请求的symbol str对象
:param json_dict: 请求返回的json数据
"""

data = json_dict
# 为AbuDataParseWrap准备类必须的属性序列
if len(data) > 0:
# 时间日期序列
self.date = [item[0] for item in data]
# 开盘价格序列
self.open = [item[1] for item in data]
# 最高价格序列
self.high = [item[2] for item in data]
# 最低价格序列
self.low = [item[3] for item in data]
# 收盘价格序列
self.close = [item[4] for item in data]
# 成交量序列
self.volume = [item[5] for item in data]

# 时间日期进行格式转化,转化为如2017-07-26格式字符串
self.date = list(map(lambda date: ABuDateUtil.fmt_epoch(date), self.date))

(3)BNApi

到这里,我们可以完成BNApi这个类了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class BNApi(TCBaseMarket, SupportMixin):
"""Binance数据源"""

K_NET_BASE = 'https://api.binance.com/api/v1/klines'

def __init__(self, symbol):
"""
:param symbol: Symbol类型对象
"""
super(BNApi, self).__init__(symbol)
# 设置数据源解析对象类
self.data_parser_cls = BIANParser
self.coinpair = symbol.symbol_code[2:]

def _support_market(self):
"""只支持币类市场"""
return [EMarketTargetType.E_MARKET_TARGET_TC]

def kline(self, n_folds=2, start=None, end=None, interval = "1d"):
"""日k线接口"""
startunixm = None
endunixm = None
if start is not None:
startunixm = ABuDateUtil.datestr_unixm(start)
if end is not None:
endunixm = ABuDateUtil.datestr_unixm(end)
data = ABuNetWork.get(url=BNApi.K_NET_BASE, params={"symbol":self.coinpair, "interval":interval,
"startTime": startunixm, "endTime": endunixm}, timeout=K_TIME_OUT).json()
kl_df = self.data_parser_cls(self._symbol, data).df
if kl_df is None:
return None
return TCBaseMarket._fix_kline_pd(kl_df,n_folds,start,end)

def minute(self, *args, **kwargs):
"""分钟k线接口"""
raise NotImplementedError('HBApi minute NotImplementedError!')

完成这一切后就可以愉快地使用这个api来获取币安地数据了,不过很快,我们就悲剧地发现币安的api存在诸多问题,例如‘BTCUSDT’这个交易对只能获取17年到18年底的数据。显然,我们开发的这个api并不实用,所以我们可以根据上述模式增加较好的api。具体使用方法如下。

1
2
3
abupy.env.g_market_source = EMarketSourceType.E_MARKET_SOURCE_binance
abupy.env.g_data_fetch_mode = EMarketDataFetchMode.E_DATA_FETCH_NORMAL
print(ABuSymbolPd.make_kl_df(symbol='QsETHUSDT'))

结论

写到这里,我总结下我这个小project的优缺点

优点:

  1. 能够获取币安的加密货币数据,优矿上需要付费才能获得,并且abu原本的火币接口已经无法使用
  2. 后续想要增加接口可以按照此模式进行

缺点:

  1. 该接口无法获取完整数据,不同的交易对能获取的数据时间段并不相同
  2. 只有获取某时间段交易数据的接口,其他接口未开发
  3. 币安接口调用必须翻墙,自身ip需要是海外ip

后续研究

  1. 增加较为实用的api,币安很多api暂时未实现
  2. 利用离线数据进行策略研究