解析:tushare 数字货币接口


(daonaldo) #1

昨晚看到 tushare 更新,新加入了数字货币接口,米哥直接在推文中写道:

程序采用了最简洁的写法,只有一个代码文件,三个交易所4个接口11类数据只用了240行代码左右,其中文件注释和程序配置又占了一半代码。所以写的还算简洁可用,python初学者或者数据接口设计人员有兴趣的话,可以翻看一下代码,欢迎各位拍砖吐槽。

想到自己在七月份也写了一套接口(代码质量太差了,只敢自己玩玩),也因为最近几天一直在看 MySQL 的书想换换胃口,于是到 tushare 看了下源码 ,真的是不知道碾压自己多少倍。废话不多说了,先来看代码吧。

注意

这篇解析并不涉及特别细节的部分,而是从比较大的方向上来学习写代码的思路和实现步骤。另外,这里所涉及的一些背景知识,也不一一做介绍了,大家可以参考 《比特币程序化交易入门(2):REST API》,我最初也是从这里开始折腾接口的。

Part 1

这块就不说了:

#!/usr/bin/env python
# -*- coding:utf-8 -*- 
"""
数字货币行情数据
Created on 2017年9月9日
@author: Jimmy Liu
@group : waditu
@contact: jimmysoa@sina.cn
"""

Part 2

之前我自己写接口的时候,全是手写的 Raise,还并不知道 traceback 这个东西,简单看了下文档,应该是用来“追溯报错”的。另外一个惊奇的地方是,原来可以 urllib 和 urllib2 一起用,这也是之前根本没想到的(当时我只用到了 urllib):

import pandas as pd
import traceback
import time
import json
try:
    from urllib.request import urlopen, Request
except ImportError:
    from urllib2 import urlopen, Request

Part 3

不知道大家之前有没有手写过相关的接口,至少这篇代码中,让我收获最多的就是上面这两段用于存放各种参数的 dict 了。为什么这么说呢,因为我之前所写的接口中,完全把这些参数一个个拆开来,塞到所写的各个函数:比如 kline 和 tick 中的参数,只是略有不同,但我还是分别定义了两回。这样一来,虽然在效率上并没有什么损失,但代码整体的可读性降低了不少。

URL = {
       "hb": {
              "rt"         : 'http://api.huobi.com/staticmarket/ticker_%s_json.js',
              "kline"      : 'http://api.huobi.com/staticmarket/%s_kline_%s_json.js?length=%s',
              "snapshot"   : 'http://api.huobi.com/staticmarket/depth_%s_%s.js',
              "tick"       : 'http://api.huobi.com/staticmarket/detail_%s_json.js',
              },
       "ok": {
              "rt"         : 'https://www.okcoin.cn/api/v1/ticker.do?symbol=%s_cny',
              "kline"      : 'https://www.okcoin.cn/api/v1/kline.do?symbol=%s_cny&type=%s&size=%s',
              "snapshot"   : 'https://www.okcoin.cn/api/v1/depth.do?symbol=%s_cny&merge=&size=%s',
              "tick"       : 'https://www.okcoin.cn/api/v1/trades.do?symbol=%s_cny',
              },
       'chbtc': {
                "rt"       : 'http://api.chbtc.com/data/v1/ticker?currency=%s_cny',
                "kline"    : 'http://api.chbtc.com/data/v1/kline?currency=%s_cny&type=%s&size=%s',
                "snapshot" : 'http://api.chbtc.com/data/v1/depth?currency=%s_cny&size=%s&merge=',
                "tick"     : 'http://api.chbtc.com/data/v1/trades?currency=%s_cny',
                }
       }

KTYPES = {
          "D": {
                "hb"       : '100',
                'ok'       : '1day',
                'chbtc'    : '1day',
                },
          "W": {
                "hb"       : '200',
                'ok'       : '1week',
                'chbtc'    : '1week',
                },
          "M": {
                "hb"       : '300',
                "ok"       : '',
                "chbtc"    : '',
                },
          "1MIN": {
                   "hb"    : '001',
                   'ok'    : '1min',
                   'chbtc' : '1min',
                   },
          "5MIN": {
                   "hb"    : '005',
                   'ok'    : '5min',
                   'chbtc' : '5min',
                   },
          "15MIN": {
                   "hb"    : '015',
                   'ok'    : '15min',
                   'chbtc' : '15min',
                   },
          "30MIN": {
                   "hb"    : '030',
                   'ok'    : '30min',
                   'chbtc' : '30min',
                   },
          "60MIN": {
                   "hb"    : '060',
                   'ok'    : '1hour',
                   'chbtc' : '1hour',
                   },
          } 

