303 lines
11 KiB
Python
303 lines
11 KiB
Python
# encoding: UTF-8
|
||
|
||
'''
|
||
'''
|
||
|
||
from __future__ import print_function
|
||
|
||
import base64
|
||
import hashlib
|
||
import hmac
|
||
import json
|
||
import re
|
||
import urllib
|
||
import zlib
|
||
|
||
from vnpy.api.rest import Request, RestClient
|
||
from vnpy.api.websocket import WebsocketClient
|
||
from vnpy.trader.vtGateway import *
|
||
|
||
REST_HOST = 'https://api.huobipro.com'
|
||
WEBSOCKET_MARKET_HOST = 'wss://api.huobi.pro/ws' # Global站行情
|
||
WEBSOCKET_ASSETS_HOST = 'wss://api.huobi.pro/ws/v1' # 资产和订单
|
||
WEBSOCKET_CONTRACT_HOST = 'wss://www.hbdm.com/ws' # 合约站行情
|
||
|
||
|
||
#----------------------------------------------------------------------
|
||
def _split_url(url):
|
||
"""
|
||
将url拆分为host和path
|
||
:return: host, path
|
||
"""
|
||
m = re.match('\w+://([^/]*)(.*)', url)
|
||
if m:
|
||
return m.group(1), m.group(2)
|
||
|
||
|
||
#----------------------------------------------------------------------
|
||
def createSignature(apiKey, method, host, path, secretKey):
|
||
"""创建签名"""
|
||
sortedParams = (
|
||
("AccessKeyId", apiKey),
|
||
("SignatureMethod", 'HmacSHA256'),
|
||
("SignatureVersion", "2"),
|
||
("Timestamp", datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S'))
|
||
)
|
||
encodeParams = urllib.urlencode(sortedParams)
|
||
|
||
payload = [method, host, path, encodeParams]
|
||
payload = '\n'.join(payload)
|
||
payload = payload.encode(encoding='UTF8')
|
||
|
||
secretKey = secretKey.encode(encoding='UTF8')
|
||
|
||
digest = hmac.new(secretKey, payload, digestmod=hashlib.sha256).digest()
|
||
|
||
signature = base64.b64encode(digest)
|
||
params = dict(sortedParams)
|
||
params["Signature"] = signature
|
||
return params
|
||
|
||
|
||
########################################################################
|
||
class HuobiRestApi(RestClient):
|
||
|
||
def __init__(self, gateway): # type: (VtGateway)->HuobiRestApi
|
||
super(HuobiRestApi, self).__init__()
|
||
self.gateway = gateway
|
||
self.gatewayName = gateway.gatewayName
|
||
|
||
self.apiKey = ""
|
||
self.apiSecret = ""
|
||
self.signHost = ""
|
||
|
||
#----------------------------------------------------------------------
|
||
def sign(self, request):
|
||
request.headers = {
|
||
"User-Agent":
|
||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36"}
|
||
additionalParams = createSignature(self.apiKey,
|
||
request.method,
|
||
self.signHost,
|
||
request.path,
|
||
self.apiSecret)
|
||
if not request.params:
|
||
request.params = additionalParams
|
||
else:
|
||
request.params.update(additionalParams)
|
||
if request.method == "POST":
|
||
request.headers['Content-Type'] = 'application/json'
|
||
return request
|
||
|
||
#----------------------------------------------------------------------
|
||
def connect(self, apiKey, apiSecret, sessionCount=3):
|
||
"""连接服务器"""
|
||
self.apiKey = apiKey
|
||
self.apiSecret = apiSecret
|
||
|
||
host, path = _split_url(REST_HOST)
|
||
self.init(REST_HOST)
|
||
self.signHost = host
|
||
self.start(sessionCount)
|
||
|
||
#----------------------------------------------------------------------
|
||
def qeuryAccount(self):
|
||
self.addRequest('GET', '/v1/account/accounts', self.onAccount)
|
||
|
||
#----------------------------------------------------------------------
|
||
def onAccount(self, data, request): # type: (dict, Request)->None
|
||
pass
|
||
|
||
#----------------------------------------------------------------------
|
||
def cancelWithdraw(self, id):
|
||
self.addRequest('POST',
|
||
"/v1/dw/withdraw-virtual/" + str(id) + "/cancel",
|
||
self.onWithdrawCanceled
|
||
)
|
||
|
||
#----------------------------------------------------------------------
|
||
def onWithdrawCanceled(self, data, request): # type: (dict, Request)->None
|
||
pass
|
||
|
||
|
||
########################################################################
|
||
class HuobiWebsocketApiBase(WebsocketClient):
|
||
|
||
#----------------------------------------------------------------------
|
||
def __init__(self, gateway):
|
||
"""Constructor"""
|
||
super(HuobiWebsocketApiBase, self).__init__()
|
||
|
||
self.gateway = gateway
|
||
self.gatewayName = gateway.gatewayName
|
||
|
||
self.apiKey = ''
|
||
self.apiSecret = ''
|
||
self.signHost = ''
|
||
self.path = ''
|
||
|
||
#----------------------------------------------------------------------
|
||
def connect(self, apiKey, apiSecret, url):
|
||
""""""
|
||
self.apiKey = apiKey
|
||
self.apiSecret = apiSecret
|
||
|
||
host, path = _split_url(url)
|
||
|
||
self.init(url)
|
||
self.signHost = host
|
||
self.path = path
|
||
self.start()
|
||
|
||
#----------------------------------------------------------------------
|
||
def login(self):
|
||
params = {
|
||
'op': 'auth',
|
||
}
|
||
params.update(
|
||
createSignature(self.apiKey,
|
||
'GET',
|
||
self.signHost,
|
||
self.path,
|
||
self.apiSecret)
|
||
)
|
||
return self.sendPacket(params)
|
||
|
||
#----------------------------------------------------------------------
|
||
def onLogin(self, packet):
|
||
pass
|
||
|
||
#----------------------------------------------------------------------
|
||
@staticmethod
|
||
def unpackData(data):
|
||
return json.loads(zlib.decompress(data, 31))
|
||
|
||
#----------------------------------------------------------------------
|
||
def onPacket(self, packet):
|
||
"""
|
||
这里我新增了一个onHuobiPacket的函数,也可以让子类重写这个函数,然后调用super.onPacket
|
||
"""
|
||
if 'ping' in packet:
|
||
self.sendPacket({'pong': packet['ping']})
|
||
return
|
||
|
||
# todo: use another error handing method
|
||
if 'err-msg' in packet:
|
||
return self.onHuobiErrorPacket(packet)
|
||
|
||
if "op" in packet and packet["op"] == "auth":
|
||
return self.onLogin(packet)
|
||
|
||
self.onHuobiPacket(packet)
|
||
|
||
#----------------------------------------------------------------------
|
||
def onHuobiPacket(self, packet): # type: (dict)->None
|
||
pass
|
||
|
||
#----------------------------------------------------------------------
|
||
def onHuobiErrorPacket(self, packet): # type: (dict)->None
|
||
print("error : {}".format(packet))
|
||
|
||
|
||
########################################################################
|
||
class HuobiAssetsWebsocketApi(HuobiWebsocketApiBase):
|
||
|
||
def connect(self, apiKey, apiSecret, host=WEBSOCKET_ASSETS_HOST):
|
||
"""
|
||
这里我使用重写connect,添加了默认参数。这样写感觉~~不太好~~,不过目前想到的比较好的方式就是这样了
|
||
虽然在Python中可以直接把这个connect()写成不接收host和path的形式,但是PyCharm会提示重载错误,所以不接收host和path似乎不太好?
|
||
|
||
我觉得最好的写法应该是这个函数不接收host和path。同时为了让PyCharm不提示重载错误(减少歧义),应该给
|
||
HuobiWebsocketApiBase.connect起另外一个名字。
|
||
"""
|
||
return super(HuobiAssetsWebsocketApi, self). \
|
||
connect(apiKey, apiSecret, host)
|
||
|
||
#----------------------------------------------------------------------
|
||
def onConnected(self):
|
||
self.login()
|
||
|
||
#----------------------------------------------------------------------
|
||
def subscribeAccount(self):
|
||
"""
|
||
:param symbol: str ethbtc, ltcbtc, etcbtc, bchbtc
|
||
:param period: str 1min, 5min, 15min, 30min, 60min, 1day, 1mon, 1week, 1year
|
||
"""
|
||
self.sendPacket({
|
||
"op": "sub",
|
||
"cid": "any thing you want",
|
||
"topic": "accounts"
|
||
})
|
||
|
||
#----------------------------------------------------------------------
|
||
def onHuobiPacket(self, packet): # type: (dict)->None
|
||
if 'op' in packet:
|
||
if packet['op'] == 'sub':
|
||
timestamp = packet['ts']
|
||
topic = packet['topic']
|
||
"""
|
||
"data": {
|
||
"event": "order.match|order.place|order.refund|order.cancel|order.fee-refund|margin.transfer|margin.loan|margin.interest|margin.repay|other",
|
||
"list": [
|
||
{
|
||
"account-id": 419013,
|
||
"currency": "usdt",
|
||
"type": "trade",
|
||
"balance": "500009195917.4362872650"
|
||
},
|
||
{
|
||
"account-id": 419013,
|
||
"currency": "btc",
|
||
"type": "frozen",
|
||
"balance": "9786.6783000000"
|
||
}
|
||
]
|
||
}
|
||
"""
|
||
pass
|
||
|
||
|
||
########################################################################
|
||
class HuobiMarketWebsocketApi(HuobiWebsocketApiBase):
|
||
|
||
#----------------------------------------------------------------------
|
||
def connect(self, apiKey, apiSecret, host=WEBSOCKET_MARKET_HOST):
|
||
"""
|
||
这里我使用重写connect,添加了默认参数。这样写感觉~~不太好~~,不过目前想到的比较好的方式就是这样了
|
||
虽然在Python中可以直接把这个connect()写成不接收host和path的形式,但是PyCharm会提示重载错误,所以不接收host和path似乎不太好?
|
||
|
||
我觉得最好的写法应该是这个函数不接收host和path。同时为了让PyCharm不提示重载错误(减少歧义),应该给
|
||
HuobiWebsocketApiBase.connect起另外一个名字。
|
||
"""
|
||
return super(HuobiMarketWebsocketApi, self). \
|
||
connect(apiKey, apiSecret, host)
|
||
|
||
#----------------------------------------------------------------------
|
||
def subscribeKLine(self, symbol, period): # type:(str, str)->None
|
||
"""
|
||
:param symbol: str ethbtc, ltcbtc, etcbtc, bchbtc
|
||
:param period: str 1min, 5min, 15min, 30min, 60min, 1day, 1mon, 1week, 1year
|
||
:return:
|
||
"""
|
||
self.sendPacket({
|
||
"sub": "market." + symbol + ".kline." + period,
|
||
"id": "any thing you want"
|
||
})
|
||
|
||
#----------------------------------------------------------------------
|
||
def onHuobiPacket(self, packet): # type: (dict)->None
|
||
# code for test purpose only
|
||
if 'ch' in packet:
|
||
if packet['ch'] == 'market.btcusdt.kline.1min':
|
||
timestamp = packet['ts']
|
||
data = packet['tick']
|
||
id = data['id']
|
||
amount = data['amount']
|
||
count = data['count']
|
||
open = data['open']
|
||
close = data['close']
|
||
low = data['low']
|
||
high = data['high']
|
||
vol = data['vol']
|
||
pass
|