索性拿我自己所写的第一版接口作为反面教材,可以看到我的代码中,所有参数都是不统一的,并且还有一些更细节的参数是在函数内部定义的,可读性很低(所以说只敢给自己玩玩)。至于米哥这两段 dict 的用处,到下面就很自然地用上了,继续看代码吧。

# 注意这是我写的反面教材
def ticker(item):
    item = str(item)
    url='https://www.okcoin.cn/api/v1/ticker.do?symbol=ltc_cny'
    response = urllib.request.urlopen(url,timeout=3)#打开连接,timeout为请求超时时间
    result=response.read().decode('utf-8')#返回结果解码
    json_data=json.loads(result)
    return(json_data['ticker'][item])

# 注意这是我写的反面教材
def kline(freq):
    freq = str(freq)
    url = "https://www.okcoin.cn/api/v1/kline.do?symbol=ltc_cny&type=" + freq
    response = urllib.request.urlopen(url,timeout=3)#打开连接,timeout为请求超时时间
    result=response.read().decode('utf-8')#返回结果解码
    json_data=json.loads(result)
    return json_data

Part 4

coins_tick 函数的注释主要是用给大家看一下各个参数怎么用,以及会返回哪些东西,实际上主逻辑是在 _get_data 中完成的。而这里也可以看到,在上面两段 dict 定义好之后,后面的获取数据就很方便了: URL[broker][‘rt’] % (code) 这句用得很灵性。为什么要用 _get_data,然后外面重新套一层函数呢,是为了提高代码的复用性和可读性,可以看到 _get_data 在后面的函数中也被反复用到。

def coins_tick(broker='hb', code='btc'):
    """
    实时tick行情
    params:
    ---------------
    broker: hb:火币
            ok:okCoin
            chbtc:中国比特币
    code: hb:btc,ltc
        ----okcoin---
        btc_cny:比特币    ltc_cny:莱特币    eth_cny :以太坊     etc_cny :以太经典    bcc_cny :比特现金 
        ----chbtc----
        btc_cny:BTC/CNY
        ltc_cny :LTC/CNY
        eth_cny :以太币/CNY
        etc_cny :ETC币/CNY
        bts_cny :BTS币/CNY
        eos_cny :EOS币/CNY
        bcc_cny :BCC币/CNY
        qtum_cny :量子链/CNY
        hsr_cny :HSR币/CNY
    return:json
    ---------------
    hb:
    {
    "time":"1504713534",
    "ticker":{
        "symbol":"btccny",
        "open":26010.90,
        "last":28789.00,
        "low":26000.00,
        "high":28810.00,
        "vol":17426.2198,
        "buy":28750.000000,
        "sell":28789.000000
        }
    }
    ok:
    {
    "date":"1504713864",
    "ticker":{
        "buy":"28743.0",
        "high":"28886.99",
        "last":"28743.0",
        "low":"26040.0",
        "sell":"28745.0",
        "vol":"20767.734"
        }
    }
    chbtc: 
        {
         u'date': u'1504794151878',
         u'ticker': {
             u'sell': u'28859.56', 
             u'buy': u'28822.89', 
             u'last': u'28859.56', 
             u'vol': u'2702.71', 
             u'high': u'29132', 
             u'low': u'27929'
         }
        }

    """
    return _get_data(URL[broker]['rt'] % (code))

Part 5

具体看一个 coins_bar 函数,剩下的另外两个函数也是同样的思路,就不赘述了。这里学习点在于 pandas 的运用:我之前是直接返回 dict 的,或者更暴力一点,返回 dict 中的值,然后在真正用数据的时候再上 pandas。

def coins_bar(broker='hb', code='btc', ktype='D', size='2000'):
    """
            获取各类k线数据
    params:
    broker:hb,ok,chbtc
    code:btc,ltc,eth,etc,bcc
    ktype:D,W,M,1min,5min,15min,30min,60min
    size:<2000
    return DataFrame: 日期时间,开盘价,最高价,最低价,收盘价,成交量
    """
    try:
        js = _get_data(URL[broker]['kline'] % (code, KTYPES[ktype.strip().upper()][broker], size))
        # 这里就用到了 _get_data 以及之前定义的两个 dict
        if js is None:
            return js
        if broker == 'chbtc':
            js = js['data']
        df = pd.DataFrame(js, columns=['DATE', 'OPEN', 'HIGH', 'LOW', 'CLOSE', 'VOL'])
        # 这里用 pandas(这也是我之前所没有想到的),这样一来,拿出来的数据就更容易处理了
        if broker == 'hb':
        # 处理 df['DATE'],各交易所给出的数据格式不同
            if ktype.strip().upper() in ['D', 'W', 'M']:
                df['DATE'] = df['DATE'].apply(lambda x: x[0:8])
            else:
                df['DATE'] = df['DATE'].apply(lambda x: x[0:12])
        else:
            df['DATE'] = df['DATE'].apply(lambda x: int2time(x / 1000))
        if ktype.strip().upper() in ['D', 'W', 'M']:
            df['DATE'] = df['DATE'].apply(lambda x: str(x)[0:10])
        # 最后处理为 Timestamp 格式
        df['DATE'] = pd.to_datetime(df['DATE'])
        return df
    except Exception:
        print(traceback.print_exc())

Part 6

这两个函数不细说了,不过里面对 pandas 的运用很值得体会。

def coins_snapshot(broker='hb', code='btc', size='5'):
    """
            获取实时快照数据
    params:
    broker:hb,ok,chbtc
    code:btc,ltc,eth,etc,bcc
    size:<150
    return Panel: asks,bids
    """
    try:
        js = _get_data(URL[broker]['snapshot'] % (code, size))
        if js is None:
            return js
        if broker == 'hb':
            timestr = js['ts']
            timestr = int2time(timestr / 1000)
        if broker == 'ok':
            timestr = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 
        if broker == 'chbtc':
            timestr = js['timestamp']
            timestr = int2time(timestr)
        asks = pd.DataFrame(js['asks'], columns = ['price', 'vol'])
        bids = pd.DataFrame(js['bids'], columns = ['price', 'vol'])
        asks['time'] = timestr
        bids['time'] = timestr
        djs = {"asks": asks, "bids": bids}
        pf = pd.Panel(djs)
        return pf
    except Exception:
        print(traceback.print_exc())


def coins_trade(broker='hb', code='btc'):
    """
    获取实时交易数据
    params:
    -------------
    broker: hb,ok,chbtc
    code:btc,ltc,eth,etc,bcc

    return:
    ---------------
    DataFrame
    'tid':order id
    'datetime', date time 
    'price' : trade price
    'amount' : trade amount
    'type' : buy or sell
    """
    js = _get_data(URL[broker]['tick'] % code)
    if js is None:
        return js
    if broker == 'hb':
        df = pd.DataFrame(js['trades'])
        df = df[['id', 'ts', 'price', 'amount', 'direction']]
        df['ts'] = df['ts'].apply(lambda x: int2time(x / 1000))
    if broker == 'ok':
        df = pd.DataFrame(js)
        df = df[['tid', 'date_ms', 'price', 'amount', 'type']]
        df['date_ms'] = df['date_ms'].apply(lambda x: int2time(x / 1000))
    if broker == 'chbtc':
        df = pd.DataFrame(js)
        df = df[['tid', 'date', 'price', 'amount', 'type']]
        df['date'] = df['date'].apply(lambda x: int2time(x))
    df.columns = ['tid', 'datetime', 'price', 'amount', 'type']
    return df

Part 7

看,这就是我所写不出来的那种高复用性函数:

def _get_data(url):
    try:
    # 这里用的是 try,我觉得也是为了谨慎起见    
        request = Request(url)
        lines = urlopen(request, timeout = 10).read()
        if len(lines) < 50: #no data
        # 看这里,判断了无数据返回的情况
            return None
        js = json.loads(lines.decode('GBK'))
        return js
    except Exception:
        print(traceback.print_exc())
        # 我以前用的是 Raise

Part 8

处理时间格式的小函数:

def int2time(timestamp):
    value = time.localtime(timestamp)
    dt = time.strftime('%Y-%m-%d %H:%M:%S', value)
    return dt

最后

走马观花一般看完了两百多行代码,很清楚地看到了自己和工程代码之间的差距。再多说两句,写完数据接口之后,我一口气把诸如下单、撤单、账户查询之类的接口也写掉了,随后迫不及待地写了个实盘脚本,最终以十个交易日亏损十三个点告终(全亏在了手续费上)。再后来,暑假的时候,和我一起在实习的南大小哥开了台服务器,写了脚本用来存实时数据,想要以后做回测用。如果有对这方面感兴趣的同学,不妨私信我,交流一下。

update

有点小遗憾。