├── _config.yml ├── QRCode.png ├── requirements.txt ├── huobitrade ├── __init__.py ├── extra │ ├── log_handler.md │ ├── loggging_handler.py │ └── rpc.py ├── main.py ├── handler.py ├── utils.py ├── datatype.py ├── core.py └── service.py ├── .travis.yml ├── examples ├── auth_test.py ├── run_demo.py ├── strategy_demo.py └── example.py ├── HBVisual ├── templates │ ├── temp.html │ └── index.html └── VisualApp.py ├── LICENSE ├── setup.py └── README.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-tactile -------------------------------------------------------------------------------- /QRCode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadrianl/huobi/HEAD/QRCode.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pymongo 2 | websocket-client>=0.53 3 | requests 4 | requests-futures 5 | python-dateutil 6 | pandas 7 | ecdsa 8 | click -------------------------------------------------------------------------------- /huobitrade/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2018/5/29 0029 14:02 4 | # @Author : Hadrianl 5 | # @File : __init__.py 6 | # @Contact : 137150224@qq.com 7 | 8 | from .utils import setKey, setUrl, logger 9 | from .service import HBRestAPI, HBWebsocket, HBDerivativesRestAPI 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | - "3.7" 5 | install: 6 | - pip install -r requirements.txt 7 | 8 | script: 9 | - "python setup.py install" 10 | 11 | #deploy: 12 | # provider: pypi 13 | # user: Hadrianl # pypi 用户名 14 | # password: 15 | # secure: "Your encrypted password" 16 | # on: 17 | # python: 3.6 18 | # tags: true 19 | # branch: master 20 | # distributions: "sdist bdist_wheel" 21 | -------------------------------------------------------------------------------- /examples/auth_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2018/9/25 0025 12:39 4 | # @Author : Hadrianl 5 | # @File : auth_test.py 6 | # @Contact : 137150224@qq.com 7 | 8 | """ 9 | 该例子仅限于用来测试鉴权接口是否能鉴权成功 10 | """ 11 | 12 | from huobitrade.service import HBWebsocket 13 | from huobitrade import setKey, logger 14 | logger.setLevel('DEBUG') 15 | setKey('access_key', 'secret_key') 16 | 17 | auhb = HBWebsocket(auth=True) 18 | 19 | auhb.after_auth(auhb.sub_accounts) 20 | 21 | @auhb.after_auth 22 | def init_sub(): 23 | auhb.sub_accounts() 24 | auhb.sub_orders() 25 | 26 | auhb.run() 27 | -------------------------------------------------------------------------------- /HBVisual/templates/temp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

11 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/run_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2018/10/22 0022 13:33 4 | # @Author : Hadrianl 5 | # @File : run_demo.py 6 | # @Contact : 137150224@qq.com 7 | 8 | 9 | from huobitrade import HBRestAPI 10 | from huobitrade.core import _HBWS, _AuthWS 11 | 12 | def init(restapi:HBRestAPI, ws:_HBWS, auth_ws:_AuthWS): 13 | print(restapi.get_timestamp()) 14 | print(ws.sub_depth('omgeth')) 15 | print(auth_ws.sub_orders()) 16 | 17 | def handle_depth(msg): 18 | # print(msg) 19 | ... 20 | 21 | def handle_account(msg): 22 | # print(msg) 23 | ... 24 | 25 | handle_func = {'market.omgeth.depth.step0': handle_depth, 26 | 'accounts': handle_account} 27 | 28 | def schedule(restapi:HBRestAPI, ws:_HBWS, auth_ws:_AuthWS, *, interval=10): 29 | print(interval) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Hadrianl_yang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /huobitrade/extra/log_handler.md: -------------------------------------------------------------------------------- 1 | # EXTRA 2 | - 此目录用于增加一些与交易策略运营相关的模块 3 | 4 | ## Logging Handler 5 | - 额外的日志handler 6 | ### WeChat Handler 7 | - 增加一个微信的日志handler,推送日志消息到设定的群聊中 8 | ```python 9 | from huobitrade.extra.logging_handler import WeChatHandler 10 | from huobitrade.utils import logger 11 | wechat_handler = WeChatHandler('chatroom name') # 默认的level是TRADE_INFO(60),enableCmdQR用于调整二维码,详见itchat 12 | wechat_handler.run() 13 | logger.addHandler(wechat_handler) 14 | logger.log(TRADE_INFO,'wechat_handler testing!') 15 | @wechat_handler.client.msg_register(TEXT, isGroupChat=True) 16 | def reply(msg): 17 | if msg['isAt']: 18 | wechat_handler.send_log(f'HELLO {msg["FromUserName"}') 19 | ``` 20 | 21 | ## RPC 22 | - 远程调用 23 | ## RPCServer 24 | - 实现了一个订阅端口和请求端口,已封装在RPCServerHandler里面 25 | ```python 26 | from huobitrade import logger 27 | logger.setLevel('DEBUG') 28 | from huobitrade import setKey 29 | setKey("", "") 30 | from huobitrade.service import HBWebsocket, HBRestAPI 31 | from huobitrade.handler import RPCServerHandler 32 | import time 33 | ws = HBWebsocket() 34 | api = HBRestAPI(get_acc=True) 35 | ws.run() 36 | time.sleep(1) 37 | ws.sub_tick('omgeth') 38 | rpc = RPCServerHandler() 39 | rpc.register_func('getTime', api.get_timestamp) # 把函数注册到rpcFunc里面,就可以实现远程调用 40 | ws.register_handler(rpc) 41 | ``` 42 | 43 | ## 44 | - rpc的客户端,初始化,订阅topic,开启订阅线程, 最后交由handle函数处理 45 | - 调用远程已经注册的函数 46 | ```python 47 | from huobitrade.extra.rpc import RPCClient 48 | from huobitrade import logger 49 | logger.setLevel('DEBUG') 50 | rpcclient = RPCClient('localhost', 'localhost') # 地址与端口 51 | rpcclient.subscribe('') # 订阅topic,如果是''的话,则接受所有topic 52 | rpcclient.startSUB() # 开启订阅线程,用handle函数来处理,需要继承RPCClient重载handle函数实现 53 | rpcclient.handle = lambda topic, msg:print(topic, msg) 54 | rpcclient.getTime() # Server端已经注册的函数,可以直接调用 55 | 56 | ``` -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2018/5/29 0029 14:10 4 | # @Author : Hadrianl 5 | # @File : setup.py 6 | # @Contact : 137150224@qq.com 7 | 8 | from setuptools import setup, find_packages 9 | __version__ = "0.5.6" 10 | 11 | with open("README.md", "r", encoding='utf-8') as rm: 12 | long_description = rm.read() 13 | 14 | requires = ['websocket-client>=0.53', 15 | 'requests', 16 | 'pymongo', 17 | 'pyzmq', 18 | 'pandas', 19 | 'requests-futures', 20 | 'ecdsa', 21 | 'click'] 22 | 23 | hb_packages = ['huobitrade', 'huobitrade/extra'] 24 | 25 | setup(name='huobitrade', 26 | version=__version__, 27 | description='HuoBi Trading Framwork(python)', 28 | long_description=long_description, 29 | long_description_content_type="text/markdown", 30 | author='Hadrianl', 31 | author_email='137150224@qq.com', 32 | url='https://hadrianl.github.io/huobi/', 33 | packages=hb_packages, 34 | entry_points={ 35 | "console_scripts": [ 36 | "huobitrade = huobitrade.main:entry_point" 37 | ] 38 | }, 39 | classifiers=( 40 | "Development Status :: 5 - Production/Stable", 41 | "Natural Language :: Chinese (Simplified)", 42 | "Operating System :: MacOS", 43 | "Operating System :: Microsoft :: Windows", 44 | "Operating System :: POSIX :: Linux", 45 | "Programming Language :: Python :: Implementation :: CPython", 46 | "Programming Language :: Python :: 3.6", 47 | "Programming Language :: Python :: 3.7", 48 | "License :: OSI Approved :: MIT License", 49 | "Topic :: Software Development :: Libraries :: Python Modules", 50 | "Topic :: Software Development :: Version Control :: Git" 51 | ), 52 | install_requires=requires) -------------------------------------------------------------------------------- /examples/strategy_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | @author:Hadrianl 6 | 7 | 该demo主要用于展示一个简单的策略应该如何编写,其中核心部分是实现一个handler 8 | 9 | 10 | """ 11 | from huobitrade.handler import BaseHandler 12 | from huobitrade.utils import logger 13 | import pymongo as pmo 14 | 15 | 16 | class DemoHandler(BaseHandler): 17 | def __init__(self, symbol, _ktype='1min'): 18 | self._type = _ktype 19 | self._symbol = symbol 20 | self._topic = f'market.{self._symbol}.kline.{self._type}' 21 | BaseHandler.__init__(self, 'market_maker', self._topic, latest=True) # 在handle需要执行比较长时间的情况下,最好用latest,保持获取最新行情,对于在handle处理过程中推送的行情忽略 22 | self._db = pmo.MongoClient('localhost', 27017).get_database('HuoBi') 23 | 24 | def into_db(self, data, collection): 25 | 26 | collection = self._db.get_collection(collection) 27 | collection.create_index('id') 28 | try: 29 | collection.replace_one({'id': data['id']}, data, upsert=True) 30 | except Exception as e: 31 | logger.error(f'<数据>插入交易深度数据错误{e}') 32 | 33 | def handle(self, topic, msg): # 核心handle函数!!! 34 | data = msg.get('tick') 35 | symbol = topic.split('.')[1] 36 | ts = msg.get('ts') 37 | self.into_db(data, topic) 38 | logger.info(msg) 39 | 40 | buy_cond = False 41 | sell_cond = False 42 | 43 | if buy_cond: 44 | buy_amount = ... #买入量 45 | buy_price = ... # 买入价 46 | 47 | api.send_order(api.acc_id, buy_amount, symbol, 'buy-limit', buy_price) 48 | 49 | if sell_cond: 50 | sell_amount = ... 51 | sell_price = ... 52 | api.send_order(api.acc_id, sell_amount, symbol, 'sell-limit', sell_price) 53 | 54 | 55 | if __name__ == '__main__': 56 | from huobitrade.service import HBWebsocket, HBRestAPI 57 | from huobitrade import setUrl, setKey 58 | 59 | setKey('', '') 60 | # setUrl('https://api.huobi.pro', 'https://api.huobi.pro') 61 | ws = HBWebsocket('api.huobi.br.com') # 生产环境请不要用api.huobi.br.com 62 | api = HBRestAPI(get_acc=True) # get_acc为true,初始化时候会获取acount_id中的第一个id并赋值给acc_id属性 63 | 64 | 65 | @ws.after_open # 连接成功后会调用此函数,一般在这个位置进行初始化订阅 66 | def sub_depth(): 67 | ws.sub_kline('dcreth', '1min') 68 | 69 | ws.run() 70 | 71 | demo_handler = DemoHandler('dcreth') 72 | ws.register_handler(demo_handler) 73 | -------------------------------------------------------------------------------- /huobitrade/extra/loggging_handler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2018/6/15 0015 14:01 4 | # @Author : Hadrianl 5 | # @File : loggging_handler.py 6 | # @Contact : 137150224@qq.com 7 | 8 | from logging import Handler, Formatter 9 | from ..datatype import HBMarket 10 | try: 11 | import itchat 12 | from itchat.content import TEXT 13 | except ImportError: 14 | print(f'Module itchat not found.WeChatHandler is not available.') 15 | 16 | TRADE_INFO = 60 17 | 18 | class WeChatHandler(Handler): 19 | def __init__(self, chatroom=None, level=TRADE_INFO, enableCmdQR=True): 20 | super(WeChatHandler, self).__init__(level) 21 | self._CmdQR = enableCmdQR 22 | self._chatroom = chatroom 23 | self.client = itchat.new_instance() 24 | formatter = Formatter('%(asctime)s - %(levelname)s - %(message)s') 25 | self.setFormatter(formatter) 26 | 27 | def run(self): 28 | self.client.auto_login(hotReload=True, loginCallback=self._loginCallback, exitCallback=self._exitCallback, enableCmdQR=self._CmdQR) 29 | self.client.run(debug=False, blockThread=False) 30 | 31 | def stop(self): 32 | self.send_log('暂停接收HUOBI信息') 33 | self.client.logout() 34 | 35 | def _loginCallback(self): 36 | try: 37 | self.log_receiver = self.client.search_chatrooms(name=self._chatroom)[0]['UserName'] 38 | except Exception as e: 39 | print(e) 40 | self.log_receiver = None 41 | self.send_log('开始接收HUOBI信息') 42 | 43 | def _exitCallback(self): 44 | print(f'微信logger->{self.log_receiver}已终止') 45 | 46 | def init_reply(self): 47 | d = HBMarket() 48 | 49 | @self.client.msg_register(TEXT, isGroupChat=True) 50 | def reply(msg): 51 | if msg['isAt']: 52 | try: 53 | topic = msg['Text'].split('.') 54 | v = d 55 | for t in topic: 56 | v = getattr(v, t) 57 | self.send_log(f'{v}') 58 | except Exception as e: 59 | print(e) 60 | 61 | def send_log(self, msg): 62 | self.client.send(msg, self.log_receiver) 63 | 64 | def emit(self, record): 65 | try: 66 | msg = self.format(record) 67 | self.send_log(msg) 68 | except Exception as e: 69 | print(e) 70 | -------------------------------------------------------------------------------- /examples/example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2018/6/8 0008 13:47 4 | # @Author : Hadrianl 5 | # @File : example.py 6 | # @Contact : 137150224@qq.com 7 | 8 | """ 9 | 该demo是用于模拟较为复杂的交易策略, 10 | 同时用上了普通行情websocket,鉴权websocket与restfulapi, 11 | 包括展示如何初始化订阅,以及如果注册handler等 12 | 13 | """ 14 | 15 | 16 | from huobitrade.service import HBWebsocket, HBRestAPI 17 | from huobitrade.handler import BaseHandler 18 | from huobitrade import setKey, logger 19 | from functools import partial 20 | import time 21 | # logger.setLevel('DEBUG') 22 | setKey('access_key', 'secret_key') 23 | 24 | class MyHandler(BaseHandler): 25 | def __init__(self, topic, *args, **kwargs): 26 | BaseHandler.__init__(self, 'just Thread name', topic) 27 | 28 | def handle(self, topic, msg): # 实现handle来处理websocket推送的msg 29 | print(topic) 30 | 31 | # 初始化restapi 32 | restapi = HBRestAPI(get_acc=True) 33 | print(restapi.get_accounts()) # 请求账户 34 | 35 | # 构造异步请求 36 | rep1 = restapi.get_timestamp(_async=True) 37 | rep2 = restapi.get_all_last_24h_kline(_async=True) 38 | result = restapi.async_request([rep1, rep2]) # 一起发起请求 39 | for r in result: 40 | print(r) 41 | 42 | 43 | # 初始化多个ws 44 | auhb = HBWebsocket(auth=True) 45 | hb = HBWebsocket() 46 | hb2 = HBWebsocket() 47 | 48 | # 注册鉴权或连接后的订阅行为, 断线重连后依然会重新订阅 49 | auhb.after_auth(auhb.sub_accounts) 50 | hb.after_open(partial(hb.sub_kline, 'dcreth', '1min')) 51 | hb2.after_open(partial(hb2.sub_depth, 'dcreth')) 52 | 53 | # 把handler与handle_func注册进相应的ws 54 | handler = MyHandler(None) # topic为None即订阅全部topic 55 | auhb.register_handler(handler) 56 | hb.register_handler(handler) 57 | hb2.register_handler(handler) 58 | @hb2.register_handle_func('market.dcreth.depth.step0') 59 | def print_msg(msg): 60 | print('handle_func', msg) 61 | 62 | # 开始连接ws 63 | auhb.run() 64 | hb.run() 65 | hb2.run() 66 | 67 | time.sleep(5) 68 | 69 | # 取消订阅 70 | auhb.unsub_accounts() 71 | hb.unsub_kline('dcreth', '1min') 72 | hb2.unsub_depth('dcreth') 73 | 74 | # 注销handler与handle_func 75 | auhb.unregister_handler(handler) 76 | hb.unregister_handler(handler) 77 | hb2.unregister_handler(handler) 78 | hb2.unregister_handle_func(print_msg, 'market.dcreth.depth.step0') 79 | 80 | # 添加req请求的回调并发起请求 81 | @hb.register_onRsp('market.dcreth.depth.step0') 82 | def req1_callback(msg): 83 | print('req1', msg) 84 | 85 | @hb.register_onRsp('market.dcreth.depth.step0') # 可以添加多个请求回调 86 | def req2_callback(msg): 87 | print('req2', msg) 88 | topic, ReqId = hb.req_depth('dcreth',_id='ReqId') 89 | print(topic, ReqId) 90 | 91 | time.sleep(2) 92 | # 一次性将所有请求回调注销 93 | hb.unregister_onRsp('market.dcreth.depth.step0') 94 | 95 | time.sleep(5) 96 | 97 | 98 | # 关闭停止ws 99 | hb.stop() 100 | hb2.stop() 101 | auhb.stop() 102 | 103 | # hb.sub_kline('ethbtc', '1min') 104 | # hb.req_kline('ethbtc', '1min') 105 | # handler = DBHandler() 106 | # hb.register_handler(handler) 107 | 108 | # @hb.register_handle_func('market.dcreth.kline.1min') 109 | # def handle(msg): 110 | # print('handle:', msg) 111 | 112 | # api = HBRestAPI() 113 | # print(api.get_timestamp()) -------------------------------------------------------------------------------- /HBVisual/VisualApp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2018/8/7 0007 17:17 4 | # @Author : Hadrianl 5 | # @File : VisualApp 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | import gevent.monkey 18 | gevent.monkey.patch_all() 19 | 20 | from flask import Flask, render_template, g, session 21 | from flask_socketio import SocketIO 22 | import json 23 | from huobitrade import HBRestAPI, setKey, HBWebsocket, logger 24 | import json 25 | setKey('', '') 26 | 27 | app = Flask(__name__) 28 | socketio = SocketIO(app) 29 | # logger.setLevel('DEBUG') 30 | 31 | @app.route('/') 32 | def backtest(): 33 | return render_template('index.html') 34 | 35 | @app.route('/data/get_symbols') 36 | def get_symbols(): 37 | api = HBRestAPI() 38 | symbols = api.get_symbols()['data'] 39 | return json.dumps(symbols) 40 | 41 | @socketio.on('connect', namespace='/ws') 42 | def connect(): 43 | api = session.__dict__.setdefault('api', HBRestAPI(get_acc=True)) 44 | ws = session.__dict__.setdefault('ws', HBWebsocket()) 45 | auws = session.__dict__.setdefault('auws', HBWebsocket(auth=True)) 46 | session.api = HBRestAPI(get_acc=True) 47 | # session.ws = HBWebsocket() 48 | # session.auws = HBWebsocket(auth=True) 49 | if not ws._active: 50 | ws.run() 51 | if not auws._active: 52 | @auws.after_auth 53 | def sub(): 54 | auws.sub_accounts() 55 | auws.sub_orders() 56 | 57 | @auws.register_handle_func(f'accounts') 58 | def push_account(msg): 59 | print(msg) 60 | accounts = msg['data'] 61 | socketio.emit('accounts', accounts, namespace='/ws') 62 | 63 | @session.auws.register_handle_func(f'orders.*') 64 | def push_orders(msg): 65 | orders = msg['data'] 66 | socketio.emit('orders', orders, namespace='/ws') 67 | 68 | auws.run() 69 | 70 | @socketio.on('disconnect', namespace='/ws') 71 | def disconnect(): 72 | session.ws.stop() 73 | session.auws.stop() 74 | 75 | 76 | @socketio.on('get_klines', namespace='/ws') 77 | def get_klines(msg): 78 | ret = session.api.get_kline(msg['symbol'], msg['period'], 600) 79 | klines = ret['data'] 80 | socketio.emit('klines', klines, namespace='/ws') 81 | 82 | @socketio.on('sub_kline', namespace='/ws') 83 | def sub_kline(msg): 84 | session.ws.sub_kline(msg['symbol'], msg['period']) 85 | @session.ws.register_handle_func(f"market.{msg['symbol']}.kline.{msg['period']}") 86 | def push_kline(msg): 87 | kline = msg['tick'] 88 | socketio.emit('kline_tick', kline, namespace='/ws') 89 | 90 | @socketio.on('unsub_kline', namespace='/ws') 91 | def unsub_kline(msg): 92 | session.ws.unsub_kline(msg['symbol'], msg['period']) 93 | session.ws.unregister_handle_func('push_kline', f"market.{msg['symbol']}.kline.{msg['period']}") 94 | 95 | # @socketio.on('sub_accounts', namespace='/ws') 96 | # def sub_accounts(): 97 | # session.auws.sub_accounts() 98 | # session.auws.after_auth(session.auws.sub_accounts) 99 | # @session.auws.register_handle_func(f'accounts') 100 | # def push_account(msg): 101 | # print(msg) 102 | # accounts = msg['data'] 103 | # socketio.emit('accounts', accounts, namespace='/ws') 104 | 105 | # @socketio.on('sub_orders', namespace='/ws') 106 | # def sub_orders(symbol): 107 | # session.auws.sub_orders(symbol) 108 | # @session.auws.register_handle_func(f'orders') 109 | # def push_orders(msg): 110 | # orders = msg['data'] 111 | # socketio.emit('orders', orders, namespace='/ws') 112 | 113 | 114 | 115 | if __name__ == '__main__': 116 | import sys 117 | socketio.run(app, debug=False, port=8989) 118 | -------------------------------------------------------------------------------- /huobitrade/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2018/10/22 0022 13:18 4 | # @Author : Hadrianl 5 | # @File : main.py 6 | # @Contact : 137150224@qq.com 7 | 8 | import click 9 | import os 10 | import importlib 11 | from huobitrade import setKey, setUrl, logger 12 | from urllib.parse import urlparse 13 | from huobitrade.handler import TimeHandler 14 | import time 15 | import traceback 16 | 17 | @click.group() 18 | @click.version_option('0.5.2') 19 | @click.help_option(help='HuoBiTrade命令行工具帮助') 20 | def cli(): 21 | click.secho('Welcome to HuoBiTrade!', fg='blue') 22 | 23 | @click.command() 24 | @click.option('-f', '--file', default=None, type=click.Path(exists=True), help='策略文件') 25 | @click.option('-a', '--access-key', prompt='Access-key', help='访问密钥') 26 | @click.option('-s', '--secret-key', prompt='Secret-key', help='私密密钥') 27 | @click.option('--url', help='火币服务器url,默认为api.huobi.br.com') 28 | @click.option('--reconn', type=click.INT, help='重连次数,默认为-1,即无限重连') 29 | def run(file, access_key, secret_key, **kwargs): 30 | """命令行运行huobitrade""" 31 | if file: 32 | import sys 33 | file_path, file_name = os.path.split(file) 34 | sys.path.append(file_path) 35 | strategy_module = importlib.import_module(os.path.splitext(file_name)[0]) 36 | init = getattr(strategy_module, 'init', None) 37 | handle_func = getattr(strategy_module, 'handle_func', None) 38 | schedule = getattr(strategy_module, 'schedule', None) 39 | else: 40 | init, handle_func, scedule = [None] * 3 41 | 42 | setKey(access_key, secret_key) 43 | url = kwargs.get('url') 44 | hostname = 'api.huobi.br.com' 45 | if url: 46 | hostname = urlparse(url).hostname 47 | setUrl('https://' + hostname, 'https://' + hostname) 48 | 49 | reconn = kwargs.get('reconn', -1) 50 | from huobitrade import HBWebsocket, HBRestAPI 51 | from huobitrade.datatype import HBMarket, HBAccount, HBMargin 52 | restapi = HBRestAPI(get_acc=True) 53 | ws = HBWebsocket(host=hostname, reconn=reconn) 54 | auth_ws = HBWebsocket(host=hostname, auth=True, reconn=reconn) 55 | data = HBMarket() 56 | account = HBAccount() 57 | margin = HBMargin() 58 | ws_open = False 59 | ws_auth = False 60 | 61 | @ws.after_open 62 | def _open(): 63 | nonlocal ws_open 64 | click.echo('行情接口连接成功') 65 | ws_open = True 66 | 67 | @auth_ws.after_auth 68 | def _auth(): 69 | nonlocal ws_auth 70 | click.echo('鉴权接口鉴权成功') 71 | ws_auth = True 72 | 73 | ws.run() 74 | auth_ws.run() 75 | 76 | for i in range(10): 77 | time.sleep(3) 78 | click.echo(f'连接:第{i+1}次连接') 79 | if ws_open&ws_auth: 80 | break 81 | else: 82 | ws.stop() 83 | auth_ws.stop() 84 | raise Exception('连接失败') 85 | if init: 86 | init(restapi, ws, auth_ws) 87 | 88 | if handle_func: 89 | for k, v in handle_func.items(): 90 | if k.split('.')[0].lower() == 'market': 91 | ws.register_handle_func(k)(v) 92 | else: 93 | auth_ws.register_handle_func(k)(v) 94 | 95 | if schedule: 96 | print('testing') 97 | from huobitrade.handler import TimeHandler 98 | interval = scedule.__kwdefaults__['interval'] 99 | timerhandler = TimeHandler('scheduler', interval) 100 | timerhandler.handle = lambda msg: schedule(restapi, ws, auth_ws) 101 | timerhandler.start() 102 | 103 | 104 | while True: 105 | try: 106 | code = click.prompt('huobitrade>>') 107 | if code == 'exit': 108 | if click.confirm('是否要退出huobitrade'): 109 | break 110 | else: 111 | continue 112 | else: 113 | result = eval(code) 114 | click.echo(result) 115 | except Exception as e: 116 | click.echo(traceback.format_exc()) 117 | 118 | ws.stop() 119 | auth_ws.stop() 120 | 121 | @click.command('test_conn') 122 | @click.option('-a', '--access-key', prompt='Access-key', help='访问密钥') 123 | @click.option('-s', '--secret-key', prompt='Secret-key', help='私密密钥') 124 | def test_connection(access_key, secret_key): 125 | """通过查询账户信息测试密钥是否可用""" 126 | setKey(access_key, secret_key) 127 | from huobitrade import HBRestAPI 128 | api = HBRestAPI() 129 | try: 130 | account = api.get_accounts() 131 | if account['status'] == 'ok': 132 | click.secho('连接成功!', fg='blue') 133 | click.echo(account['data']) 134 | else: 135 | click.secho('连接失败!', fg='red') 136 | click.secho(account['err-msg'], fg='red') 137 | except Exception as e: 138 | click.echo(traceback.format_exc()) 139 | 140 | @click.command('doc') 141 | def document(): 142 | """打开huobitrade文档""" 143 | click.launch('https://hadrianl.github.io/huobi/') 144 | 145 | 146 | def entry_point(): 147 | cli.add_command(run) 148 | cli.add_command(test_connection) 149 | cli.add_command(document) 150 | cli() 151 | -------------------------------------------------------------------------------- /huobitrade/extra/rpc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2018/6/25 0025 16:49 4 | # @Author : Hadrianl 5 | # @File : rpc.py 6 | # @Contact : 137150224@qq.com 7 | 8 | import zmq 9 | 10 | from threading import Thread 11 | import pickle 12 | from abc import abstractmethod 13 | from ..utils import logger 14 | 15 | 16 | class RPC: 17 | """ 18 | 暂时使用pickle序列化,如果后期遇到兼容性和性能问题,可能转其他的序列化 19 | """ 20 | def pack(self, data): # 用pickle打包 21 | data_ = pickle.dumps(data,) 22 | return data_ 23 | 24 | def unpack(self, data_): 25 | data = pickle.loads(data_) 26 | return data 27 | 28 | 29 | class RPCServer(RPC): 30 | def __init__(self, repPort=6868, pubPort=6869): # 请求端口和订阅端口 31 | self.__rpcFunc = {} 32 | self.__ctx = zmq.Context() 33 | self.__repSocket = self.__ctx.socket(zmq.REP) 34 | self.__pubSocket = self.__ctx.socket(zmq.PUB) 35 | self.__repSocket.bind(f'tcp://*:{repPort}') 36 | self.__pubSocket.bind(f'tcp://*:{pubPort}') 37 | self.__repSocket.setsockopt(zmq.SNDTIMEO, 3000) 38 | self._active = False 39 | 40 | def publish(self, topic, msg): # 发布订阅消息 41 | topic_ = self.pack(topic) 42 | msg_ = self.pack(msg) 43 | self.__pubSocket.send_multipart([topic_, msg_]) 44 | 45 | def rep(self): 46 | while self._active: 47 | try: 48 | func_name, args, kwargs = self.__repSocket.recv_pyobj() 49 | ret = self.__rpcFunc[func_name](*args, **kwargs) 50 | except zmq.error.Again: 51 | continue 52 | except Exception as e: 53 | ret = e 54 | finally: 55 | self.__repSocket.send_pyobj(ret) 56 | 57 | def startREP(self): 58 | if not self._active: 59 | self._active = True 60 | self.reqThread = Thread(target=self.rep, name='RPCSERVER') 61 | self.reqThread.setDaemon(True) 62 | self.reqThread.start() 63 | 64 | def stopREP(self): 65 | if self._active: 66 | self._active = False 67 | self.reqThread.join(5) 68 | 69 | def register_rpcFunc(self, name, func): 70 | self.__rpcFunc.update({name: func}) 71 | 72 | def unregister_rpcFunc(self, name): 73 | self.__rpcFunc.pop(name) 74 | 75 | @property 76 | def rpcFunc(self): 77 | return self.__rpcFunc 78 | 79 | 80 | class RPCClient(RPC): 81 | def __init__(self, reqAddr, subAddr, reqPort=6868, subPort=6869): 82 | self.__ctx = zmq.Context() 83 | self.__reqSocket = self.__ctx.socket(zmq.REQ) 84 | self.__subSocket = self.__ctx.socket(zmq.SUB) 85 | self.__subSocket.setsockopt(zmq.RCVTIMEO, 3000) 86 | self.__reqSocket.setsockopt(zmq.RCVTIMEO, 5000) 87 | self.__repAddr = f'tcp://{reqAddr}:{reqPort}' 88 | self.__subAddr = f'tcp://{subAddr}:{subPort}' 89 | self.__reqSocket.connect(self.__repAddr) 90 | self._active = False 91 | 92 | def rpcCall(self, func_name, *args, **kwargs): 93 | logger.debug(f'func:{func_name} args:{args} kwargs:{kwargs}') 94 | self.__reqSocket.send_pyobj([func_name, args, kwargs]) 95 | ret = self.__reqSocket.recv_pyobj() 96 | if isinstance(ret, Exception): 97 | raise ret 98 | else: 99 | return ret 100 | 101 | def _run(self): 102 | self.__subSocket.connect(self.__subAddr) 103 | while self._active: 104 | try: 105 | ret_ = self.__subSocket.recv_multipart() 106 | topic, msg = [self.unpack(d_) for d_ in ret_] 107 | self.handle(topic, msg) 108 | except zmq.error.Again: 109 | ... 110 | except Exception as e: 111 | logger.exception(f'-suberror:{e}', exc_info=True) 112 | self.__subSocket.disconnect(self.__subAddr) 113 | 114 | def startSUB(self): 115 | if not self._active: 116 | self._active = True 117 | self.subThread = Thread(target=self._run, name='RPCClient') 118 | self.subThread.setDaemon(True) 119 | self.subThread.start() 120 | 121 | def stopSUB(self): 122 | if self._active: 123 | self._active = False 124 | self.subThread.join(5) 125 | 126 | def subscribe(self, topic): 127 | if topic != '': 128 | topic_ = self.pack(topic) 129 | self.__subSocket.setsockopt(zmq.SUBSCRIBE, topic_) 130 | else: 131 | self.__subSocket.subscribe('') 132 | 133 | def unsubscribe(self, topic): 134 | if topic != '': 135 | topic_ = self.pack(topic) 136 | self.__subSocket.setsockopt(zmq.UNSUBSCRIBE, topic_) 137 | else: 138 | self.__subSocket.unsubscribe('') 139 | 140 | @abstractmethod 141 | def handle(self, topic, msg): 142 | raise NotImplementedError('Please overload handle function') 143 | 144 | def __getattr__(self, item): 145 | def wrapper(*args, **kwargs): 146 | return self.rpcCall(item, *args, **kwargs) 147 | return wrapper 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /huobitrade/handler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2018/5/25 0025 14:52 4 | # @Author : Hadrianl 5 | # @File : handler.py 6 | # @Contact : 137150224@qq.com 7 | 8 | import pymongo as pmo 9 | from .utils import logger, handler_profiler, zmq_ctx 10 | from threading import Thread, Timer 11 | from abc import abstractmethod 12 | import zmq 13 | import pickle 14 | from .extra.rpc import RPCServer 15 | from queue import deque 16 | from concurrent.futures import ThreadPoolExecutor 17 | import time 18 | 19 | class BaseHandler: 20 | def __init__(self, name, topic: (str, list) = None, *args, **kwargs): 21 | self.name = name 22 | self.topic = set(topic if isinstance(topic, list) else 23 | [topic]) if topic is not None else set() 24 | self.ctx = zmq_ctx 25 | self.sub_socket = self.ctx.socket(zmq.SUB) 26 | self.sub_socket.setsockopt(zmq.RCVTIMEO, 3000) 27 | self._thread_pool = ThreadPoolExecutor(max_workers=1) 28 | self.inproc = set() 29 | 30 | if self.topic: # 如果topic默认为None,则对所有的topic做处理 31 | for t in self.topic: 32 | self.sub_socket.setsockopt(zmq.SUBSCRIBE, pickle.dumps(t)) 33 | else: 34 | self.sub_socket.subscribe('') 35 | 36 | if kwargs.get('latest', False): # 可以通过latest(bool)来订阅最新的数据 37 | self.data_queue = deque(maxlen=1) 38 | self.latest = True 39 | else: 40 | self.data_queue = deque() 41 | self.latest = False 42 | self.__active = False 43 | 44 | def run(self): 45 | self.task = self._thread_pool.submit(logger.info, f'Handler:{self.name}启用') 46 | while self.__active: 47 | try: 48 | topic_, msg_ = self.sub_socket.recv_multipart() 49 | if msg_ is None: # 向队列传入None来作为结束信号 50 | break 51 | topic = pickle.loads(topic_) 52 | msg = pickle.loads(msg_) 53 | self.data_queue.append([topic, msg]) 54 | if len(self.data_queue) >= 1000: 55 | logger.warning(f'Handler:{self.name}未处理msg超过1000!') 56 | 57 | if self.task.done(): 58 | self.task = self._thread_pool.submit(self.handle, *self.data_queue.popleft()) 59 | except zmq.error.Again: 60 | ... 61 | except Exception as e: 62 | logger.exception(f'-{self.name} exception:{e}') 63 | self._thread_pool.shutdown() 64 | logger.info(f'Handler:{self.name}停止') 65 | 66 | def add_topic(self, new_topic): 67 | self.sub_socket.setsockopt(zmq.SUBSCRIBE, pickle.dumps(new_topic)) 68 | self.topic.add(new_topic) 69 | 70 | def remove_topic(self, topic): 71 | self.sub_socket.setsockopt(zmq.UNSUBSCRIBE, pickle.dumps(topic)) 72 | self.topic.remove(topic) 73 | 74 | def stop(self, wsname): 75 | try: 76 | self.inproc.remove(wsname) 77 | self.sub_socket.disconnect(f'inproc://{wsname}') 78 | finally: 79 | if not self.inproc: 80 | self.__active = False 81 | self.thread.join() 82 | 83 | def start(self, wsname): 84 | self.sub_socket.connect(f'inproc://{wsname}') 85 | self.inproc.add(wsname) 86 | if not self.__active: 87 | self.__active = True 88 | self.thread = Thread(target=self.run, name=self.name) 89 | self.thread.setDaemon(True) 90 | self.thread.start() 91 | 92 | @abstractmethod 93 | def handle(self, topic, msg): # 所有handler需要重写这个函数 94 | ... 95 | 96 | 97 | class TimeHandler: 98 | def __init__(self, name, interval, get_msg=None): 99 | self.name = name 100 | self.interval = interval 101 | self.get_msg = get_msg 102 | self._active = False 103 | 104 | def run(self, interval): 105 | while self._active: 106 | try: 107 | msg = self.get_msg() if self.get_msg else None 108 | self.handle(msg) 109 | except Exception as e: 110 | logger.exception(f'-{self.name} exception:{e}') 111 | finally: 112 | time.sleep(interval) 113 | 114 | def stop(self): 115 | self._active = False 116 | 117 | def start(self): 118 | self.timer = Thread(target=self.run, args=(self.interval, )) 119 | self.timer.setName(self.name) 120 | self.timer.setDaemon(True) 121 | self.timer.start() 122 | 123 | @abstractmethod 124 | def handle(self, msg): 125 | ... 126 | 127 | 128 | class DBHandler(BaseHandler, pmo.MongoClient): 129 | def __init__(self, topic=None, host='localhost', port=27017, db='HuoBi'): 130 | BaseHandler.__init__(self, 'DB', topic) 131 | pmo.MongoClient.__init__(self, host, port) 132 | self.db = self.get_database(db) 133 | 134 | def into_db(self, data, topic: str): 135 | collection = self.db.get_collection(topic) 136 | try: 137 | if 'kline' in topic: 138 | if isinstance(data, dict): 139 | collection.update({'id': data['id']}, data, upsert=True) 140 | elif isinstance(data, list): 141 | for d in data: 142 | collection.update({'id': d['id']}, d, upsert=True) 143 | elif 'trade.detail' in topic: 144 | for d in data: 145 | d['id'] = str(d['id']) 146 | collection.update({'id': d['id']}, d, upsert=True) 147 | elif 'depth' in topic: 148 | collection.update({'version': data['version']}, data, upsert=True) 149 | except Exception as e: 150 | logger.error(f'<数据>插入数据库错误-{e}') 151 | 152 | def handle(self, topic, msg): 153 | if 'ch' in msg or 'rep' in msg: 154 | topic = msg.get('ch') or msg.get('rep') 155 | data = msg.get('tick') or msg.get('data') 156 | self.into_db(data, topic) 157 | 158 | 159 | class RPCServerHandler(BaseHandler): 160 | def __init__(self, reqPort=6868, pubPort=6869, topic=None): 161 | BaseHandler.__init__(self, 'RPCServer', topic) 162 | self.rpcServer = RPCServer(reqPort, pubPort) 163 | self.rpcServer.startREP() 164 | 165 | def handle(self, topic, msg): 166 | self.rpcServer.publish(topic, msg) 167 | 168 | def register_func(self, name, func): 169 | self.rpcServer.register_rpcFunc(name, func) 170 | 171 | def unregister_func(self, name): 172 | self.rpcServer.unregister_rpcFunc(name) 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [火币API的Python版](https://hadrianl.github.io/huobi/) 2 | - websocket封装成`HBWebsocket`类,用`run`开启连接线程 3 | - `HBWebsocket`通过注册`Handler`的方式来处理数据,消息通过`pub_msg`来分发到个各`topic`下的`Handler`线程来处理 4 | - 火币的鉴权WS是与普通WS独立的,所以同时使用需要开启两个WS 5 | - restful api基本参照火币网的demo封装成`HBRestAPI`类 6 | - 兼容win,mac,linux,python版本必须3.6或以上,因为使用了大量的f*** 7 | - 目前已经稳定使用,后续会基于框架提供如行情持久化,交易数据持久化等`handler` 8 | - 有疑问或者需要支持和交流的小伙伴可以联系我, QQ:[137150224](http://wpa.qq.com/msgrd?v=3&uin=137150224&site=qq&menu=yes) 9 | - 鉴于小伙伴数量也越来越多,所以建个小群:859745469 , 方便大家交流 10 | 11 | ![QQ Group](/QRCode.png) 12 | 13 | ## Notice 14 | - 该封装的函数命名跟火币本身的请求命名表达不太一致 15 | - 包含open, close, high, low的数据命名是kline(其中部分有ask和bid,都纳入这类命名) 16 | - 当且仅当数据只有一条逐笔tick(没有ohlc),命名是ticker 17 | - 深度数据则命名为depth 18 | 19 | ## Lastest 20 | - 合约与现货已经进行了部分测试,保证可用性 21 | - 优化相关datatype 22 | 23 | [![PyPI](https://img.shields.io/pypi/v/huobitrade.svg)](https://pypi.org/project/huobitrade/) 24 | [![GitHub forks](https://img.shields.io/github/forks/hadrianl/huobi.svg)](https://github.com/hadrianl/huobi/network) 25 | [![GitHub stars](https://img.shields.io/github/stars/hadrianl/huobi.svg)](https://github.com/hadrianl/huobi/stargazers) 26 | ![build](https://travis-ci.org/hadrianl/huobi.svg?branch=master) 27 | ![license](https://img.shields.io/github/license/hadrianl/huobi.svg) 28 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/huobitrade.svg) 29 | 30 | - [HuoBi Trading](#火币api的python版) 31 | - [1. Installation](#1-installation) 32 | - [2. Usage](#2-usage) 33 | - [2.1 huobitrade CLI Tool](#21-huobitrade-cli-tool) 34 | - [2.2.1 WebSocket API](#221-websocket-api) 35 | - [2.2.2 Auth WebSocket API](#222-auth-websocket-api) 36 | - [2.3 Restful API](#23-restful-api) 37 | - [2.4 Message Handler](#25-message-handler) 38 | - [2.5 Latest Message Handler](#26-latest-message-handler) 39 | - [2.6 HBData](#27-hbdata) 40 | - [3. Extra](#3-extra) 41 | 42 | 43 | ## 1. Installation 44 | ```sh 45 | pip install huobitrade 46 | ``` 47 | 48 | ## 2. Usage 49 | - 实现长连订阅策略最核心的部分是实现handler里面的handle函数 50 | 1. 通过`HBWebsocket`实例的`sub`开头的函数订阅需要的topic 51 | 2. 通过继承`BaseHandler`的实例的初始化或者`add_topic`来增加对相关topic,实现`handl`e函数来处理相关topic的消息 52 | 3. 通过`HBWebsocket`实例的`register_handler`来注册`handler` 53 | 4. `handler`初始化中有个`latest`,建议使用depth或者ticker数据来做处理,且策略性能不高的时候使用它 54 | - 基于websocket的接口都是用异步回调的方式来处理 55 | 1. 首先需要用`HBWebsocket`的装饰器`register_onRsp`来绑定实现一个处理相关的topic消息的函数 56 | 2. 再用`req`开头的函数来请求相关topic数据,回调的数据将会交给回调函数处理 57 | - 交易相关的都是用的restful api(因为火币还没推出websocket的交易接口) 58 | 1. `setKey`是必要的,如果需要用到交易相关请求的话,只是请求行情的话可以不设。 59 | 2. `HBRestAPI`是单例类,所以多次初始化也木有啥大问题,如在handler里面初始化 60 | 3. 每个请求都有一个`_async`参数来提供异步请求,建议尽量使用它,具体用法是先初始化数个请求到一个list,再用`async_request`一次性向服务器发起请求 61 | 4. 子账户体系没用过,可能会有问题,有bug欢迎pr 62 | - 另外还提供了几个简单易用的封装类 63 | 1. `HBMarket`, `HBAccount`, `HBMargin`分别是行情,账户和借贷账户类,里面提供了大部分属性调用请求,均基于`HBRestAPI` 64 | 2. 一般情景下应该是可以替代HBRestAPI的使用的 65 | - 最后还提供了数个运营管理的工具 66 | 1. 微信推送handler,可以实现一些交易信息推送之类的,但是建议朋友们慎用,因为鄙人有试过一天推送几千条信息被封禁了半个月微信web端登陆的经历 67 | 2. `rpc`模块,具体用法就不细说了,懂的应该都懂的,看下源码就知道咋用啦 68 | - 最后的最后,其实基于这个项目,还有附带的另外一个可视化web的项目没有放出来 69 | 1. 基于`flask`写的一个用于查询当日成交明细和成交分布图,很丑很简陋 70 | 2. 有兴趣的小伙伴可以联系我 71 | 72 | ### 2.1 huobitrade CLI Tool 73 | - `huobitrade run -f strategy.py -a access-key -s secret-key`用于启用一个基本简单的策略,其中strategy里应该可以包含一个init和handle_func用于初始化或处理相关topic.连接和鉴权成功后,会进入交互环境,提供6个命名空间来进行交互,包括`restapi` `ws` `auth_ws` `account` `data` `margin`,分别都是huobitrade几个主要类的实例huobi 74 | - `huobitrade test_conn`用于测试是否可以正常连接, `huobitrade doc`打开huobitrade文档 75 | - `huobitrade --help`通过该命令获取帮助 76 | 77 | 78 | ### 2.2.1 WebSocket API 79 | ```python 80 | from huobitrade.service import HBWebsocket 81 | 82 | hb = HBWebsocket() # 可以填入url参数,默认是api.huobi.br.com 83 | @hb.after_open # 使用装饰器注册函数,当ws连接之后会调用函数,可以实现订阅之类的 84 | def sub_depth(): 85 | hb.sub_depth('ethbtc') 86 | 87 | hb.run() # 开启websocket进程 88 | 89 | # -------------------------------------------- 90 | hb.sub_kline('ethbtc', '1min') # 订阅数据 91 | @hb.register_handle_func('market.ethbtc.kline.1min') # 注册一个处理函数,最好的处理方法应该是实现一个handler 92 | def handle(msg): 93 | print('handle:', msg) 94 | 95 | hb.unregister_handle_func(handle, 'market.ethbtc.kline.1min') # 释放处理函数 96 | 97 | # -------------------------------------------- 98 | # websocket请求数据是异步请求回调,所以先注册一个回调处理函数,再请求 99 | @hb.register_onRsp('market.btcusdt.kline.1min') 100 | def OnRsp_print(msg): 101 | print(msg) 102 | 103 | hb.req_kline('btcusdt', '1min') 104 | hb.unregister_onRsp('market.btcusdt.kline.1min') # 注销某topic的请求回调处理 105 | 106 | ``` 107 | 108 | ### 2.2.2 Auth WebSocket API 109 | ```python 110 | from huobitrade import setKey 111 | from huobitrade.service import HBWebsocket 112 | setKey('your acess_key', 'you secret_key') 113 | hb = HBWebsocket(auth=True) # 可以填入url参数,默认是api.huobi.br.com 114 | @hb.after_auth # 会再鉴权成功通过之后自动调用 115 | def sub_accounts(): 116 | hb.sub_accounts() 117 | 118 | hb.run() # 开启websocket进程 119 | 120 | @hb.register_handle_func('accounts') # 注册一个处理函数,最好的处理方法应该是实现一个handler 121 | def auth_handle(msg): 122 | print('auth_handle:', msg) 123 | 124 | ``` 125 | 126 | 127 | ### 2.3 Restful API 128 | - restapi需要先用`setKey`设置密钥 129 | - 默认交易和行情url都是https://api.huobi.br.com (调试用),实盘要用`from huobitrade import setUrl`设置url 130 | 131 | ```python 132 | from huobitrade.service import HBRestAPI 133 | from huobitrade import setKey 134 | # setUrl('', '') 135 | setKey('your acess_key', 'you secret_key') # setKey很重要,最好在引入其他模块之前先setKey,鉴权ws和restapi的部分函数是基于密钥 136 | api = HBRestAPI(get_acc=True) # get_acc参数默认为False,初始化不会取得账户ID,需要ID的函数无法使用.也可用api.set_acc_id('you_account_id') 137 | print(api.get_timestamp()) 138 | 139 | api = HBRestAPI(get_acc=True) # 异步请求 140 | klines = api.get_kline('omgeth', '1min', _async=True) 141 | symbols = api.get_symbols(_async=True) 142 | results = api.async_request([klines, symbols]) 143 | for r in results: 144 | print(r) 145 | ``` 146 | 147 | ### 2.4 Message Handler 148 | - handler是用来处理websocket的原始返回消息的,通过继承basehandler实现handle函数以及注册进HBWebsocket相关的topic来使用 149 | 150 | ```python 151 | from huobitrade.handler import BaseHandler 152 | from huobitrade.utils import handler_profiler 153 | from huobitrade import setKey 154 | from huobitrade.service import HBWebsocket 155 | setKey('your acess_key', 'you secret_key') 156 | hb = HBWebsocket(auth=True) # 可以填入url参数,默认是api.huobi.br.com 157 | 158 | class MyHandler(BaseHandler): 159 | def __init__(self, topic, *args, **kwargs): 160 | BaseHandler.__init__(self, 'just Thread name', topic) 161 | 162 | @handler_profiler('profiler.csv') # 可以加上这个装饰器来测试handle函数的执行性能,加参数会输出到单独文件 163 | def handle(self, topic, msg): # 实现handle来处理websocket推送的msg 164 | print(topic, msg) 165 | 166 | 167 | handler = MyHandler('market.ethbtc.kline.1min') # topic为str或者list 168 | handler.add_topic('market.ethbtc.kline.5min') # 为handler增加处理topic(remove_topic来删除) 169 | hb.register_handler(handler) # 通过register来把handler注册到相应的topic 170 | 171 | 172 | ``` 173 | - 内置实现了一个mongodb的`DBHandler` 174 | 175 | ```python 176 | from huobitrade.handler import DBHandler 177 | from huobitrade import setKey 178 | from huobitrade.service import HBWebsocket 179 | setKey('your acess_key', 'you secret_key') 180 | hb = HBWebsocket(auth=True) # 可以填入url参数,默认是api.huobi.br.com 181 | handler = DBHandler() # topic为空的话,会对所有topic的msg做处理 182 | hb.register_handler(handler) 183 | ``` 184 | 185 | ### 2.5 Latest Message Handler 186 | - 基于handler函数根据策略复杂度和性能的的不同造成对message的处理时间不一样,可能造成快生产慢消费的情况,增加lastest参数,每次都是handle最新的message 187 | ```python 188 | from huobitrade.handler import BaseHandler 189 | from huobitrade.utils import handler_profiler 190 | class MyLatestHandler(BaseHandler): 191 | def __init__(self, topic, *args, **kwargs): 192 | BaseHandler.__init__(self, 'just Thread name', topic, latest=True) 193 | 194 | @handler_profiler() # 可以加上这个装饰器来测试handle函数的执行性能 195 | def handle(self, topic, msg): # 实现handle来处理websocket推送的msg 196 | print(topic, msg) 197 | ``` 198 | 199 | ### 2.6 HBData 200 | - 使用类似topic的方式来取数据,topic的表达方式与火币有不同 201 | 202 | ```python 203 | from huobitrade import setKey 204 | setKey('acess_key', 'secret_key') 205 | from huobitrade.datatype import HBMarket, HBAccount, HBMargin 206 | 207 | data = HBMarket() # 行情接口类 208 | account = HBAccount() # 交易接口类 209 | margin = HBMargin() # 借贷接口类 210 | 211 | data.omgeth 212 | # 213 | data.omgeth.kline 214 | # < for omgeth> 215 | data.omgeth.depth 216 | # < for omgeth> 217 | data.omgeth.ticker 218 | # < for omgeth> 219 | data.omgeth.kline._1min_200 # period前面加'_', 后面加数量最大值为2000 220 | data.omgeth.kline.last 221 | data.omgeth.kline.last_24_hour 222 | data.omgeth.depth.step0 # step0,1,2,3,4,5 223 | data.omgeth.ticker.last # 最新的一条tick 224 | data.omgeth.ticker.last_20 # last_1至last_2000 225 | data.all_24h_kline # 当前所有交易对的ticker 226 | account.Detail # 所有账户明细 227 | account.balance_XXXX # XXXX为account_id,某账户的结余, 引用结余信息会自动更新 228 | account.order # 账户的订单类 229 | account.order['order_id'] # 查询某order明细,或者用get方法 230 | account.order.send('account_id', 1, 'omgeth', 'buy-limit', 0.001666) # 发送订单 231 | account.order.batchcancel(['order_id1', 'order_id2']) 232 | account.order + [1, 'omgeth', 'buy-limit', 0.001666] # 发送订单 233 | account.order + {'acc_id': 'your_account_id', 'amount': 1, 'symbol': 'omgeth', 'type': 'buy-limit', 'price': 0.001666} 234 | account.order - 'order_id' # 取消订单 235 | account.order - ['order_id1', 'order_id2'] # 批量取消订单 236 | account.trade.get_by_id('order_id') # 某账户的成交类(即火币的matchresults),也可以直接索引 237 | margin.transferIn('ethusdt', 'eth', 1) 238 | ethusdt_margin_info = margin['ethusdt'] # 或者用getBalance 239 | ethusdt_margin_info.balance # ethusdt交易对的保证金结余信息 240 | 241 | ``` 242 | 243 | ## 3. Extra 244 | - 交易策略运营相关的模块,`wechat推送`,`rpc远程订阅调用`等 245 | 详见[extra](https://github.com/hadrianl/huobi/blob/master/huobitrade/extra/log_handler.md) -------------------------------------------------------------------------------- /huobitrade/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2018/5/24 0024 14:12 4 | # @Author : Hadrianl 5 | # @File : utils.py 6 | # @Contact : 137150224@qq.com 7 | 8 | 9 | from requests_futures.sessions import FuturesSession 10 | import logging 11 | import sys 12 | import base64 13 | import datetime 14 | import hashlib 15 | import hmac 16 | from ecdsa import SigningKey 17 | import json 18 | import urllib 19 | import urllib.parse 20 | import urllib.request 21 | import requests 22 | from functools import wraps 23 | import time 24 | import zmq 25 | 26 | _format = "%(asctime)-15s [%(levelname)s] [%(name)s] %(message)s" 27 | _datefmt = "%Y/%m/%d %H:%M:%S" 28 | _level = logging.INFO 29 | 30 | handlers = [ 31 | logging.StreamHandler(sys.stdout), 32 | logging.FileHandler('huobi.log') 33 | ] 34 | 35 | logging.basicConfig( 36 | format=_format, datefmt=_datefmt, level=_level, handlers=handlers) 37 | logging.addLevelName(60, 'WeChatLog') 38 | logger = logging.getLogger('HuoBi') 39 | 40 | SYMBOL = {'ethbtc', 'ltcbtc', 'etcbtc', 'bchbtc'} 41 | PERIOD = { 42 | '1min', '5min', '15min', '30min', '60min', '1day', '1mon', '1week', '1year' 43 | } 44 | DEPTH = { 45 | 0: 'step0', 46 | 1: 'step1', 47 | 2: 'step2', 48 | 3: 'step3', 49 | 4: 'step4', 50 | 5: 'step5' 51 | } 52 | 53 | DerivativesDEPTH = { 54 | 0: 'step0', 55 | 1: 'step1', 56 | 2: 'step2', 57 | 3: 'step3', 58 | 4: 'step4', 59 | 5: 'step5', 60 | 6: 'step6', 61 | 7: 'step7', 62 | 8: 'step8', 63 | 9: 'step9', 64 | 10: 'step10', 65 | 11: 'step11', 66 | } 67 | 68 | class Depth: 69 | Step0 = 'step0' 70 | Step1 = 'step1' 71 | Step2 = 'step2' 72 | Step3 = 'step3' 73 | Step4 = 'step4' 74 | Step5 = 'step5' 75 | 76 | class OrderType: 77 | BuyMarket = 'buy-market' # '市价买' 78 | SellMarket = 'sell-market' # '市价卖' 79 | BuyLimit = 'buy-limit' #'限价买' 80 | SellLimit = 'sell-limit' # '限价卖' 81 | BuyIoc = 'buy-ioc' #'IOC买单' 82 | SellIoc = 'sell-ioc' #'IOC卖单 83 | 84 | class OrserStatus: 85 | PreSumitted = 'pre-submitted' # '准备提交' 86 | Submitted = 'submitted' # '已提交' 87 | PartialFilled = 'partial-filled' # '部分成交' 88 | PartialCanceled = 'partial-canceled' # '部分成交撤销' 89 | Filled = 'filled' # '完全成交' 90 | Canceled = 'canceled' # '已撤销' 91 | 92 | 93 | ORDER_TYPE = { 94 | 'buy-market': '市价买', 95 | 'sell-market': '市价卖', 96 | 'buy-limit': '限价买', 97 | 'sell-limit': '限价卖', 98 | 'buy-ioc': 'IOC买单', 99 | 'sell-ioc': 'IOC卖单', 100 | 'buy-limit-maker': '限价买入做市', 101 | 'sell-limit-maker': '限价卖出做市' 102 | } 103 | ORDER_STATES = { 104 | 'pre-submitted': '准备提交', 105 | 'submitted': '已提交', 106 | 'partial-filled': '部分成交', 107 | 'partial-canceled': '部分成交撤销', 108 | 'filled': '完全成交', 109 | 'canceled': '已撤销' 110 | } 111 | 112 | ORDER_SOURCE = { 113 | 'spot-web': '现货 Web 交易单', 114 | 'spot-api': '现货 Api 交易单', 115 | 'spot-app': '现货 App 交易单', 116 | 'margin-web': '借贷 Web 交易单', 117 | 'margin-api': '借贷 Api 交易单', 118 | 'margin-app': '借贷 App 交易单', 119 | 'fl-sys': '借贷强制平仓单(爆仓单)' 120 | } 121 | 122 | 123 | 124 | 125 | ACCESS_KEY = "" 126 | SECRET_KEY = "" 127 | PRIVATE_KEY = "" 128 | 129 | ETF_SWAP_CODE = {200: '正常', 130 | 10404: '基金代码不正确或不存在', 131 | 13403: '账户余额不足', 132 | 13404: '基金调整中,不能换入换出', 133 | 13405: '因配置项问题基金不可换入换出', 134 | 13406: '非API调用,请求被拒绝', 135 | 13410: 'API签名错误', 136 | 13500: '系统错误', 137 | 13601: '调仓期:暂停换入换出', 138 | 13603: '其他原因:暂停换入和换出', 139 | 13604: '暂停换入', 140 | 13605: '暂停换出', 141 | 13606: '换入或换出的基金份额超过规定范围'} 142 | 143 | zmq_ctx = zmq.Context() 144 | async_session = FuturesSession(max_workers=8) 145 | 146 | # API 请求地址 147 | DEFAULT_URL = 'https://api.huobi.br.com' 148 | DEFAULT_DM_URL = 'https://api.hbdm.com' 149 | MARKET_URL = 'https://api.huobi.br.com' 150 | TRADE_URL = 'https://api.huobi.br.com' 151 | 152 | ACCOUNT_ID = None 153 | 154 | 155 | def setKey(access_key, secret_key): 156 | global ACCESS_KEY, SECRET_KEY, PRIVATE_KEY 157 | ACCESS_KEY = access_key 158 | SECRET_KEY = secret_key 159 | 160 | def setUrl(market_url, trade_url): 161 | global MARKET_URL, TRADE_URL 162 | MARKET_URL = market_url 163 | TRADE_URL = trade_url 164 | 165 | def createSign(pParams, method, host_url, request_path, secret_key): 166 | """ 167 | from 火币demo, 构造签名 168 | :param pParams: 169 | :param method: 170 | :param host_url: 171 | :param request_path: 172 | :param secret_key: 173 | :return: 174 | """ 175 | sorted_params = sorted(pParams.items(), key=lambda d: d[0], reverse=False) 176 | encode_params = urllib.parse.urlencode(sorted_params) 177 | payload = [method, host_url, request_path, encode_params] 178 | payload = '\n'.join(payload) 179 | payload = payload.encode(encoding='UTF8') 180 | secret_key = secret_key.encode(encoding='UTF8') 181 | 182 | digest = hmac.new(secret_key, payload, digestmod=hashlib.sha256).digest() 183 | signature = base64.b64encode(digest) 184 | signature = signature.decode() 185 | return signature 186 | 187 | def createPrivateSign(secret_sign, private_key): 188 | signingkey = SigningKey.from_pem(private_key, hashfunc=hashlib.sha256) 189 | secret_sign = secret_sign.encode(encoding='UTF8') 190 | 191 | privateSignature = signingkey.sign(secret_sign) 192 | privateSignature = base64.b64encode(privateSignature) 193 | return privateSignature 194 | 195 | def http_get_request(url, params, add_to_headers=None, _async=False): 196 | """ 197 | from 火币demo, get方法 198 | :param url: 199 | :param params: 200 | :param add_to_headers: 201 | :return: 202 | """ 203 | headers = { 204 | 'Content-type': 205 | 'application/x-www-form-urlencoded', 206 | 'User-Agent': 207 | 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36', 208 | } 209 | if add_to_headers: 210 | headers.update(add_to_headers) 211 | postdata = urllib.parse.urlencode(params) 212 | if _async: 213 | response = async_session.get(url, params=postdata, headers=headers, timeout=5) 214 | return response 215 | else: 216 | try: 217 | response = requests.get(url, postdata, headers=headers, timeout=5) 218 | if response.status_code == 200: 219 | return response.json() 220 | else: 221 | logger.debug( 222 | f'error_code:{response.status_code} reason:{response.reason} detail:{response.text}') 223 | return 224 | except Exception as e: 225 | logger.exception(f'httpGet failed, detail is:{response.text},{e}') 226 | return 227 | 228 | 229 | def http_post_request(url, params, add_to_headers=None, _async=False): 230 | """ 231 | from 火币demo, post方法 232 | :param url: 233 | :param params: 234 | :param add_to_headers: 235 | :return: 236 | """ 237 | headers = { 238 | "Accept": "application/json", 239 | 'Content-Type': 'application/json' 240 | } 241 | if add_to_headers: 242 | headers.update(add_to_headers) 243 | postdata = json.dumps(params) 244 | if _async: 245 | response = async_session.post(url, postdata, headers=headers, timeout=10) 246 | return response 247 | else: 248 | try: 249 | response = requests.post(url, postdata, headers=headers, timeout=10) 250 | if response.status_code == 200: 251 | return response.json() 252 | else: 253 | logger.debug(f'error_code:{response.status_code} reason:{response.reason} detail:{response.text}') 254 | return 255 | except Exception as e: 256 | logger.exception( 257 | f'httpPost failed, detail is:{response.text},{e}') 258 | return 259 | 260 | 261 | def api_key_get(params, request_path, _async=False, url=None): 262 | """ 263 | from 火币demo, 构造get请求并调用get方法 264 | :param params: 265 | :param request_path: 266 | :return: 267 | """ 268 | method = 'GET' 269 | timestamp = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S') 270 | params.update({ 271 | 'AccessKeyId': ACCESS_KEY, 272 | 'SignatureMethod': 'HmacSHA256', 273 | 'SignatureVersion': '2', 274 | 'Timestamp': timestamp 275 | }) 276 | 277 | host_url = DEFAULT_URL if url is None else url 278 | host_name = urllib.parse.urlparse(host_url).hostname.lower() 279 | secret_sign = createSign(params, method, host_name, request_path, 280 | SECRET_KEY) 281 | params['Signature'] = secret_sign 282 | 283 | url = host_url + request_path 284 | return http_get_request(url, params, _async=_async) 285 | 286 | 287 | def api_key_post(params, request_path, _async=False, url=None): 288 | """ 289 | from 火币demo, 构造post请求并调用post方法 290 | :param params: 291 | :param request_path: 292 | :return: 293 | """ 294 | method = 'POST' 295 | timestamp = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S') 296 | params_to_sign = { 297 | 'AccessKeyId': ACCESS_KEY, 298 | 'SignatureMethod': 'HmacSHA256', 299 | 'SignatureVersion': '2', 300 | 'Timestamp': timestamp 301 | } 302 | 303 | host_url = DEFAULT_URL if url is None else url 304 | host_name = urllib.parse.urlparse(host_url).hostname.lower() 305 | secret_sign = createSign(params_to_sign, method, host_name, 306 | request_path, SECRET_KEY) 307 | params_to_sign['Signature'] = secret_sign 308 | 309 | url = ''.join([host_url, request_path , '?', urllib.parse.urlencode(params_to_sign)]) 310 | return http_post_request(url, params, _async=_async) 311 | 312 | 313 | def handler_profiler(filename=None): 314 | """ 315 | handler的性能测试装饰器 316 | :param filename: 317 | :return: 318 | """ 319 | if filename == None: 320 | f = sys.stdout 321 | else: 322 | f = open(filename, 'w') 323 | def _callfunc(handle): 324 | @wraps(handle) 325 | def func(self, topic, msg): 326 | t0 = time.time() 327 | handle(self, topic, msg) 328 | t1 = time.time() 329 | print(f'{self.name}-handle运行时间:{t1 - t0}s', file=f) 330 | 331 | return func 332 | return _callfunc 333 | 334 | class Singleton(type): 335 | _instances = {} 336 | def __call__(cls, *args, **kwargs): 337 | if cls not in cls._instances: 338 | cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) 339 | return cls._instances[cls] -------------------------------------------------------------------------------- /huobitrade/datatype.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2018/6/13 0013 16:36 4 | # @Author : Hadrianl 5 | # @File : datatype.py 6 | # @Contact : 137150224@qq.com 7 | 8 | import pandas as pd 9 | from .service import HBRestAPI 10 | from .utils import PERIOD, DEPTH, logger 11 | from itertools import chain 12 | from typing import Iterable 13 | 14 | __all__ = ['HBMarket', 'HBAccount', 'HBMargin'] 15 | _api = HBRestAPI(get_acc=True) 16 | 17 | 18 | class HBKline: 19 | def __init__(self, symbol): 20 | self.__symbol = symbol 21 | 22 | def __getattr__(self, item): 23 | global _api 24 | if item[0] == '_': 25 | args = item[1:].split('_') 26 | if args[0] not in PERIOD: 27 | raise Exception('period not exist.') 28 | else: 29 | reply = _api.get_kline(self.__symbol, args[0], int(args[1])) 30 | klines = pd.DataFrame(reply['data']) 31 | return klines 32 | elif item == 'last': 33 | reply = _api.get_last_1m_kline(self.__symbol) 34 | last_kline = pd.Series(reply['tick']) 35 | return last_kline 36 | elif item == 'last_24_hour': 37 | reply = _api.get_last_24h_kline(self.__symbol) 38 | last_24h = pd.Series(reply['tick']) 39 | return last_24h 40 | else: 41 | raise AttributeError 42 | 43 | def __repr__(self): 44 | return f'<{self.__class__} for {self.__symbol}>' 45 | 46 | def __str__(self): 47 | return f'<{self.__class__} for {self.__symbol}>' 48 | 49 | 50 | class HBDepth: 51 | def __init__(self, symbol): 52 | self.__symbol = symbol 53 | 54 | def __getattr__(self, item): 55 | global _api 56 | if item in (d for d in DEPTH.values()): 57 | reply = _api.get_last_depth(self.__symbol, item) 58 | bids, asks = reply['tick']['bids'], reply['tick']['asks'] 59 | df_bids = pd.DataFrame(bids, columns=['bid', 'bid_qty']) 60 | df_asks = pd.DataFrame(asks, columns=['ask', 'ask_qty']) 61 | depth = pd.concat([df_bids, df_asks], 1) 62 | return depth 63 | else: 64 | raise AttributeError 65 | 66 | def __repr__(self): 67 | return f'<{self.__class__} for {self.__symbol}>' 68 | 69 | def __str__(self): 70 | return f'<{self.__class__} for {self.__symbol}>' 71 | 72 | 73 | class HBTicker: 74 | def __init__(self, symbol): 75 | self.__symbol = symbol 76 | 77 | def __getattr__(self, item): 78 | global _api 79 | if item == 'last': 80 | reply = _api.get_last_ticker(self.__symbol) 81 | last_ticker = pd.DataFrame(reply['tick']['data']) 82 | return last_ticker 83 | elif 'last' in item: 84 | args = item.split('_') 85 | size = int(args[1]) 86 | reply = _api.get_tickers(self.__symbol, size) 87 | ticker_list = [ 88 | t for t in chain(*[i['data'] for i in reply['data']]) 89 | ] 90 | tickers = pd.DataFrame(ticker_list) 91 | return tickers 92 | 93 | def __repr__(self): 94 | return f'<{self.__class__} for {self.__symbol}>' 95 | 96 | def __str__(self): 97 | return f'<{self.__class__} for {self.__symbol}>' 98 | 99 | class HBSymbol: 100 | def __init__(self, name, **kwargs): 101 | self.name = name 102 | self.attr = kwargs 103 | for k, v in kwargs.items(): 104 | k = k.replace('-', '_') 105 | setattr(self, k, v) 106 | self.kline = HBKline(self.name) 107 | self.depth = HBDepth(self.name) 108 | self.ticker = HBTicker(self.name) 109 | 110 | def __repr__(self): 111 | return f'' 112 | 113 | def __str__(self): 114 | return f'' 115 | 116 | 117 | class HBMarket: 118 | """ 119 | 火币的市场数据类,快捷获取数据 120 | """ 121 | 122 | def __init__(self): 123 | self.symbols = [] 124 | self._update_symbols() 125 | 126 | def add_symbol(self, symbol): 127 | setattr(self, symbol.name, symbol) 128 | 129 | def _update_symbols(self): 130 | global _api 131 | _symbols = _api.get_symbols() 132 | if _symbols['status'] == 'ok': 133 | for d in _symbols['data']: # 获取交易对信息 134 | name = d['base-currency'] + d['quote-currency'] 135 | self.add_symbol(HBSymbol(name, **d)) 136 | self.symbols.append(name) 137 | else: 138 | raise Exception(f'err-code:{_symbols["err-code"]} err-msg:{_symbols["err-msg"]}') 139 | 140 | def __repr__(self): 141 | return f':{self.symbols}' 142 | 143 | def __str__(self): 144 | return f':{self.symbols}' 145 | 146 | def __getitem__(self, item): 147 | return getattr(self, item) 148 | 149 | def __getattr__(self, item): 150 | global _api 151 | if item == 'all_24h_kline': 152 | return _api.get_all_last_24h_kline() 153 | 154 | 155 | class HBOrder: 156 | def __init__(self): 157 | ... 158 | 159 | def send(self, acc_id, amount, symbol, _type, price=0): 160 | ret = _api.send_order(acc_id, amount, symbol, _type, price) 161 | logger.debug(f'send_order_ret:{ret}') 162 | if ret and ret['status'] == 'ok': 163 | return ret['data'] 164 | else: 165 | raise Exception(f'send order request failed!--{ret}') 166 | 167 | def __add__(self, order_params): 168 | if isinstance(order_params, Iterable): 169 | return self.send(*order_params) 170 | 171 | return self.send(order_params['acc_id'], 172 | order_params['amount'], 173 | order_params['symbol'], 174 | order_params['type'], 175 | order_params['price']) 176 | 177 | def cancel(self, order_id): 178 | ret = _api.cancel_order(order_id) 179 | logger.debug(f'cancel_order_ret:{ret}') 180 | if ret and ret['status'] == 'ok': 181 | return ret['data'] 182 | else: 183 | raise Exception(f'cancel order request failed!--{ret}') 184 | 185 | def __sub__(self, oid_or_list): 186 | if isinstance(oid_or_list, Iterable): 187 | return self.batchcancel(oid_or_list) 188 | return self.cancel(oid_or_list) 189 | 190 | def batchcancel(self, order_ids:list): 191 | ret = _api.batchcancel_order(order_ids) 192 | logger.debug(f'batchcancel_order_ret:{ret}') 193 | if ret and ret['status'] == 'ok': 194 | return ret['data'] 195 | else: 196 | raise Exception(f'batchcancel order request failed!--{ret}') 197 | 198 | def get_by_id(self, order_id): 199 | oi_ret = _api.get_order_info(order_id, _async=True) 200 | mr_ret = _api.get_order_matchresults(order_id, _async=True) 201 | ret = _api.async_request([oi_ret, mr_ret]) 202 | logger.debug(f'get_order_ret:{ret}') 203 | d = dict() 204 | if all(ret): 205 | if ret[0]['status'] == 'ok': 206 | d.update({'order_info': ret[0]['data']}) 207 | else: 208 | d.update({'order_info':{}}) 209 | 210 | if ret[1]['status'] == 'ok': 211 | d.update({'match_result': ret[1]['data']}) 212 | else: 213 | d.update({'match_result': {}}) 214 | return d 215 | else: 216 | raise Exception(f'get order request failed!--{ret}') 217 | 218 | def get_by_symbol(self, symbol, states, types=None, start_date=None, end_date=None, _from=None, direct=None, size=None): 219 | ret = _api.get_orders_info(symbol, states, types, start_date, end_date, _from, direct, size) 220 | logger.debug(f'get_orders_ret:{ret}') 221 | if ret and ret['status'] == 'ok': 222 | data = ret['data'] 223 | df = pd.DataFrame(data).set_index('id') 224 | return df 225 | else: 226 | raise Exception(f'get orders request failed!--{ret}') 227 | 228 | def __getitem__(self, item): 229 | return self.get_by_id(item) 230 | 231 | 232 | class HBTrade: 233 | def __init__(self): 234 | ... 235 | 236 | def get_by_id(self, order_id): 237 | ret = _api.get_order_matchresults(order_id) 238 | logger.debug(f'trade_ret:{ret}') 239 | if ret and ret['status'] == 'ok': 240 | data = ret['data'] 241 | df = pd.DataFrame(data).set_index('id') 242 | return df 243 | else: 244 | raise Exception(f'trade results request failed!--{ret}') 245 | 246 | def get_by_symbol(self, symbol, types, start_date=None, end_date=None, _from=None, direct=None, size=None): 247 | ret = _api.get_orders_matchresults(symbol, types, start_date, end_date, _from, direct, size) 248 | logger.debug(f'trade_ret:{ret}') 249 | if ret and ret['status'] == 'ok': 250 | data = ret['data'] 251 | df = pd.DataFrame(data).set_index('id') 252 | return df 253 | else: 254 | raise Exception(f'trade results request failed!--{ret}') 255 | 256 | def __getitem__(self, item): 257 | return self.get_by_id(item) 258 | 259 | 260 | class HBAccount: 261 | def __init__(self): 262 | ret = _api.get_accounts() 263 | logger.debug(f'get_order_ret:{ret}') 264 | if ret and ret['status'] == 'ok': 265 | data = ret['data'] 266 | self.Detail = pd.DataFrame(data).set_index('id') 267 | else: 268 | raise Exception(f'get accounts request failed!--{ret}') 269 | 270 | self._balances = {} 271 | self._orders = {} 272 | self._trades = {} 273 | 274 | def __getattr__(self, item): 275 | try: 276 | args = item.split('_') 277 | if int(args[1]) in self.Detail.index.tolist(): 278 | if args[0] == 'balance': 279 | bal = HBBalance(args[1]) 280 | self._balances[bal.acc_id] = bal 281 | setattr(self.__class__, item, bal) 282 | return bal 283 | elif args[0] == 'order': 284 | order = HBOrder() 285 | setattr(self, 'order', order) 286 | return order 287 | elif args[0] == 'trade': 288 | trade = HBTrade() 289 | setattr(self, 'trade', trade) 290 | return trade 291 | else: 292 | raise AttributeError 293 | else: 294 | raise AttributeError 295 | except Exception as e: 296 | raise e 297 | 298 | def __repr__(self): 299 | return f'Detail:\n{self.Detail}' 300 | 301 | def __str__(self): 302 | return f'Detail:\n{self.Detail}' 303 | 304 | 305 | class HBBalance: 306 | def __init__(self, account_id): 307 | self.acc_id = account_id 308 | self.update() 309 | 310 | def update(self): 311 | ret = _api.get_balance(self.acc_id) 312 | if ret and ret['status'] == 'ok': 313 | data = ret['data'] 314 | self.Id = data['id'] 315 | self.Type = data['type'] 316 | self.State = data['state'] 317 | self.Detail = pd.DataFrame(data['list']).set_index('currency') 318 | else: 319 | raise Exception(f'get balance request failed--{ret}') 320 | 321 | def __get__(self, instance, owner): 322 | # bals = instance._balances 323 | # bals[self.acc_id] = self 324 | self.update() 325 | return self 326 | 327 | def __repr__(self): 328 | return f'ID:{self.Id} Type:{self.Type} State:{self.State}' 329 | 330 | def __str__(self): 331 | return f'ID:{self.Id} Type:{self.Type} State:{self.State}' 332 | 333 | def __getitem__(self, item): 334 | return self.Detail.loc[item] 335 | 336 | 337 | class HBMargin: 338 | def __init__(self): 339 | ... 340 | 341 | def transferIn(self, symbol, currency, amount): 342 | ret = _api.exchange_to_margin(symbol, currency, amount) 343 | logger.debug(f'transferIn_ret:{ret}') 344 | if ret and ret['status'] == 'ok': 345 | return ret['data'] 346 | else: 347 | raise Exception(f'transferIn request failed!--{ret}') 348 | 349 | def transferOut(self, symbol, currency, amount): 350 | ret = _api.exchange_to_margin(symbol, currency, amount) 351 | logger.debug(f'transferOut_ret:{ret}') 352 | if ret and ret['status'] == 'ok': 353 | return ret['data'] 354 | else: 355 | raise Exception(f'transferOut request failed!--{ret}') 356 | 357 | def applyLoan(self, symbol, currency, amount): 358 | ret = _api.apply_loan(symbol, currency, amount) 359 | logger.debug(f'apply_loan_ret:{ret}') 360 | if ret and ret['status'] == 'ok': 361 | return ret['data'] 362 | else: 363 | raise Exception(f'apply_loan request failed!--{ret}') 364 | 365 | def repayLoan(self, symbol, currency, amount): 366 | ret = _api.repay_loan(symbol, currency, amount) 367 | logger.debug(f'repay_loan_ret:{ret}') 368 | if ret and ret['status'] == 'ok': 369 | return ret['data'] 370 | else: 371 | raise Exception(f'repay_loan request failed!--{ret}') 372 | 373 | def getLoan(self, symbol, currency, states=None, start_date=None, end_date=None, _from=None, direct=None, size=None): 374 | ret = _api.get_loan_orders(symbol, currency, states, start_date, end_date, _from, direct, size) 375 | logger.debug(f'get_loan_ret:{ret}') 376 | if ret and ret['status'] == 'ok': 377 | df = pd.DataFrame(ret['data']).set_index('id') 378 | return df 379 | else: 380 | raise Exception(f'get_loan request failed!--{ret}') 381 | 382 | def getBalance(self, symbol): 383 | return HBMarginBalance(symbol) 384 | 385 | def __getitem__(self, item): 386 | return self.getBalance(item) 387 | 388 | class HBMarginBalance: 389 | def __init__(self, symbol): 390 | ret = _api.get_margin_balance(symbol) 391 | logger.debug(f'<保证金结余>信息:{ret}') 392 | if ret and ret['status'] == 'ok': 393 | balance = {} 394 | for d in ret['data']: 395 | data = balance.setdefault(d['id'], {}) 396 | data['id'] = d['id'] 397 | data['type'] = d['type'] 398 | data['state'] = d['state'] 399 | data['symbol'] = d['symbol'] 400 | data['fl-price'] = d['fl-price'] 401 | data['fl-type'] = d['fl-type'] 402 | data['risk-rate'] = d['risk-rate'] 403 | data['detail'] = pd.DataFrame(d['list']).set_index('currency') 404 | else: 405 | raise Exception(f'get balance request failed--{ret}') 406 | 407 | self.__balance = balance 408 | 409 | def __repr__(self): 410 | info = [] 411 | for b in self._balance.values(): 412 | info.append(f'ID:{b["id"]} Type:{b["type"]} State:{b["state"]} Risk-rate:{b["risk-rate"]}') 413 | info = '\n'.join(info) 414 | return info 415 | 416 | def __str__(self): 417 | info = [] 418 | for b in self.__balance: 419 | info.append(f'ID:{b["id"]} Type:{b["type"]} State:{b["state"]} Risk-rate:{b["risk-rate"]}') 420 | info = '\n'.join(info) 421 | return info 422 | 423 | def __getitem__(self, item): 424 | return self.__balance[item] 425 | 426 | @property 427 | def balance(self): 428 | return self.__balance -------------------------------------------------------------------------------- /HBVisual/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 火币交易可视化 8 | 9 | 10 | 11 | 12 | 13 | 43 | 44 | 45 | 69 | 70 |
71 |
72 |
73 |

Trading

74 | 95 | 96 |
97 |
98 |
99 | 100 |
101 | 102 | 111 | 112 | 113 | 114 |
115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 |
账户信息
账户ID币种账户类型账户余额
{[a['account-id']]}{[a.currency]}{[a.type]}{[a.balance]}
135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 |
交易信息
流水号订单ID交易对帐号ID订单数量订单价格创建时间类型来源状态Role成交价格成交数量未成交数量成交金额手续费
{[t['seq-id']]}{[t['order-id']]}{[t['symbol']]}{[t['account-id']]}{[t['order-amount']]}{[t['order-price']]}{[t['created-at']]}{[t['order-type']]}{[t['order-source']]}{[t['order-state']]}{[t['role']]}{[t['price']]}{[t['filled-amount']]}{[t['unfilled-amount']]}{[t['filled-cash-amount']]}{[t['filled-fees']]}
178 | 179 |
180 |
181 |
182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 |
203 |
204 | 205 |
206 |
207 |

huobitrade可视化@copyright 2018

208 |
209 |
210 | 211 | 212 | 527 | 528 | -------------------------------------------------------------------------------- /huobitrade/core.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2018/9/20 0020 9:23 4 | # @Author : Hadrianl 5 | # @File : core.py 6 | # @Contact : 137150224@qq.com 7 | 8 | 9 | import websocket as ws 10 | import gzip as gz 11 | import json 12 | from . import utils as u 13 | from .utils import logger, zmq_ctx 14 | from threading import Thread 15 | import datetime as dt 16 | from dateutil import parser 17 | from functools import wraps 18 | import zmq 19 | import pickle 20 | import time 21 | from abc import abstractmethod 22 | import uuid 23 | from .handler import BaseHandler 24 | from concurrent.futures import ThreadPoolExecutor 25 | 26 | logger.debug(f'LOG_TESTING') 27 | 28 | 29 | class BaseWebsocket(object): 30 | ws_count = 0 31 | def __new__(cls, *args, **kwargs): 32 | cls.ws_count += 1 33 | if cls is _AuthWS: 34 | from .utils import ACCESS_KEY, SECRET_KEY 35 | if not (ACCESS_KEY and SECRET_KEY): 36 | raise Exception('ACCESS_KEY或SECRET_KEY未设置!') 37 | 38 | return object.__new__(cls) 39 | 40 | def send_message(self, msg): # 发送消息 41 | msg_json = json.dumps(msg).encode() 42 | self.ws.send(msg_json) 43 | 44 | def on_message(self, _msg): # 接收ws的消息推送并处理,包括了pingpong,处理订阅列表,以及处理数据推送 45 | json_data = gz.decompress(_msg).decode() 46 | msg = json.loads(json_data) 47 | logger.debug(f'{msg}') 48 | 49 | @abstractmethod 50 | def pub_msg(self, msg): 51 | """核心的处理函数,如果是handle_func直接处理,如果是handler,推送到handler的队列""" 52 | raise NotImplementedError 53 | 54 | def on_error(self, error): 55 | logger.error(f'<错误>on_error:{error}') 56 | 57 | def on_close(self): 58 | logger.info(f'<连接>已断开与{self.addr}的连接') 59 | if not self._active: 60 | return 61 | 62 | if self._reconn > 0: 63 | logger.info(f'<连接>尝试与{self.addr}进行重连') 64 | self.__start() 65 | self._reconn -= 1 66 | time.sleep(self._interval) 67 | else: 68 | logger.info(f'<连接>尝试与{self.addr}进行重连') 69 | self.__start() 70 | time.sleep(self._interval) 71 | 72 | def on_open(self): 73 | self._active = True 74 | logger.info(f'<连接>建立与{self.addr}的连接') 75 | 76 | # ------------------- 注册回调处理函数 ------------------------------- 77 | def register_onRsp(self, req): 78 | """ 79 | 添加回调处理函数的装饰器 80 | :param req: 具体的topic,如 81 | :return: 82 | """ 83 | def wrapper(_callback): 84 | callbackList = self._req_callbacks.setdefault(req, []) 85 | callbackList.append(_callback) 86 | return _callback 87 | return wrapper 88 | 89 | def unregister_onRsp(self, req): 90 | return self._req_callbacks.pop(req) 91 | 92 | # ------------------------------------------------------------------ 93 | 94 | # ------------------------- 注册handler ----------------------------- 95 | def register_handler(self, handler): # 注册handler 96 | if handler not in self._handlers: 97 | self._handlers.append(handler) 98 | handler.start(self.name) 99 | 100 | def unregister_handler(self, handler): # 注销handler 101 | if handler in self._handlers: 102 | self._handlers.remove(handler) 103 | handler.stop(self.name) 104 | 105 | def __add__(self, handler): 106 | if isinstance(handler, BaseHandler): 107 | self.register_handler(handler) 108 | else: 109 | raise Exception('{handler} is not aHandler') 110 | 111 | return self 112 | 113 | 114 | def __sub__(self, handler): 115 | if isinstance(handler, BaseHandler): 116 | self.unregister_handler(handler) 117 | else: 118 | raise Exception('{handler} is not aHandler') 119 | 120 | return self 121 | # ----------------------------------------------------------------- 122 | 123 | # --------------------- 注册handle_func -------------------------- 124 | def register_handle_func(self, topic): # 注册handle_func 125 | def _wrapper(_handle_func): 126 | if topic not in self._handle_funcs: 127 | self._handle_funcs[topic] = [] 128 | self._handle_funcs[topic].append(_handle_func) 129 | return _handle_func 130 | 131 | return _wrapper 132 | 133 | def unregister_handle_func(self, _handle_func_name, topic): 134 | """ 注销handle_func """ 135 | handler_list = self._handle_funcs.get(topic, []) 136 | for i, h in enumerate(handler_list): 137 | if h is _handle_func_name or h.__name__ == _handle_func_name: 138 | handler_list.pop(i) 139 | 140 | if self._handle_funcs.get(topic) == []: 141 | self._handle_funcs.pop(topic) 142 | 143 | # ----------------------------------------------------------------- 144 | 145 | # --------------------- handle属性 -------------------------------- 146 | @property 147 | def handlers(self): 148 | return self._handlers 149 | 150 | @property 151 | def handle_funcs(self): 152 | return self._handle_funcs 153 | 154 | @property 155 | def OnRsp_callbacks(self): 156 | return self._req_callbacks 157 | # ----------------------------------------------------------------- 158 | 159 | 160 | # -------------------------开关ws----------------------------------------- 161 | def run(self): 162 | if not hasattr(self, 'ws_thread') or not self.ws_thread.is_alive(): 163 | self.__start() 164 | 165 | def __start(self): 166 | self.ws = ws.WebSocketApp( 167 | self.addr, 168 | on_open=self.on_open, 169 | on_message=self.on_message, 170 | on_error=self.on_error, 171 | on_close=self.on_close, 172 | # on_data=self.on_data 173 | ) 174 | self.ws_thread = Thread(target=self.ws.run_forever, name=self.name) 175 | self.ws_thread.setDaemon(True) 176 | self.ws_thread.start() 177 | 178 | def stop(self): 179 | if hasattr(self, 'ws_thread') and self.ws_thread.is_alive(): 180 | self._active = False 181 | self.ws.close() 182 | # self.ws_thread.join() 183 | # ------------------------------------------------------------------------ 184 | 185 | 186 | class _AuthWS(BaseWebsocket): 187 | def __init__(self, host='api.huobi.br.com', 188 | reconn=10, interval=3): 189 | self._protocol = 'wss://' 190 | self._host = host 191 | self._path = '/ws/v1' 192 | self.addr = self._protocol + self._host + self._path 193 | self._threadPool = ThreadPoolExecutor(max_workers=3) 194 | # self.name = f'HuoBiAuthWS{self.ws_count}' 195 | self.name = f'HuoBiAuthWS_{uuid.uuid1()}' 196 | self.sub_dict = {} # 订阅列表 197 | self._handlers = [] # 对message做处理的处理函数或处理类 198 | self._req_callbacks = {} 199 | self._handle_funcs = {} 200 | self._auth_callbacks = [] 201 | self.ctx = zmq_ctx 202 | self.pub_socket = self.ctx.socket(zmq.PUB) 203 | self.pub_socket.bind(f'inproc://{self.name}') 204 | self._active = False 205 | self._reconn = reconn 206 | self._interval = interval 207 | 208 | def on_open(self): 209 | self._active = True 210 | logger.info(f'<连接>建立与{self.addr}的连接') 211 | self.auth() 212 | logger.info(f'<鉴权>向{self.addr}发起鉴权请求') 213 | 214 | def on_message(self, _msg): # 鉴权ws的消息处理 215 | json_data = gz.decompress(_msg).decode() 216 | msg = json.loads(json_data) 217 | logger.debug(f'{msg}') 218 | op = msg['op'] 219 | if op == 'ping': 220 | pong = {'op': 'pong', 'ts': msg['ts']} 221 | self.send_message(pong) 222 | if msg.setdefault('err-code', 0) == 0: 223 | if op == 'notify': 224 | self.pub_msg(msg) 225 | elif op == 'sub': 226 | logger.info( 227 | f'<订阅>Topic:{msg["topic"]}订阅成功 Time:{dt.datetime.fromtimestamp(msg["ts"] / 1000)} #{msg["cid"]}#') 228 | elif op == 'unsub': 229 | logger.info( 230 | f'<订阅>Topic:{msg["topic"]}取消订阅成功 Time:{dt.datetime.fromtimestamp(msg["ts"] / 1000)} #{msg["cid"]}#') 231 | elif op == 'req': 232 | logger.info(f'<请求>Topic:{msg["topic"]}请求数据成功 #{msg["cid"]}#') 233 | OnRsp = self._req_callbacks.get(msg['topic'], []) 234 | 235 | def callbackThread(_m): 236 | for cb in OnRsp: 237 | try: 238 | cb(_m) 239 | except Exception as e: 240 | logger.error(f'<请求回调>{msg["topic"]}的回调函数{cb.__name__}异常-{e}') 241 | 242 | task = self._threadPool.submit(callbackThread, msg) 243 | # _t = Thread(target=callbackThread, args=(msg,)) 244 | # _t.setDaemon(True) 245 | # _t.start() 246 | elif op == 'auth': 247 | logger.info( 248 | f'<鉴权>鉴权成功 Time:{dt.datetime.fromtimestamp(msg["ts"] / 1000)} #{msg["cid"]}#') 249 | for cb in self._auth_callbacks: 250 | cb() 251 | 252 | else: 253 | logger.error( 254 | f'<错误>{msg.get("cid")}-OP:{op} ErrTime:{dt.datetime.fromtimestamp(msg["ts"] / 1000)} ErrCode:{msg["err-code"]} ErrMsg:{msg["err-msg"]}' 255 | ) 256 | 257 | def pub_msg(self, msg): 258 | """核心的处理函数,如果是handle_func直接处理,如果是handler,推送到handler的队列""" 259 | topic = msg.get('topic') 260 | self.pub_socket.send_multipart( 261 | [pickle.dumps(topic), pickle.dumps(msg)]) 262 | 263 | for h in self._handle_funcs.get(topic, []): 264 | h(msg) 265 | 266 | def auth(self, cid:str =''): 267 | from .utils import ACCESS_KEY, SECRET_KEY, createSign 268 | timestamp = dt.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S') 269 | params = { 270 | "AccessKeyId": ACCESS_KEY, 271 | "SignatureMethod": "HmacSHA256", 272 | "SignatureVersion": "2", 273 | "Timestamp": timestamp,} 274 | 275 | signature = createSign(params, 'GET', self._host, self._path, SECRET_KEY) 276 | params['Signature'] = signature 277 | params['op'] = 'auth' 278 | params['cid'] = cid 279 | self.send_message(params) 280 | return 'auth', cid 281 | 282 | def sub_accounts(self, cid:str=''): 283 | msg = {'op': 'sub', 'cid': cid, 'topic': 'accounts'} 284 | self.send_message(msg) 285 | logger.info(f'<订阅>accouts-发送订阅请求 #{cid}#') 286 | return msg['topic'], cid 287 | 288 | def unsub_accounts(self, cid:str=''): 289 | msg = {'op': 'unsub', 'cid': cid, 'topic': 'accounts'} 290 | self.send_message(msg) 291 | logger.info(f'<订阅>accouts-发送订阅取消请求 #{cid}#') 292 | return msg['topic'], cid 293 | 294 | def sub_orders(self, symbol='*', cid:str=''): 295 | """ 296 | 297 | :param symbol: '*'为订阅所有订单变化 298 | :param cid: 299 | :return: 300 | """ 301 | msg = {'op': 'sub', 'cid': cid, 'topic': f'orders.{symbol}'} 302 | self.send_message(msg) 303 | logger.info(f'<订阅>orders-发送订阅请求*{symbol}* #{cid}#') 304 | return msg['topic'], cid 305 | 306 | def unsub_orders(self, symbol='*', cid:str=''): 307 | """ 308 | 309 | :param symbol: '*'为订阅所有订单变化 310 | :param cid: 311 | :return: 312 | """ 313 | msg = {'op': 'unsub', 'cid': cid, 'topic': f'orders.{symbol}'} 314 | self.send_message(msg) 315 | logger.info(f'<订阅>orders-发送取消订阅请求*{symbol}* #{cid}#') 316 | return msg['topic'], cid 317 | 318 | # ------------------------------------------------------------------------ 319 | # ----------------------帐户请求函数-------------------------------------- 320 | def req_accounts(self, cid:str=''): 321 | msg = {'op': 'req', 'cid': cid, 'topic': 'accounts.list'} 322 | self.send_message(msg) 323 | logger.info(f'<请求>accounts-发送请求 #{cid}#') 324 | return msg['topic'], cid 325 | 326 | def req_orders(self, acc_id, symbol, states:list, 327 | types:list=None, 328 | start_date=None, end_date=None, 329 | _from=None, direct=None, 330 | size=None, cid:str=''): 331 | states = ','.join(states) 332 | msg = {'op': 'req', 'account-id': acc_id, 'symbol': symbol, 'states': states, 'cid': cid, 333 | 'topic': 'orders.list'} 334 | if types: 335 | types = ','.join(types) 336 | msg['types'] = types 337 | 338 | if start_date: 339 | start_date = parser.parse(start_date).strftime('%Y-%m-%d') 340 | msg['start-date'] = start_date 341 | 342 | if end_date: 343 | end_date = parser.parse(end_date).strftime('%Y-%m-%d') 344 | msg['end-date'] = end_date 345 | 346 | if _from: 347 | msg['_from'] = _from 348 | 349 | if direct: 350 | msg['direct'] = direct 351 | 352 | if size: 353 | msg['size'] = size 354 | 355 | self.send_message(msg) 356 | logger.info(f'<请求>orders-发送请求 #{cid}#') 357 | return msg['topic'], cid 358 | 359 | def req_orders_detail(self, order_id, cid:str=''): 360 | msg = {'op': 'req', 'order-id': order_id, 'cid': cid, 'topic': 'orders.detail'} 361 | self.send_message(msg) 362 | logger.info(f'<请求>accounts-发送请求 #{cid}#') 363 | return msg['topic'], cid 364 | 365 | def after_auth(self,_func): # ws开启之后需要完成的初始化处理 366 | @wraps(_func) 367 | def _callback(): 368 | try: 369 | _func() 370 | except Exception as e: 371 | logger.exception(f'afer_open回调处理错误{e}') 372 | self._auth_callbacks.append(_callback) 373 | 374 | return _callback 375 | 376 | 377 | class _HBWS(BaseWebsocket): 378 | def __init__(self, host='api.huobi.br.com', 379 | reconn=10, interval=3): 380 | self._protocol = 'wss://' 381 | self._host = host 382 | self._path = '/ws' 383 | self.addr = self._protocol + self._host + self._path 384 | self._threadPool = ThreadPoolExecutor(max_workers=3) 385 | # self.name = f'HuoBiWS{self.ws_count}' 386 | self.name = f'HuoBiWS_{uuid.uuid1()}' 387 | self.sub_dict = {} # 订阅列表 388 | self._handlers = [] # 对message做处理的处理函数或处理类 389 | self._req_callbacks = {} 390 | self._handle_funcs = {} 391 | self._open_callbacks = [] 392 | self.ctx = zmq_ctx 393 | self.pub_socket = self.ctx.socket(zmq.PUB) 394 | self.pub_socket.bind(f'inproc://{self.name}') 395 | self._active = False 396 | self._reconn = reconn 397 | self._interval = interval 398 | 399 | def on_open(self): 400 | self._active = True 401 | logger.info(f'<连接>建立与{self.addr}的连接') 402 | for topic, subbed in self.sub_dict.items(): 403 | msg = {'sub': subbed['topic'], 'id': subbed['id']} 404 | self.send_message(msg) 405 | else: 406 | logger.info(f'<订阅>初始化订阅完成') 407 | 408 | for fun in self._open_callbacks: 409 | fun() 410 | 411 | def on_message(self, _msg): # 接收ws的消息推送并处理,包括了pingpong,处理订阅列表,以及处理数据推送 412 | json_data = gz.decompress(_msg).decode() 413 | msg = json.loads(json_data) 414 | logger.debug(f'{msg}') 415 | if 'ping' in msg: 416 | pong = {'pong': msg['ping']} 417 | self.send_message(pong) 418 | elif 'status' in msg: 419 | if msg['status'] == 'ok': 420 | if 'subbed' in msg: 421 | self.sub_dict.update({ 422 | msg['subbed']: { 423 | 'topic': msg['subbed'], 424 | 'id': msg['id'] 425 | } 426 | }) 427 | logger.info( 428 | f'<订阅>Topic:{msg["subbed"]}订阅成功 Time:{dt.datetime.fromtimestamp(msg["ts"] / 1000)} #{msg["id"]}#' 429 | ) 430 | elif 'unsubbed' in msg: 431 | self.sub_dict.pop(msg['unsubbed']) 432 | logger.info( 433 | f'<订阅>Topic:{msg["unsubbed"]}取消订阅成功 Time:{dt.datetime.fromtimestamp(msg["ts"] / 1000)} #{msg["id"]}#' 434 | ) 435 | elif 'rep' in msg: 436 | logger.info(f'<请求>Topic:{msg["rep"]}请求数据成功 #{msg["id"]}#') 437 | OnRsp = self._req_callbacks.get(msg['rep'], []) 438 | def callbackThread(_m): 439 | for cb in OnRsp: 440 | try: 441 | cb(_m) 442 | except Exception as e: 443 | logger.error(f'<请求回调>{msg["rep"]}的回调函数{cb.__name__}异常-{e}') 444 | 445 | task = self._threadPool.submit(callbackThread, msg) 446 | elif 'data' in msg: 447 | self.pub_msg(msg) 448 | # _t = Thread(target=callbackThread, args=(msg, )) 449 | # _t.setDaemon(True) 450 | # _t.start() 451 | elif msg['status'] == 'error': 452 | logger.error( 453 | f'<错误>{msg.get("id")}-ErrTime:{dt.datetime.fromtimestamp(msg["ts"] / 1000)} ErrCode:{msg["err-code"]} ErrMsg:{msg["err-msg"]}' 454 | ) 455 | else: 456 | self.pub_msg(msg) 457 | 458 | def pub_msg(self, msg): 459 | """核心的处理函数,如果是handle_func直接处理,如果是handler,推送到handler的队列""" 460 | if 'ch' in msg: 461 | topic = msg.get('ch') 462 | self.pub_socket.send_multipart( 463 | [pickle.dumps(topic), pickle.dumps(msg)]) 464 | 465 | for h in self._handle_funcs.get(topic, []): 466 | h(msg) 467 | 468 | @staticmethod 469 | def _check_info(**kwargs): 470 | log = [] 471 | if 'period' in kwargs and kwargs['period'] not in u.PERIOD: 472 | log.append(f'<验证>不存在Period:{kwargs["period"]}') 473 | 474 | if 'depth' in kwargs and kwargs['depth'] not in u.DEPTH: 475 | log.append(f'<验证>不存在Depth:{kwargs["depth"]}') 476 | 477 | if log: 478 | for l in log: 479 | logger.warning(l) 480 | return False 481 | else: 482 | return True 483 | 484 | # ----------------------行情订阅函数--------------------------------------- 485 | def sub_overview(self, _id=''): 486 | msg = {'sub': 'market.overview', 'id': _id} 487 | self.send_message(msg) 488 | logger.info(f'<订阅>overview-发送订阅请求 #{_id}#') 489 | return msg['sub'], _id 490 | 491 | def unsub_overview(self, _id=''): 492 | msg = {'unsub': 'market.overview', 'id': _id} 493 | self.send_message(msg) 494 | logger.info(f'<订阅>overview-发送取消订阅请求 #{_id}#') 495 | return msg['unsub'], _id 496 | 497 | def sub_kline(self, symbol, period, _id=''): 498 | if self._check_info(symbol=symbol, period=period): 499 | msg = {'sub': f'market.{symbol}.kline.{period}', 'id': _id} 500 | self.send_message(msg) 501 | logger.info(f'<订阅>kline-发送订阅请求*{symbol}*@{period} #{_id}#') 502 | return msg['sub'], _id 503 | 504 | def unsub_kline(self, symbol, period, _id=''): 505 | if self._check_info(symbol=symbol, period=period): 506 | msg = {'unsub': f'market.{symbol}.kline.{period}', 'id': _id} 507 | self.send_message(msg) 508 | logger.info(f'<订阅>kline-发送取消订阅请求*{symbol}*@{period} #{_id}#') 509 | return msg['unsub'], _id 510 | 511 | def sub_depth(self, symbol, depth=0, _id=''): 512 | if self._check_info(symbol=symbol, depth=depth): 513 | msg = {'sub': f'market.{symbol}.depth.{u.DEPTH[depth]}', 'id': _id} 514 | self.send_message(msg) 515 | logger.info(f'<订阅>depth-发送订阅请求*{symbol}*@{u.DEPTH[depth]} #{_id}#') 516 | return msg['sub'], _id 517 | 518 | def unsub_depth(self, symbol, depth=0, _id=''): 519 | if self._check_info(symbol=symbol, depth=depth): 520 | msg = { 521 | 'unsub': f'market.{symbol}.depth.{u.DEPTH[depth]}', 522 | 'id': _id 523 | } 524 | self.send_message(msg) 525 | logger.info( 526 | f'<订阅>depth-发送取消订阅请求*{symbol}*@{u.DEPTH[depth]} #{_id}#') 527 | return msg['unsub'], _id 528 | 529 | def sub_tick(self, symbol, _id=''): 530 | if self._check_info(symbol=symbol): 531 | msg = {'sub': f'market.{symbol}.trade.detail', 'id': _id} 532 | self.send_message(msg) 533 | logger.info(f'<订阅>tick-发送订阅请求*{symbol}* #{_id}#') 534 | return msg['sub'], _id 535 | 536 | def unsub_tick(self, symbol, _id=''): 537 | if self._check_info(symbol=symbol): 538 | msg = {'unsub': f'market.{symbol}.trade.detail', 'id': _id} 539 | self.send_message(msg) 540 | logger.info(f'<订阅>tick-发送取消订阅请求*{symbol}* #{_id}#') 541 | return msg['unsub'], _id 542 | 543 | def sub_all_lastest_24h_ohlc(self, _id=''): 544 | msg = {'sub': f'market.tickers', 'id': _id} 545 | self.send_message(msg) 546 | logger.info(f'<订阅>all_ticks-发送订阅请求 #{_id}#') 547 | return msg['sub'], _id 548 | 549 | def unsub_all_lastest_24h_ohlc(self, _id=''): 550 | msg = {'unsub': f'market.tickers', 'id': _id} 551 | self.send_message(msg) 552 | logger.info(f'<订阅>all_ticks-发送取消订阅请求 #{_id}#') 553 | return msg['unsub'], _id 554 | # ------------------------------------------------------------------------- 555 | 556 | # -------------------------行情请求函数---------------------------------------- 557 | def req_kline(self, symbol, period, _id='', **kwargs): 558 | if self._check_info(symbol=symbol, period=period): 559 | msg = {'req': f'market.{symbol}.kline.{period}', 'id': _id} 560 | if '_from' in kwargs: 561 | _from = parser.parse(kwargs['_from']).timestamp() if isinstance( 562 | kwargs['_from'], str) else kwargs['_from'] 563 | msg.update({'from': int(_from)}) 564 | if '_to' in kwargs: 565 | _to = parser.parse(kwargs['_to']).timestamp() if isinstance( 566 | kwargs['_to'], str) else kwargs['_to'] 567 | msg.update({'to': int(_to)}) 568 | self.send_message(msg) 569 | logger.info(f'<请求>kline-发送请求*{symbol}*@{period} #{_id}#') 570 | return msg['req'], _id 571 | 572 | def req_depth(self, symbol, depth=0, _id=''): 573 | if self._check_info(depth=depth): 574 | msg = {'req': f'market.{symbol}.depth.{u.DEPTH[depth]}', 'id': _id} 575 | self.send_message(msg) 576 | logger.info(f'<请求>depth-发送请求*{symbol}*@{u.DEPTH[depth]} #{_id}#') 577 | return msg['req'], _id 578 | 579 | def req_tick(self, symbol, _id=''): 580 | msg = {'req': f'market.{symbol}.trade.detail', 'id': _id} 581 | self.send_message(msg) 582 | logger.info(f'<请求>tick-发送请求*{symbol}* #{_id}#') 583 | return msg['req'], _id 584 | 585 | def req_symbol(self, symbol, _id=''): 586 | msg = {'req': f'market.{symbol}.detail', 'id': _id} 587 | self.send_message(msg) 588 | logger.info(f'<请求>symbol-发送请求*{symbol}* #{_id}#') 589 | return msg['req'], _id 590 | 591 | # ------------------------------------------------------------------------- 592 | 593 | def after_open(self,_func): # ws开启之后需要完成的初始化处理 594 | @wraps(_func) 595 | def _callback(): 596 | try: 597 | _func() 598 | except Exception as e: 599 | logger.exception(f'afer_open回调处理错误{e}') 600 | self._open_callbacks.append(_callback) 601 | 602 | return _callback 603 | 604 | 605 | class _HBDerivativesWS(BaseWebsocket): 606 | def __init__(self, host='www.hbdm.com', 607 | reconn=10, interval=3): 608 | self._protocol = 'wss://' 609 | self._host = host 610 | self._path = '/ws' 611 | self.addr = self._protocol + self._host + self._path 612 | self._threadPool = ThreadPoolExecutor(max_workers=3) 613 | # self.name = f'HuoBiWS{self.ws_count}' 614 | self.name = f'HuoBiDerivativesWS_{uuid.uuid1()}' 615 | self.sub_dict = {} # 订阅列表 616 | self._handlers = [] # 对message做处理的处理函数或处理类 617 | self._req_callbacks = {} 618 | self._handle_funcs = {} 619 | self._open_callbacks = [] 620 | self.ctx = zmq_ctx 621 | self.pub_socket = self.ctx.socket(zmq.PUB) 622 | self.pub_socket.bind(f'inproc://{self.name}') 623 | self._active = False 624 | self._reconn = reconn 625 | self._interval = interval 626 | 627 | def on_open(self): 628 | self._active = True 629 | logger.info(f'<连接>建立与{self.addr}的连接') 630 | for topic, subbed in self.sub_dict.items(): 631 | msg = {'sub': subbed['topic'], 'id': subbed['id']} 632 | self.send_message(msg) 633 | else: 634 | logger.info(f'<订阅>初始化订阅完成') 635 | 636 | for fun in self._open_callbacks: 637 | fun() 638 | 639 | def on_message(self, _msg): # 接收ws的消息推送并处理,包括了pingpong,处理订阅列表,以及处理数据推送 640 | json_data = gz.decompress(_msg).decode() 641 | msg = json.loads(json_data) 642 | logger.debug(f'{msg}') 643 | if 'ping' in msg: 644 | pong = {'pong': msg['ping']} 645 | self.send_message(pong) 646 | elif 'status' in msg: 647 | if msg['status'] == 'ok': 648 | if 'subbed' in msg: 649 | self.sub_dict.update({ 650 | msg['subbed']: { 651 | 'topic': msg['subbed'], 652 | 'id': msg['id'] 653 | } 654 | }) 655 | logger.info( 656 | f'<订阅>Topic:{msg["subbed"]}订阅成功 Time:{dt.datetime.fromtimestamp(msg["ts"] / 1000)} #{msg["id"]}#' 657 | ) 658 | elif 'unsubbed' in msg: 659 | self.sub_dict.pop(msg['unsubbed']) 660 | logger.info( 661 | f'<订阅>Topic:{msg["unsubbed"]}取消订阅成功 Time:{dt.datetime.fromtimestamp(msg["ts"] / 1000)} #{msg["id"]}#' 662 | ) 663 | elif 'rep' in msg: 664 | logger.info(f'<请求>Topic:{msg["rep"]}请求数据成功 #{msg["id"]}#') 665 | OnRsp = self._req_callbacks.get(msg['rep'], []) 666 | def callbackThread(_m): 667 | for cb in OnRsp: 668 | try: 669 | cb(_m) 670 | except Exception as e: 671 | logger.error(f'<请求回调>{msg["rep"]}的回调函数{cb.__name__}异常-{e}') 672 | 673 | task = self._threadPool.submit(callbackThread, msg) 674 | elif 'data' in msg: 675 | self.pub_msg(msg) 676 | # _t = Thread(target=callbackThread, args=(msg, )) 677 | # _t.setDaemon(True) 678 | # _t.start() 679 | elif msg['status'] == 'error': 680 | logger.error( 681 | f'<错误>{msg.get("id")}-ErrTime:{dt.datetime.fromtimestamp(msg["ts"] / 1000)} ErrCode:{msg["err-code"]} ErrMsg:{msg["err-msg"]}' 682 | ) 683 | else: 684 | self.pub_msg(msg) 685 | 686 | 687 | def pub_msg(self, msg): 688 | """核心的处理函数,如果是handle_func直接处理,如果是handler,推送到handler的队列""" 689 | if 'ch' in msg: 690 | topic = msg.get('ch') 691 | self.pub_socket.send_multipart( 692 | [pickle.dumps(topic), pickle.dumps(msg)]) 693 | 694 | for h in self._handle_funcs.get(topic, []): 695 | h(msg) 696 | 697 | @staticmethod 698 | def _check_info(**kwargs): 699 | log = [] 700 | if 'period' in kwargs and kwargs['period'] not in u.PERIOD: 701 | log.append(f'<验证>不存在Period:{kwargs["period"]}') 702 | 703 | if 'depth' in kwargs and kwargs['depth'] not in u.DerivativesDEPTH: 704 | log.append(f'<验证>不存在Depth:{kwargs["depth"]}') 705 | 706 | if log: 707 | for l in log: 708 | logger.warning(l) 709 | return False 710 | else: 711 | return True 712 | 713 | 714 | def sub_kline(self, symbol, period, _id=''): 715 | if self._check_info(symbol=symbol, period=period): 716 | msg = {'sub': f'market.{symbol}.kline.{period}', 'id': _id} 717 | self.send_message(msg) 718 | logger.info(f'<订阅>kline-发送订阅请求*{symbol}*@{period} #{_id}#') 719 | return msg['sub'], _id 720 | 721 | def unsub_kline(self, symbol, period, _id=''): 722 | if self._check_info(symbol=symbol, period=period): 723 | msg = {'unsub': f'market.{symbol}.kline.{period}', 'id': _id} 724 | self.send_message(msg) 725 | logger.info(f'<订阅>kline-发送取消订阅请求*{symbol}*@{period} #{_id}#') 726 | return msg['unsub'], _id 727 | 728 | def sub_depth(self, symbol, depth=0, _id=''): 729 | if self._check_info(symbol=symbol, depth=depth): 730 | msg = {'sub': f'market.{symbol}.depth.{u.DEPTH[depth]}', 'id': _id} 731 | self.send_message(msg) 732 | logger.info(f'<订阅>depth-发送订阅请求*{symbol}*@{u.DEPTH[depth]} #{_id}#') 733 | return msg['sub'], _id 734 | 735 | def unsub_depth(self, symbol, depth=0, _id=''): 736 | if self._check_info(symbol=symbol, depth=depth): 737 | msg = { 738 | 'unsub': f'market.{symbol}.depth.{u.DEPTH[depth]}', 739 | 'id': _id 740 | } 741 | self.send_message(msg) 742 | logger.info( 743 | f'<订阅>depth-发送取消订阅请求*{symbol}*@{u.DEPTH[depth]} #{_id}#') 744 | return msg['unsub'], _id 745 | 746 | def sub_last_24h_kline(self, symbol, _id=''): 747 | msg = {'sub': f'market.{symbol}.detail', 'id': _id} 748 | self.send_message(msg) 749 | logger.info(f'<订阅>Last_24h_kline-发送订阅请求*{symbol}* #{_id}#') 750 | return msg['sub'], _id 751 | 752 | def unsub_last_24h_kline(self, symbol, _id=''): 753 | msg = { 754 | 'unsub': f'market.{symbol}.detail', 755 | 'id': _id 756 | } 757 | self.send_message(msg) 758 | logger.info( 759 | f'<订阅>Last_24h_kline-发送取消订阅请求*{symbol}* #{_id}#') 760 | return msg['unsub'], _id 761 | 762 | 763 | def sub_tick(self, symbol, _id=''): 764 | if self._check_info(symbol=symbol): 765 | msg = {'sub': f'market.{symbol}.trade.detail', 'id': _id} 766 | self.send_message(msg) 767 | logger.info(f'<订阅>tick-发送订阅请求*{symbol}* #{_id}#') 768 | return msg['sub'], _id 769 | 770 | def unsub_tick(self, symbol, _id=''): 771 | if self._check_info(symbol=symbol): 772 | msg = {'unsub': f'market.{symbol}.trade.detail', 'id': _id} 773 | self.send_message(msg) 774 | logger.info(f'<订阅>tick-发送取消订阅请求*{symbol}* #{_id}#') 775 | return msg['unsub'], _id 776 | 777 | # ------------------------------------------------------------------------- 778 | 779 | # -------------------------行情请求函数---------------------------------------- 780 | def req_kline(self, symbol, period, _id='', **kwargs): 781 | if self._check_info(symbol=symbol, period=period): 782 | msg = {'req': f'market.{symbol}.kline.{period}', 'id': _id} 783 | if '_from' in kwargs: 784 | _from = parser.parse(kwargs['_from']).timestamp() if isinstance( 785 | kwargs['_from'], str) else kwargs['_from'] 786 | msg.update({'from': int(_from)}) 787 | if '_to' in kwargs: 788 | _to = parser.parse(kwargs['_to']).timestamp() if isinstance( 789 | kwargs['_to'], str) else kwargs['_to'] 790 | msg.update({'to': int(_to)}) 791 | self.send_message(msg) 792 | logger.info(f'<请求>kline-发送请求*{symbol}*@{period} #{_id}#') 793 | return msg['req'], _id 794 | 795 | def req_tick(self, symbol, _id=''): 796 | msg = {'req': f'market.{symbol}.trade.detail', 'id': _id} 797 | self.send_message(msg) 798 | logger.info(f'<请求>tick-发送请求*{symbol}* #{_id}#') 799 | return msg['req'], _id 800 | 801 | # ------------------------------------------------------------------------- 802 | 803 | def after_open(self,_func): # ws开启之后需要完成的初始化处理 804 | @wraps(_func) 805 | def _callback(): 806 | try: 807 | _func() 808 | except Exception as e: 809 | logger.exception(f'afer_open回调处理错误{e}') 810 | self._open_callbacks.append(_callback) 811 | 812 | return _callback 813 | 814 | 815 | class _DerivativesAuthWS(BaseWebsocket): 816 | def __init__(self, host='api.hbdm.com', 817 | reconn=10, interval=3): 818 | self._protocol = 'wss://' 819 | self._host = host 820 | self._path = '/notification' 821 | self.addr = self._protocol + self._host + self._path 822 | self._threadPool = ThreadPoolExecutor(max_workers=3) 823 | self.name = f'HuoBiDerivativesAuthWS_{uuid.uuid1()}' 824 | self.sub_dict = {} # 订阅列表 825 | self._handlers = [] # 对message做处理的处理函数或处理类 826 | self._req_callbacks = {} 827 | self._handle_funcs = {} 828 | self._auth_callbacks = [] 829 | self.ctx = zmq_ctx 830 | self.pub_socket = self.ctx.socket(zmq.PUB) 831 | self.pub_socket.bind(f'inproc://{self.name}') 832 | self._active = False 833 | self._reconn = reconn 834 | self._interval = interval 835 | 836 | def on_open(self): 837 | self._active = True 838 | logger.info(f'<连接>建立与{self.addr}的连接') 839 | self.auth() 840 | logger.info(f'<鉴权>向{self.addr}发起鉴权请求') 841 | 842 | def on_message(self, _msg): # 鉴权ws的消息处理 843 | json_data = gz.decompress(_msg).decode() 844 | msg = json.loads(json_data) 845 | logger.debug(f'{msg}') 846 | op = msg['op'] 847 | if op == 'ping': 848 | pong = {'op': 'pong', 'ts': msg['ts']} 849 | self.send_message(pong) 850 | if msg.setdefault('err-code', 0) == 0: 851 | if op == 'notify': 852 | self.pub_msg(msg) 853 | elif op == 'sub': 854 | logger.info( 855 | f'<订阅>Topic:{msg["topic"]}订阅成功 Time:{dt.datetime.fromtimestamp(msg["ts"] / 1000)} #{msg["cid"]}#') 856 | elif op == 'unsub': 857 | logger.info( 858 | f'<订阅>Topic:{msg["topic"]}取消订阅成功 Time:{dt.datetime.fromtimestamp(msg["ts"] / 1000)} #{msg["cid"]}#') 859 | elif op == 'req': 860 | logger.info(f'<请求>Topic:{msg["topic"]}请求数据成功 #{msg["cid"]}#') 861 | OnRsp = self._req_callbacks.get(msg['topic'], []) 862 | 863 | def callbackThread(_m): 864 | for cb in OnRsp: 865 | try: 866 | cb(_m) 867 | except Exception as e: 868 | logger.error(f'<请求回调>{msg["topic"]}的回调函数{cb.__name__}异常-{e}') 869 | 870 | task = self._threadPool.submit(callbackThread, msg) 871 | # _t = Thread(target=callbackThread, args=(msg,)) 872 | # _t.setDaemon(True) 873 | # _t.start() 874 | elif op == 'auth': 875 | logger.info( 876 | f'<鉴权>鉴权成功 Time:{dt.datetime.fromtimestamp(msg["ts"] / 1000)}') 877 | for cb in self._auth_callbacks: 878 | cb() 879 | 880 | else: 881 | logger.error( 882 | f'<错误>{msg.get("cid")}-OP:{op} ErrTime:{dt.datetime.fromtimestamp(msg["ts"] / 1000)} ErrCode:{msg["err-code"]} ErrMsg:{msg["err-msg"]}' 883 | ) 884 | 885 | def pub_msg(self, msg): 886 | """核心的处理函数,如果是handle_func直接处理,如果是handler,推送到handler的队列""" 887 | topic = msg.get('topic') 888 | self.pub_socket.send_multipart( 889 | [pickle.dumps(topic), pickle.dumps(msg)]) 890 | 891 | for h in self._handle_funcs.get(topic, []): 892 | h(msg) 893 | 894 | def auth(self, cid:str =''): 895 | from .utils import ACCESS_KEY, SECRET_KEY, createSign 896 | timestamp = dt.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S') 897 | params = { 898 | "AccessKeyId": ACCESS_KEY, 899 | "SignatureMethod": "HmacSHA256", 900 | "SignatureVersion": "2", 901 | "Timestamp": timestamp,} 902 | 903 | signature = createSign(params, 'GET', self._host, self._path, SECRET_KEY) 904 | params['Signature'] = signature 905 | params['op'] = 'auth' 906 | params['cid'] = cid 907 | params['type'] = 'api' 908 | self.send_message(params) 909 | return 'auth', cid 910 | 911 | # def sub_accounts(self, cid:str=''): 912 | # msg = {'op': 'sub', 'cid': cid, 'topic': 'accounts'} 913 | # self.send_message(msg) 914 | # logger.info(f'<订阅>accouts-发送订阅请求 #{cid}#') 915 | # return msg['topic'], cid 916 | # 917 | # def unsub_accounts(self, cid:str=''): 918 | # msg = {'op': 'unsub', 'cid': cid, 'topic': 'accounts'} 919 | # self.send_message(msg) 920 | # logger.info(f'<订阅>accouts-发送订阅取消请求 #{cid}#') 921 | # return msg['topic'], cid 922 | 923 | def sub_orders(self, symbol='*', cid:str=''): 924 | """ 925 | 926 | :param symbol: '*'为订阅所有订单变化 927 | :param cid: 928 | :return: 929 | """ 930 | msg = {'op': 'sub', 'cid': cid, 'topic': f'orders.{symbol}'} 931 | self.send_message(msg) 932 | logger.info(f'<订阅>orders-发送订阅请求*{symbol}* #{cid}#') 933 | return msg['topic'], cid 934 | 935 | def unsub_orders(self, symbol='*', cid:str=''): 936 | """ 937 | 938 | :param symbol: '*'为订阅所有订单变化 939 | :param cid: 940 | :return: 941 | """ 942 | msg = {'op': 'unsub', 'cid': cid, 'topic': f'orders.{symbol}'} 943 | self.send_message(msg) 944 | logger.info(f'<订阅>orders-发送取消订阅请求*{symbol}* #{cid}#') 945 | return msg['topic'], cid 946 | 947 | # # ------------------------------------------------------------------------ 948 | # # ----------------------帐户请求函数-------------------------------------- 949 | # def req_accounts(self, cid:str=''): 950 | # msg = {'op': 'req', 'cid': cid, 'topic': 'accounts.list'} 951 | # self.send_message(msg) 952 | # logger.info(f'<请求>accounts-发送请求 #{cid}#') 953 | # return msg['topic'], cid 954 | # 955 | # def req_orders(self, acc_id, symbol, states:list, 956 | # types:list=None, 957 | # start_date=None, end_date=None, 958 | # _from=None, direct=None, 959 | # size=None, cid:str=''): 960 | # states = ','.join(states) 961 | # msg = {'op': 'req', 'account-id': acc_id, 'symbol': symbol, 'states': states, 'cid': cid, 962 | # 'topic': 'orders.list'} 963 | # if types: 964 | # types = ','.join(types) 965 | # msg['types'] = types 966 | # 967 | # if start_date: 968 | # start_date = parser.parse(start_date).strftime('%Y-%m-%d') 969 | # msg['start-date'] = start_date 970 | # 971 | # if end_date: 972 | # end_date = parser.parse(end_date).strftime('%Y-%m-%d') 973 | # msg['end-date'] = end_date 974 | # 975 | # if _from: 976 | # msg['_from'] = _from 977 | # 978 | # if direct: 979 | # msg['direct'] = direct 980 | # 981 | # if size: 982 | # msg['size'] = size 983 | # 984 | # self.send_message(msg) 985 | # logger.info(f'<请求>orders-发送请求 #{cid}#') 986 | # return msg['topic'], cid 987 | # 988 | # def req_orders_detail(self, order_id, cid:str=''): 989 | # msg = {'op': 'req', 'order-id': order_id, 'cid': cid, 'topic': 'orders.detail'} 990 | # self.send_message(msg) 991 | # logger.info(f'<请求>accounts-发送请求 #{cid}#') 992 | # return msg['topic'], cid 993 | 994 | def after_auth(self,_func): # ws开启之后需要完成的初始化处理 995 | @wraps(_func) 996 | def _callback(): 997 | try: 998 | _func() 999 | except Exception as e: 1000 | logger.exception(f'afer_open回调处理错误{e}') 1001 | self._auth_callbacks.append(_callback) 1002 | 1003 | return _callback -------------------------------------------------------------------------------- /huobitrade/service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2018/5/24 0024 14:16 4 | # @Author : Hadrianl 5 | # @File : service.py 6 | # @Contact : 137150224@qq.com 7 | 8 | 9 | from . import utils as u 10 | from .utils import logger, api_key_get, api_key_post, http_get_request, setUrl, setKey, Singleton 11 | from .utils import DEFAULT_URL, DEFAULT_DM_URL 12 | from dateutil import parser 13 | # from functools import wraps 14 | import datetime as dt 15 | from .core import _AuthWS, _HBWS, _DerivativesAuthWS, _HBDerivativesWS 16 | import warnings 17 | 18 | logger.debug(f'LOG_TESTING') 19 | 20 | 21 | def HBWebsocket(host='api.huobi.br.com', auth=False, isDerivatives=False, reconn=10, interval=3): 22 | if not isDerivatives: 23 | if auth: 24 | return _AuthWS(host, reconn, interval) 25 | else: 26 | return _HBWS(host, reconn, interval) 27 | else: 28 | if auth: 29 | return _DerivativesAuthWS(host, reconn, interval) 30 | else: 31 | return _HBDerivativesWS(host, reconn, interval) 32 | 33 | 34 | class HBRestAPI(metaclass=Singleton): 35 | def __init__(self, url=None, keys=None, get_acc=False): 36 | """ 37 | 火币REST API封装 38 | :param url: 传入url,若为None,默认是https://api.huobi.br.com 39 | :param keys: 传入(acess_key, secret_key),可用setKey设置 40 | """ 41 | self.url = url if url else DEFAULT_URL 42 | 43 | if keys: 44 | setKey(*keys) 45 | if get_acc: 46 | try: 47 | accounts = self.get_accounts()['data'] 48 | self.acc_id = self.get_accounts()['data'][0]['id'] 49 | if len(accounts) > 1: 50 | warnings.warn(f'默认设置acc_id为{self.acc_id}') 51 | except Exception as e: 52 | raise Exception(f'Failed to get account: key may not be set ->{e}') 53 | 54 | def set_acc_id(self, acc_id): 55 | self.acc_id = acc_id 56 | 57 | def __async_request_exception_handler(self, req, e): 58 | logger.error(f'async_request:{req}--exception:{e}') 59 | 60 | def async_request(self, reqs:list)->list: 61 | """ 62 | 异步并发请求 63 | :param reqs: 请求列表 64 | :return: 65 | """ 66 | result = (response.result() for response in reqs) 67 | ret = [r.json() if r.status_code == 200 else None for r in result] 68 | return ret 69 | 70 | def get_kline(self, symbol, period, size=150, _async=False): 71 | """ 72 | 获取KLine 73 | :param symbol 74 | :param period: 可选值:{1min, 5min, 15min, 30min, 60min, 1day, 1mon, 1week, 1year } 75 | :param size: 可选值: [1,2000] 76 | :return: 77 | """ 78 | params = {'symbol': symbol, 'period': period, 'size': size} 79 | 80 | url = self.url + '/market/history/kline' 81 | return http_get_request(url, params, _async=_async) 82 | 83 | def get_last_depth(self, symbol, _type, _async=False): 84 | """ 85 | 获取marketdepth 86 | :param symbol 87 | :param type: 可选值:{ percent10, step0, step1, step2, step3, step4, step5 } 88 | :return: 89 | """ 90 | params = {'symbol': symbol, 'type': _type} 91 | 92 | url = self.url + '/market/depth' 93 | return http_get_request(url, params, _async=_async) 94 | 95 | def get_last_ticker(self, symbol, _async=False): 96 | """ 97 | 获取tradedetail 98 | :param symbol 99 | :return: 100 | """ 101 | params = {'symbol': symbol} 102 | 103 | url = self.url + '/market/trade' 104 | return http_get_request(url, params, _async=_async) 105 | 106 | def get_tickers(self, symbol, size=1, _async=False): 107 | """ 108 | 获取历史ticker 109 | :param symbol: 110 | :param size: 可选[1,2000] 111 | :return: 112 | """ 113 | params = {'symbol': symbol, 'size': size} 114 | 115 | url = self.url + '/market/history/trade' 116 | return http_get_request(url, params, _async=_async) 117 | 118 | def get_all_last_24h_kline(self, _async=False): 119 | """ 120 | 获取所有24小时的概况 121 | :param _async: 122 | :return: 123 | """ 124 | params = {} 125 | url = self.url + '/market/tickers' 126 | return http_get_request(url, params, _async=_async) 127 | 128 | def get_last_1m_kline(self, symbol, _async=False): 129 | """ 130 | 获取最新一分钟的k线 131 | :param symbol: 132 | :return: 133 | """ 134 | params = {'symbol': symbol} 135 | 136 | url = self.url + '/market/detail/merged' 137 | return http_get_request(url, params, _async=_async) 138 | 139 | def get_last_24h_kline(self, symbol, _async=False): 140 | """ 141 | 获取最近24小时的概况 142 | :param symbol 143 | :return: 144 | """ 145 | params = {'symbol': symbol} 146 | 147 | url = self.url + '/market/detail' 148 | return http_get_request(url, params, _async=_async) 149 | 150 | def get_symbols(self, _async=False): 151 | """ 152 | 获取 支持的交易对 153 | :return: 154 | """ 155 | params = {} 156 | path = f'/v1/common/symbols' 157 | return api_key_get(params, path, _async=_async, url=self.url) 158 | 159 | def get_currencys(self, _async=False): 160 | """ 161 | 获取所有币种 162 | :return: 163 | """ 164 | params = {} 165 | path = f'/v1/common/currencys' 166 | return api_key_get(params, path, _async=_async, url=self.url) 167 | 168 | def get_timestamp(self, _async=False): 169 | params = {} 170 | path = '/v1/common/timestamp' 171 | return api_key_get(params, path, _async=_async, url=self.url) 172 | 173 | ''' 174 | Trade/Account API 175 | ''' 176 | 177 | def get_accounts(self, _async=False): 178 | """ 179 | :return: 180 | """ 181 | path = '/v1/account/accounts' 182 | params = {} 183 | return api_key_get(params, path, _async=_async, url=self.url) 184 | 185 | def get_balance(self, acc_id=None, _async=False): 186 | """ 187 | 获取当前账户资产 188 | :return: 189 | """ 190 | acc_id = self.acc_id if acc_id is None else acc_id 191 | path = f'/v1/account/accounts/{acc_id}/balance' 192 | # params = {'account-id': self.acct_id} 193 | params = {} 194 | return api_key_get(params, path, _async=_async, url=self.url) 195 | 196 | def send_order(self, acc_id, amount, symbol, _type, price=0, _async=False): 197 | """ 198 | 创建并执行订单 199 | :param amount: 200 | :param source: 如果使用借贷资产交易,请在下单接口,请求参数source中填写'margin-api' 201 | :param symbol: 202 | :param _type: 可选值 {buy-market:市价买, sell-market:市价卖, buy-limit:限价买, sell-limit:限价卖, buy-ioc:IOC买单, sell-ioc:IOC卖单} 203 | :param price: 204 | :return: 205 | """ 206 | 207 | assert _type in u.ORDER_TYPE 208 | params = { 209 | 'account-id': acc_id, 210 | 'amount': amount, 211 | 'symbol': symbol, 212 | 'type': _type, 213 | 'source': 'api' 214 | } 215 | if price: 216 | params['price'] = price 217 | 218 | path = f'/v1/order/orders/place' 219 | return api_key_post(params, path, _async=_async, url=self.url) 220 | 221 | def cancel_order(self, order_id, _async=False): 222 | """ 223 | 撤销订单 224 | :param order_id: 225 | :return: 226 | """ 227 | params = {} 228 | path = f'/v1/order/orders/{order_id}/submitcancel' 229 | return api_key_post(params, path, _async=_async, url=self.url) 230 | 231 | def batchcancel_orders(self, order_ids: list, _async=False): 232 | """ 233 | 批量撤销订单 234 | :param order_id: 235 | :return: 236 | """ 237 | assert isinstance(order_ids, list) 238 | params = {'order-ids': order_ids} 239 | path = f'/v1/order/orders/batchcancel' 240 | return api_key_post(params, path, _async=_async, url=self.url) 241 | 242 | def batchcancel_openOrders(self, acc_id, symbol=None, side=None, size=None, _async=False): 243 | """ 244 | 批量撤销未成交订单 245 | :param acc_id: 帐号ID 246 | :param symbol: 交易对 247 | :param side: 方向 248 | :param size: 249 | :param _async: 250 | :return: 251 | """ 252 | 253 | params = {} 254 | path = '/v1/order/batchCancelOpenOrders' 255 | params['account-id'] = acc_id 256 | if symbol: 257 | params['symbol'] = symbol 258 | if side: 259 | assert side in ['buy', 'sell'] 260 | params['side'] = side 261 | if size: 262 | params['size'] = size 263 | 264 | return api_key_post(params, path, _async=_async, url=self.url) 265 | 266 | 267 | def get_order_info(self, order_id, _async=False): 268 | """ 269 | 查询某个订单 270 | :param order_id: 271 | :return: 272 | """ 273 | params = {} 274 | path = f'/v1/order/orders/{order_id}' 275 | return api_key_get(params, path, _async=_async, url=self.url) 276 | 277 | def get_openOrders(self, acc_id=None, symbol=None, side=None, size=None, _async=False): 278 | """ 279 | 查询未成交订单 280 | :param acc_id: 帐号ID 281 | :param symbol: 交易对ID 282 | :param side: 交易方向,'buy'或者'sell' 283 | :param size: 记录条数,最大500 284 | :return: 285 | """ 286 | params = {} 287 | path = '/v1/order/openOrders' 288 | if all([acc_id, symbol]): 289 | params['account-id'] = acc_id 290 | params['symbol'] = symbol 291 | if side: 292 | assert side in ['buy', 'sell'] 293 | params['side'] = side 294 | if size: 295 | params['size'] = size 296 | 297 | return api_key_get(params, path, _async=_async, url=self.url) 298 | 299 | def get_order_matchresults(self, order_id, _async=False): 300 | """ 301 | 查询某个订单的成交明细 302 | :param order_id: 303 | :return: 304 | """ 305 | params = {} 306 | path = f'/v1/order/orders/{order_id}/matchresults' 307 | return api_key_get(params, path, _async=_async, url=self.url) 308 | 309 | def get_orders_info(self, 310 | symbol, 311 | states:list, 312 | types:list=None, 313 | start_date=None, 314 | end_date=None, 315 | _from=None, 316 | direct=None, 317 | size=None, 318 | _async=False): 319 | """ 320 | 查询当前委托、历史委托 321 | :param symbol: 322 | :param states: 可选值 {pre-submitted 准备提交, submitted 已提交, partial-filled 部分成交, partial-canceled 部分成交撤销, filled 完全成交, canceled 已撤销} 323 | :param types: 可选值 {buy-market:市价买, sell-market:市价卖, buy-limit:限价买, sell-limit:限价卖} 324 | :param start_date: 325 | :param end_date: 326 | :param _from: 327 | :param direct: 可选值{prev 向前,next 向后} 328 | :param size: 329 | :return: 330 | """ 331 | states = ','.join(states) 332 | params = {'symbol': symbol, 'states': states} 333 | 334 | if types: 335 | params['types'] = ','.join(types) 336 | if start_date: 337 | sd = parser.parse(start_date).date() 338 | params['start-date'] = str(sd) 339 | if end_date: 340 | ed = parser.parse(end_date).date() 341 | params['end-date'] = str(ed) 342 | if _from: 343 | params['from'] = _from 344 | if direct: 345 | assert direct in ['prev', 'next'] 346 | params['direct'] = direct 347 | if size: 348 | params['size'] = size 349 | path = '/v1/order/orders' 350 | return api_key_get(params, path, _async=_async, url=self.url) 351 | 352 | def get_recent48hours_order_info(self, 353 | symbol=None, 354 | start_time=None, 355 | end_time=None, 356 | direct=None, 357 | size=None, 358 | _async=False): 359 | """ 360 | 361 | :param symbol: 362 | :param start_time: datetime或者UTC time in millisecond 363 | :param end_time: datetime或者UTC time in millisecond 364 | :param direct: 可选值{prev 向前,next 向后} 365 | :param size: [10, 1000] default: 100 366 | :param _async: 367 | :return: 368 | """ 369 | 370 | params = {} 371 | 372 | if symbol: 373 | params['symbol'] = symbol 374 | 375 | if start_time: 376 | if isinstance(start_time, dt.datetime): 377 | start_time = int(start_time.timestamp() * 1000) 378 | params['start-time'] = start_time 379 | 380 | if end_time: 381 | if isinstance(end_time, dt.datetime): 382 | end_time = int(end_time.timestamp() * 1000) 383 | params['end-time'] = end_time 384 | 385 | if direct: 386 | assert direct in ['prev', 'next'] 387 | params['direct'] = direct 388 | 389 | if size: 390 | params['size'] = size 391 | 392 | path = '/v1/order/history' 393 | return api_key_get(params, path, _async=_async, url=self.url) 394 | 395 | 396 | def get_orders_matchresults(self, 397 | symbol, 398 | types:list=None, 399 | start_date=None, 400 | end_date=None, 401 | _from=None, 402 | direct=None, 403 | size=None, 404 | _async=False): 405 | """ 406 | 查询当前成交、历史成交 407 | :param symbol: 408 | :param types: 可选值 {buy-market:市价买, sell-market:市价卖, buy-limit:限价买, sell-limit:限价卖} 409 | :param start_date: 410 | :param end_date: 411 | :param _from: 412 | :param direct: 可选值{prev 向前,next 向后} 413 | :param size: 414 | :return: 415 | """ 416 | params = {'symbol': symbol} 417 | 418 | if types: 419 | params['types'] = ','.join(types) 420 | if start_date: 421 | sd = parser.parse(start_date).date() 422 | params['start-date'] = str(sd) 423 | if end_date: 424 | ed = parser.parse(end_date).date() 425 | params['end-date'] = str(ed) 426 | if _from: 427 | params['from'] = _from 428 | if direct: 429 | params['direct'] = direct 430 | if size: 431 | params['size'] = size 432 | path = '/v1/order/matchresults' 433 | return api_key_get(params, path, _async=_async, url=self.url) 434 | 435 | def req_withdraw(self, address, amount, currency, fee=0, addr_tag="", _async=False): 436 | """ 437 | 申请提现虚拟币 438 | :param address: 439 | :param amount: 440 | :param currency:btc, ltc, bcc, eth, etc ...(火币Pro支持的币种) 441 | :param fee: 442 | :param addr_tag: 443 | :return: { 444 | "status": "ok", 445 | "data": 700 446 | } 447 | """ 448 | params = { 449 | 'address': address, 450 | 'amount': amount, 451 | 'currency': currency, 452 | 'fee': fee, 453 | 'addr-tag': addr_tag 454 | } 455 | path = '/v1/dw/withdraw/api/create' 456 | 457 | return api_key_post(params, path, _async=_async, url=self.url) 458 | 459 | def cancel_withdraw(self, withdraw_id, _async=False): 460 | """ 461 | 申请取消提现虚拟币 462 | :param withdraw_id: 463 | :return: { 464 | "status": "ok", 465 | "data": 700 466 | } 467 | """ 468 | params = {} 469 | path = f'/v1/dw/withdraw-virtual/{withdraw_id}/cancel' 470 | 471 | return api_key_post(params, path, _async=_async, url=self.url) 472 | 473 | def get_deposit_withdraw_record(self, currency, _type, _from, size, _async=False): 474 | """ 475 | 476 | :param currency: 477 | :param _type: 478 | :param _from: 479 | :param size: 480 | :return: 481 | """ 482 | assert _type in ['deposit', 'withdraw'] 483 | params = { 484 | 'currency': currency, 485 | 'type': _type, 486 | 'from': _from, 487 | 'size': size 488 | } 489 | path = '/v1/query/deposit-withdraw' 490 | return api_key_get(params, path, _async=_async, url=self.url) 491 | 492 | ''' 493 | 借贷API 494 | ''' 495 | 496 | def send_margin_order(self, acc_id, amount, symbol, _type, price=0, _async=False): 497 | """ 498 | 创建并执行借贷订单 499 | :param amount: 500 | :param symbol: 501 | :param _type: 可选值 {buy-market:市价买, sell-market:市价卖, buy-limit:限价买, sell-limit:限价卖} 502 | :param price: 503 | :return: 504 | """ 505 | 506 | params = { 507 | 'account-id': acc_id, 508 | 'amount': amount, 509 | 'symbol': symbol, 510 | 'type': _type, 511 | 'source': 'margin-api' 512 | } 513 | if price: 514 | params['price'] = price 515 | 516 | path = '/v1/order/orders/place' 517 | return api_key_post(params, path, _async=_async, url=self.url) 518 | 519 | def exchange_to_margin(self, symbol, currency, amount, _async=False): 520 | """ 521 | 现货账户划入至借贷账户 522 | :param amount: 523 | :param currency: 524 | :param symbol: 525 | :return: 526 | """ 527 | params = {'symbol': symbol, 'currency': currency, 'amount': amount} 528 | 529 | path = '/v1/dw/transfer-in/margin' 530 | return api_key_post(params, path, _async=_async, url=self.url) 531 | 532 | def margin_to_exchange(self, symbol, currency, amount, _async=False): 533 | """ 534 | 借贷账户划出至现货账户 535 | :param amount: 536 | :param currency: 537 | :param symbol: 538 | :return: 539 | """ 540 | params = {'symbol': symbol, 'currency': currency, 'amount': amount} 541 | 542 | path = '/v1/dw/transfer-out/margin' 543 | return api_key_post(params, path, _async=_async, url=self.url) 544 | 545 | def apply_loan(self, symbol, currency, amount, _async=False): 546 | """ 547 | 申请借贷 548 | :param amount: 549 | :param currency: 550 | :param symbol: 551 | :return: 552 | """ 553 | params = {'symbol': symbol, 'currency': currency, 'amount': amount} 554 | path = '/v1/margin/orders' 555 | return api_key_post(params, path, _async=_async, url=self.url) 556 | 557 | def repay_loan(self, order_id, amount, _async=False): 558 | """ 559 | 归还借贷 560 | :param order_id: 561 | :param amount: 562 | :return: 563 | """ 564 | params = {'order-id': order_id, 'amount': amount} 565 | path = f'/v1/margin/orders/{order_id}/repay' 566 | return api_key_post(params, path, _async=_async, url=self.url) 567 | 568 | def get_loan_orders(self, 569 | symbol, 570 | states=None, 571 | start_date=None, 572 | end_date=None, 573 | _from=None, 574 | direct=None, 575 | size=None, 576 | _async=False): 577 | 578 | params = {'symbol': symbol} 579 | if states: 580 | params['states'] = states 581 | if start_date: 582 | sd = parser.parse(start_date).date() 583 | params['start-date'] = str(sd) 584 | if end_date: 585 | ed = parser.parse(end_date).date() 586 | params['end_date'] = str(ed) 587 | if _from: 588 | params['from'] = _from 589 | if direct and direct in ['prev', 'next']: 590 | params['direct'] = direct 591 | if size: 592 | params['size'] = size 593 | path = '/v1/margin/loan-orders' 594 | return api_key_get(params, path, _async=_async, url=self.url) 595 | 596 | def get_margin_balance(self, symbol=None, _async=False): 597 | """ 598 | 借贷账户详情,支持查询单个币种 599 | :param symbol: 600 | :return: 601 | """ 602 | params = {} 603 | path = '/v1/margin/accounts/balance' 604 | if symbol: 605 | params['symbol'] = symbol 606 | 607 | return api_key_get(params, path, _async=_async, url=self.url) 608 | 609 | def get_etf_config(self, etf_name, _async=False): 610 | """ 611 | 查询etf的基本信息 612 | :param etf_name: etf基金名称 613 | :param _async: 614 | :return: 615 | """ 616 | params = {} 617 | path = '/etf/swap/config' 618 | params['etf_name'] = etf_name 619 | 620 | return api_key_get(params, path, _async=_async, url=self.url) 621 | 622 | def etf_swap_in(self, etf_name, amount, _async=False): 623 | """ 624 | 换入etf 625 | :param etf_name: etf基金名称 626 | :param amount: 数量 627 | :param _async: 628 | :return: 629 | """ 630 | 631 | params = {} 632 | path = '/etf/swap/in' 633 | params['etf_name'] = etf_name 634 | params['amount'] = amount 635 | 636 | return api_key_post(params, path, _async=_async, url=self.url) 637 | 638 | def etf_swap_out(self, etf_name, amount, _async=False): 639 | """ 640 | 换出etf 641 | :param etf_name: etf基金名称 642 | :param amount: 数量 643 | :param _async: 644 | :return: 645 | """ 646 | 647 | params = {} 648 | path = '/etf/swap/out' 649 | params['etf_name'] = etf_name 650 | params['amount'] = amount 651 | 652 | return api_key_post(params, path, _async=_async, url=self.url) 653 | 654 | def get_etf_records(self, etf_name, offset, limit, _async=False): 655 | """ 656 | 查询etf换入换出明细 657 | :param etf_name: eth基金名称 658 | :param offset: 开始位置,0为最新一条 659 | :param limit: 返回记录条数(0, 100] 660 | :param _async: 661 | :return: 662 | """ 663 | params = {} 664 | path = '/etf/list' 665 | params['etf_name'] = etf_name 666 | params['offset'] = offset 667 | params['limit'] = limit 668 | 669 | return api_key_get(params, path, _async=_async, url=self.url) 670 | 671 | def get_quotation_kline(self, symbol, period, limit=None, _async=False): 672 | """ 673 | 获取etf净值 674 | :param symbol: etf名称 675 | :param period: K线类型 676 | :param limit: 获取数量 677 | :param _async: 678 | :return: 679 | """ 680 | params = {} 681 | path = '/quotation/market/history/kline' 682 | params['symbol'] = symbol 683 | params['period'] = period 684 | if limit: 685 | params['limit'] = limit 686 | 687 | return api_key_get(params, path, _async=_async, url=self.url) 688 | 689 | def transfer(self, sub_uid, currency, amount, transfer_type, _async=False): 690 | """ 691 | 母账户执行子账户划转 692 | :param sub_uid: 子账户id 693 | :param currency: 币种 694 | :param amount: 划转金额 695 | :param transfer_type: 划转类型,master-transfer-in(子账户划转给母账户虚拟币)/ master-transfer-out (母账户划转给子账户虚拟币)/master-point-transfer-in (子账户划转给母账户点卡)/master-point-transfer-out(母账户划转给子账户点卡) 696 | :param _async: 是否异步执行 697 | :return: 698 | """ 699 | params = {} 700 | path = '/v1/subuser/transfer' 701 | params['sub-uid'] = sub_uid 702 | params['currency'] = currency 703 | params['amount'] = amount 704 | params['type'] = transfer_type 705 | 706 | return api_key_post(params, path, _async=_async, url=self.url) 707 | 708 | def get_aggregate_balance(self, _async=False): 709 | """ 710 | 查询所有子账户汇总 711 | :param _async: 是否异步执行 712 | :return: 713 | """ 714 | params = {} 715 | path = '/v1/subuser/aggregate-balance' 716 | return api_key_get(params, path, _async=_async, url=self.url) 717 | 718 | def get_sub_balance(self, sub_uid, _async=False): 719 | """ 720 | 查询子账户各币种账户余额 721 | :param sub_uid: 子账户id 722 | :param _async: 723 | :return: 724 | """ 725 | 726 | params = {} 727 | params['sub-uid'] = sub_uid 728 | path = f'/v1/account/accounts/{sub_uid}' 729 | return api_key_get(params, path, _async=_async, url=self.url) 730 | 731 | 732 | class HBDerivativesRestAPI(metaclass=Singleton): 733 | def __init__(self, url=None, keys=None): 734 | """ 735 | 火币合约REST API封装 736 | :param url: 传入url,若为None,默认是https://api.hbdm.com 737 | :param keys: 传入(acess_key, secret_key),可用setKey设置 738 | """ 739 | self.url = url if url else DEFAULT_DM_URL 740 | 741 | if keys: 742 | setKey(*keys) 743 | 744 | 745 | def __async_request_exception_handler(self, req, e): 746 | logger.error(f'async_request:{req}--exception:{e}') 747 | 748 | def async_request(self, reqs:list)->list: 749 | """ 750 | 异步并发请求 751 | :param reqs: 请求列表 752 | :return: 753 | """ 754 | result = (response.result() for response in reqs) 755 | ret = [r.json() if r.status_code == 200 else None for r in result] 756 | return ret 757 | 758 | def get_contract_info(self, symbol=None, contract_type=None, contract_code=None, _async=False): 759 | """ 760 | 合约信息获取 761 | :param symbol: 762 | :param contract_type: 763 | :param contract_code: 764 | :param _async: 765 | :return: 766 | """ 767 | params = {} 768 | if symbol: 769 | params['symbol'] = symbol 770 | if contract_type: 771 | params['contract_type'] = contract_type 772 | if contract_code: 773 | params['contract_code'] = contract_code 774 | 775 | path = '/api/v1/contract_contract_info' 776 | url = self.url + path 777 | return http_get_request(url, params, _async=_async) 778 | 779 | 780 | def get_contract_index(self, symbol, _async=False): 781 | """ 782 | 获取合约指数 783 | :param symbol: 784 | :param _async: 785 | :return: 786 | """ 787 | params = {'symbol': symbol} 788 | path = '/api/v1/contract_index' 789 | url = self.url + path 790 | return http_get_request(url, params, _async=_async) 791 | 792 | def get_price_limit(self, symbol=None, contract_type=None, contract_code=None, _async=False): 793 | """ 794 | 合约高低限价 795 | :param symbol: 796 | :param contract_type: 797 | :param contract_code: 798 | :param _async: 799 | :return: 800 | """ 801 | params = {} 802 | if symbol: 803 | params['symbol'] = symbol 804 | if contract_type: 805 | params['contract_type'] = contract_type 806 | if contract_code: 807 | params['contract_code'] = contract_code 808 | 809 | path = '/api/v1/contract_price_limit' 810 | url = self.url + path 811 | return http_get_request(url, params, _async=_async) 812 | 813 | 814 | def get_delivery_price(self, symbol, _async=False): 815 | """ 816 | 817 | :param symbol: 818 | :param _async: 819 | :return: 820 | """ 821 | params = {'symbol': symbol} 822 | path = '/api/v1/contract_delivery_price' 823 | url = self.url + path 824 | return http_get_request(url, params, _async=_async) 825 | 826 | def get_open_interest(self, symbol=None, contract_type=None, contract_code=None, _async=False): 827 | """ 828 | 829 | :param symbol: 830 | :param contract_type: 831 | :param contract_code: 832 | :param _async: 833 | :return: 834 | """ 835 | params = {} 836 | if symbol: 837 | params['symbol'] = symbol 838 | if contract_type: 839 | params['contract_type'] = contract_type 840 | if contract_code: 841 | params['contract_code'] = contract_code 842 | path = '/api/v1/contract_open_interest' 843 | url = self.url + path 844 | return http_get_request(url, params, _async=_async) 845 | 846 | def get_last_depth(self, symbol, _type, _async=False): 847 | """ 848 | 849 | :param symbol: 850 | :param _type: 851 | :param _async: 852 | :return: 853 | """ 854 | params = {'symbol': symbol, 'type': _type} 855 | path = '/market/depth' 856 | url = self.url + path 857 | return http_get_request(url, params, _async=_async) 858 | 859 | def get_kline(self, symbol, period, size=150, _async=False): 860 | """ 861 | 862 | :param symbol: 863 | :param period: {1min, 5min, 15min, 30min, 60min,4hour,1day, 1mon} 864 | :param size: [1, 2000] 865 | :param _async: 866 | :return: 867 | """ 868 | params = {'symbol': symbol, 'period': period, 'size': size} 869 | path = '/market/history/kline' 870 | url = self.url + path 871 | return http_get_request(url, params, _async=_async) 872 | 873 | def get_last_1m_kline(self, symbol, _async=False): 874 | """ 875 | 876 | :param symbol: 877 | :param _async: 878 | :return: 879 | """ 880 | params = {'symbol': symbol} 881 | path = '/market/detail/merged' 882 | url = self.url + path 883 | return http_get_request(url, params, _async=_async) 884 | 885 | def get_last_ticker(self, symbol, _async=False): 886 | """ 887 | 888 | :param symbol: 889 | :param _async: 890 | :return: 891 | """ 892 | params = {'symbol': symbol} 893 | path = '/market/trade' 894 | url = self.url + path 895 | return http_get_request(url, params, _async=_async) 896 | 897 | def get_tickers(self, symbol, size=1, _async=False): 898 | """ 899 | 900 | :param symbol: 901 | :param size: [1, 2000] 902 | :param _async: 903 | :return: 904 | """ 905 | params = {'symbol': symbol, 'size': size} 906 | path = '/market/history/trade' 907 | url = self.url + path 908 | return http_get_request(url, params, _async=_async) 909 | 910 | # -----------------需要鉴权------------------ 911 | def get_accounts(self, symbol=None, _async=False): 912 | """ 913 | 914 | :param symbol: 915 | :param _async: 916 | :return: 917 | """ 918 | params = {} 919 | if symbol: 920 | params['symbol'] = symbol 921 | 922 | path = '/api/v1/contract_account_info' 923 | 924 | return api_key_post(params, path, _async=_async, url=self.url) 925 | 926 | def get_positions(self, symbol=None, _async=False): 927 | """ 928 | 929 | :param symbol: 930 | :param _async: 931 | :return: 932 | """ 933 | params = {} 934 | if symbol: 935 | params['symbol'] = symbol 936 | 937 | path = '/api/v1/contract_position_info' 938 | 939 | return api_key_post(params, path, _async=_async, url=self.url) 940 | 941 | @staticmethod 942 | def create_order_params(*, 943 | volume, 944 | direction, 945 | offset, 946 | lever_rate, 947 | order_price_type, 948 | symbol=None, 949 | contract_type=None, 950 | contract_code=None, 951 | price=None, 952 | client_order_id=None): 953 | """ 954 | 955 | :param volume: 956 | :param direction: ['buy', 'sell'] 957 | :param offset: ['open', 'close'] 958 | :param lever_rate: 959 | :param order_price_type: ['limit', 'opponent', 'post_only', 960 | :param symbol: 961 | :param contract_type: ['this_week', 'next_week', 'quarter'] 962 | :param contract_code: 963 | :param price: 964 | :param client_order_id: 965 | :return: 966 | """ 967 | params = {'volume': volume, 'direction': direction, 'offset': offset, 968 | 'lever_rate': lever_rate, 'order_price_type': order_price_type} 969 | 970 | if order_price_type != 'opponent' and price is None: 971 | raise ValueError('非opponent订单,需要填写price') 972 | 973 | params['price'] = price 974 | 975 | if contract_code is not None: 976 | params['contract_code'] = contract_code 977 | else: 978 | params['symbol'] = symbol 979 | params['contract_type'] = contract_type 980 | 981 | if client_order_id is not None: 982 | params['client_order_id'] = client_order_id 983 | 984 | return params 985 | 986 | 987 | def send_order(self, order_params, _async=False): 988 | """ 989 | 990 | :param order_params: 通过create_order_params创建的参数 991 | :param _async: 992 | :return: 993 | """ 994 | 995 | path = '/api/v1/contract_order' 996 | 997 | return api_key_post(order_params, path, _async=_async, url=self.url) 998 | 999 | def batchcancel_orders(self, order_params_list:list, _async=False): 1000 | """ 1001 | 1002 | :param order_params_list: 通过create_order_params创建的参数列表 1003 | :param _async: 1004 | :return: 1005 | """ 1006 | path = '/api/v1/contract_batchorder' 1007 | 1008 | return api_key_post(order_params_list, path, _async=_async, url=self.url) 1009 | 1010 | def cancel_order(self, symbol, order_ids:list=None, client_order_ids: list=None, _async=False): 1011 | """ 1012 | 1013 | :param symbol: 1014 | :param order_ids: 1015 | :param client_order_ids: 1016 | :param _async: 1017 | :return: 1018 | """ 1019 | params = {'symbol': symbol} 1020 | 1021 | if order_ids: 1022 | params['order_id'] = ','.join(order_ids) 1023 | 1024 | if client_order_ids: 1025 | params['client_order_id'] = ','.join(client_order_ids) 1026 | 1027 | path = '/api/v1/contract_cancel' 1028 | 1029 | return api_key_post(params, path, _async=_async, url=self.url) 1030 | 1031 | def cancel_all_orders(self, symbol, contract_code=None, contract_type=None, _async=False): 1032 | """ 1033 | 1034 | :param symbol: 1035 | :param contract_code: 1036 | :param contract_type: 1037 | :param _async: 1038 | :return: 1039 | """ 1040 | params = {'symbol': symbol} 1041 | 1042 | if contract_code: 1043 | params['contract_code'] = contract_code 1044 | 1045 | if contract_type: 1046 | params['contract_type'] = contract_type 1047 | 1048 | path = '/api/v1/contract_cancelall' 1049 | 1050 | return api_key_post(params, path, _async=_async, url=self.url) 1051 | 1052 | def get_order_info(self,symbol, order_ids:list=None, client_order_ids: list=None, _async=False): 1053 | """ 1054 | 1055 | :param symbol: 1056 | :param order_ids: 1057 | :param client_order_ids: 1058 | :param _async: 1059 | :return: 1060 | """ 1061 | params = {'symbol': symbol} 1062 | 1063 | if order_ids: 1064 | params['order_id'] = ','.join(order_ids) 1065 | 1066 | if client_order_ids: 1067 | params['client_order_id'] = ','.join(client_order_ids) 1068 | 1069 | path = '/api/v1/contract_order_info' 1070 | 1071 | return api_key_post(params, path, _async=_async, url=self.url) 1072 | 1073 | def get_order_detail(self, symbol, order_id, create_at, order_type, 1074 | page_index=None, page_size=20, _async=False): 1075 | """ 1076 | 1077 | :param symbol: 1078 | :param order_id: 1079 | :param create_at: 1080 | :param order_type: 1081 | :param page_index: 页码,不填默认第1页 1082 | :param page_size: 不填默认20,不得多于50 20 1083 | :param _async: 1084 | :return: 1085 | """ 1086 | params = {'symbol': symbol, 'order_id': order_id, 1087 | 'order_type': order_type, 'page_size': page_size} 1088 | 1089 | if page_index is not None: 1090 | params['page_index'] = page_index 1091 | 1092 | if isinstance(create_at, str): 1093 | create_at = int(parser.parse(create_at).timestamp() * 1000) 1094 | elif isinstance(create_at, dt.datetime): 1095 | create_at = int(create_at.timestamp() * 1000) 1096 | 1097 | params['create_at'] = create_at 1098 | 1099 | path = '/api/v1/contract_order_detail' 1100 | 1101 | return api_key_post(params, path, _async=_async, url=self.url) 1102 | 1103 | def get_open_orders(self, symbol, page_index=None, page_size=20, _async=False): 1104 | """ 1105 | 1106 | :param symbol: 1107 | :param page_index: 页码,不填默认第1页 1108 | :param page_size: 不填默认20,不得多于50 20 1109 | :param _async: 1110 | :return: 1111 | """ 1112 | params = {'symbol': symbol, 'page_size': page_size} 1113 | if page_index is not None: 1114 | params['page_index'] = page_index 1115 | 1116 | path = '/api/v1/contract_openorders' 1117 | 1118 | return api_key_post(params, path, _async=_async, url=self.url) 1119 | 1120 | def get_history_orders(self, symbol, trade_type, _type, status, create_date, 1121 | page_index=None, page_size=20, _async=False): 1122 | """ 1123 | 1124 | :param symbol: 1125 | :param trade_type: 0:全部,1:买入开多,2: 卖出开空,3: 买入平空,4: 卖出平多,5: 卖出强平,6: 买入强平,7:交割平多,8: 交割平空 1126 | :param _type: 1:所有订单,2:结束状态的订单 1127 | :param status: 0:全部,3:未成交, 4: 部分成交,5: 部分成交已撤单,6: 全部成交,7:已撤单 1128 | :param create_date: 7,90(7天或者90天) 1129 | :param page_index: 页码,不填默认第1页 1130 | :param page_size: 不填默认20,不得多于50 20 1131 | :param _async: 1132 | :return: 1133 | """ 1134 | params = {'symbol': symbol, 'trade_type': trade_type, 'type': _type, 1135 | 'status': status, 'create_date': create_date, 'page_size': page_size} 1136 | if page_index is not None: 1137 | params['page_index'] = page_index 1138 | 1139 | path = '/api/v1/contract_hisorders' 1140 | 1141 | return api_key_post(params, path, _async=_async, url=self.url) 1142 | 1143 | def get_order_matchresults(self, symbol, trade_type, create_date, 1144 | page_index=None, page_size=20, _async=False): 1145 | """ 1146 | 1147 | :param symbol: 1148 | :param trade_type: 0:全部,1:买入开多,2: 卖出开空,3: 买入平空,4: 卖出平多,5: 卖出强平,6: 买入强平 1149 | :param create_date: 7,90(7天或者90天) 1150 | :param page_index: 页码,不填默认第1页 1151 | :param page_size: 不填默认20,不得多于50 1152 | :param _async: 1153 | :return: 1154 | """ 1155 | params = {'symbol': symbol, 'trade_type': trade_type, 1156 | 'create_date': create_date, 'page_size': page_size} 1157 | if page_index is not None: 1158 | params['page_index'] = page_index 1159 | 1160 | path = '/api/v1/contract_matchresults' 1161 | 1162 | return api_key_post(params, path, _async=_async, url=self.url) 1163 | 1164 | def transfer_futures(self, currency, amount, _type, _async=False): 1165 | """ 1166 | 1167 | :param currency: 1168 | :param amount: 1169 | :param _type: 从合约账户到现货账户:“futures-to-pro”,从现货账户到合约账户: “pro-to-futures” 1170 | :param _async: 1171 | :return: 1172 | """ 1173 | params = {'currency': currency, 'amount': amount, 'type': _type} 1174 | path = '/v1/futures/transfer' 1175 | 1176 | return api_key_post(params, path, _async=_async, url='https://api.huobi.pro') 1177 | 1178 | 1179 | # class HBRestAPI_DEC(): 1180 | # def __init__(self, addr=None, key=None, get_acc=False): 1181 | # """ 1182 | # 火币REST API封装decoration版 1183 | # :param addrs: 传入(market_url, trade_url),若为None,默认是https://api.huobi.br.com 1184 | # :param keys: 传入(acess_key, secret_key),可用setKey设置 1185 | # :param get_acc: 设置是否初始化获取acc_id,,默认False 1186 | # """ 1187 | # if addr: 1188 | # setUrl(*addr) 1189 | # if key: 1190 | # setKey(*key) 1191 | # if get_acc: 1192 | # @self.get_accounts() 1193 | # def set_acc(msg): 1194 | # self.acc_id = msg['data'][0]['id'] 1195 | # set_acc() 1196 | # 1197 | # def set_acc_id(self, acc_id): 1198 | # self.acc_id = acc_id 1199 | # 1200 | # def get_kline(self, symbol, period, size=150): 1201 | # """ 1202 | # 获取K线 1203 | # :param symbol 1204 | # :param period: 可选值:{1min, 5min, 15min, 30min, 60min, 1day, 1mon, 1week, 1year } 1205 | # :param size: 可选值: [1,2000] 1206 | # :return: 1207 | # """ 1208 | # params = {'symbol': symbol, 'period': period, 'size': size} 1209 | # url = u.MARKET_URL + '/market/history/kline' 1210 | # 1211 | # def _wrapper(_func): 1212 | # @wraps(_func) 1213 | # def handle(): 1214 | # _func(http_get_request(url, params)) 1215 | # 1216 | # return handle 1217 | # 1218 | # return _wrapper 1219 | # 1220 | # def get_last_depth(self, symbol, _type): 1221 | # """ 1222 | # 获取marketdepth 1223 | # :param symbol 1224 | # :param type: 可选值:{ percent10, step0, step1, step2, step3, step4, step5 } 1225 | # :return: 1226 | # """ 1227 | # params = {'symbol': symbol, 'type': _type} 1228 | # 1229 | # url = u.MARKET_URL + '/market/depth' 1230 | # 1231 | # def _wrapper(_func): 1232 | # @wraps(_func) 1233 | # def handle(): 1234 | # _func(http_get_request(url, params)) 1235 | # 1236 | # return handle 1237 | # 1238 | # return _wrapper 1239 | # 1240 | # def get_last_ticker(self, symbol): 1241 | # """ 1242 | # 获取最新的ticker 1243 | # :param symbol 1244 | # :return: 1245 | # """ 1246 | # params = {'symbol': symbol} 1247 | # 1248 | # url = u.MARKET_URL + '/market/trade' 1249 | # 1250 | # def _wrapper(_func): 1251 | # @wraps(_func) 1252 | # def handle(): 1253 | # _func(http_get_request(url, params)) 1254 | # 1255 | # return handle 1256 | # 1257 | # return _wrapper 1258 | # 1259 | # def get_ticker(self, symbol, size=1): 1260 | # """ 1261 | # 获取历史ticker 1262 | # :param symbol: 1263 | # :param size: 可选[1,2000] 1264 | # :return: 1265 | # """ 1266 | # params = {'symbol': symbol, 'size': size} 1267 | # 1268 | # url = u.MARKET_URL + '/market/history/trade' 1269 | # 1270 | # def _wrapper(_func): 1271 | # @wraps(_func) 1272 | # def handle(): 1273 | # _func(http_get_request(url, params)) 1274 | # 1275 | # return handle 1276 | # 1277 | # return _wrapper 1278 | # 1279 | # def get_all_last_24h_kline(self): 1280 | # """ 1281 | # 获取所有ticker 1282 | # :param _async: 1283 | # :return: 1284 | # """ 1285 | # params = {} 1286 | # url = u.MARKET_URL + '/market/tickers' 1287 | # 1288 | # def _wrapper(_func): 1289 | # @wraps(_func) 1290 | # def handle(): 1291 | # _func(http_get_request(url, params)) 1292 | # 1293 | # return handle 1294 | # 1295 | # return _wrapper 1296 | # 1297 | # 1298 | # def get_last_1m_kline(self, symbol): 1299 | # """ 1300 | # 获取最新一分钟的kline 1301 | # :param symbol: 1302 | # :return: 1303 | # """ 1304 | # params = {'symbol': symbol} 1305 | # 1306 | # url = u.MARKET_URL + '/market/detail/merged' 1307 | # 1308 | # def _wrapper(_func): 1309 | # @wraps(_func) 1310 | # def handle(): 1311 | # _func(http_get_request(url, params)) 1312 | # 1313 | # return handle 1314 | # 1315 | # return _wrapper 1316 | # 1317 | # def get_last_24h_kline(self, symbol): 1318 | # """ 1319 | # 获取 Market Detail 24小时成交量数据 1320 | # :param symbol 1321 | # :return: 1322 | # """ 1323 | # params = {'symbol': symbol} 1324 | # 1325 | # url = u.MARKET_URL + '/market/detail' 1326 | # 1327 | # def _wrapper(_func): 1328 | # @wraps(_func) 1329 | # def handle(): 1330 | # _func(http_get_request(url, params)) 1331 | # 1332 | # return handle 1333 | # 1334 | # return _wrapper 1335 | # 1336 | # def get_symbols(self, site='Pro'): 1337 | # """ 1338 | # 获取支持的交易对 1339 | # :param site: 1340 | # :return: 1341 | # """ 1342 | # assert site in ['Pro', 'HADAX'] 1343 | # params = {} 1344 | # path = f'/v1{"/" if site == "Pro" else "/hadax/"}common/symbols' 1345 | # 1346 | # def _wrapper(_func): 1347 | # @wraps(_func) 1348 | # def handle(): 1349 | # _func(api_key_get(params, path)) 1350 | # 1351 | # return handle 1352 | # 1353 | # return _wrapper 1354 | # 1355 | # def get_currencys(self, site='Pro'): 1356 | # """ 1357 | # 1358 | # :return: 1359 | # """ 1360 | # assert site in ['Pro', 'HADAX'] 1361 | # params = {} 1362 | # path = f'/v1{"/" if site == "Pro" else "/hadax/"}common/currencys' 1363 | # 1364 | # def _wrapper(_func): 1365 | # @wraps(_func) 1366 | # def handle(): 1367 | # _func(api_key_get(params, path)) 1368 | # 1369 | # return handle 1370 | # 1371 | # return _wrapper 1372 | # 1373 | # def get_timestamp(self): 1374 | # params = {} 1375 | # path = '/v1/common/timestamp' 1376 | # 1377 | # def _wrapper(_func): 1378 | # @wraps(_func) 1379 | # def handle(): 1380 | # _func(api_key_get(params, path)) 1381 | # 1382 | # return handle 1383 | # 1384 | # return _wrapper 1385 | # 1386 | # ''' 1387 | # Trade/Account API 1388 | # ''' 1389 | # 1390 | # def get_accounts(self): 1391 | # """ 1392 | # :return: 1393 | # """ 1394 | # path = '/v1/account/accounts' 1395 | # params = {} 1396 | # 1397 | # def _wrapper(_func): 1398 | # @wraps(_func) 1399 | # def handle(): 1400 | # _func(api_key_get(params, path)) 1401 | # 1402 | # return handle 1403 | # 1404 | # return _wrapper 1405 | # 1406 | # def get_balance(self, acc_id, site='Pro'): 1407 | # """ 1408 | # 获取当前账户资产 1409 | # :return: 1410 | # """ 1411 | # assert site in ['Pro', 'HADAX'] 1412 | # path = f'/v1{"/" if site == "Pro" else "/hadax/"}account/accounts/{acc_id}/balance' 1413 | # # params = {'account-id': self.acct_id} 1414 | # params = {} 1415 | # 1416 | # def _wrapper(_func): 1417 | # @wraps(_func) 1418 | # def handle(): 1419 | # _func(api_key_get(params, path)) 1420 | # 1421 | # return handle 1422 | # 1423 | # return _wrapper 1424 | # 1425 | # def send_order(self, acc_id, amount, symbol, _type, price=0, site='Pro'): 1426 | # """ 1427 | # 创建并执行订单 1428 | # :param amount: 1429 | # :param source: 如果使用借贷资产交易,请在下单接口,请求参数source中填写'margin-api' 1430 | # :param symbol: 1431 | # :param _type: 可选值 {buy-market:市价买, sell-market:市价卖, buy-limit:限价买, sell-limit:限价卖, buy-ioc:IOC买单, sell-ioc:IOC卖单} 1432 | # :param price: 1433 | # :return: 1434 | # """ 1435 | # assert site in ['Pro', 'HADAX'] 1436 | # assert _type in u.ORDER_TYPE 1437 | # params = { 1438 | # 'account-id': acc_id, 1439 | # 'amount': amount, 1440 | # 'symbol': symbol, 1441 | # 'type': _type, 1442 | # 'source': 'api' 1443 | # } 1444 | # if price: 1445 | # params['price'] = price 1446 | # 1447 | # path = f'/v1{"/" if site == "Pro" else "/hadax/"}order/orders/place' 1448 | # 1449 | # def _wrapper(_func): 1450 | # @wraps(_func) 1451 | # def handle(): 1452 | # _func(api_key_post(params, path)) 1453 | # 1454 | # return handle 1455 | # 1456 | # return _wrapper 1457 | # 1458 | # def cancel_order(self, order_id): 1459 | # """ 1460 | # 撤销订单 1461 | # :param order_id: 1462 | # :return: 1463 | # """ 1464 | # params = {} 1465 | # path = f'/v1/order/orders/{order_id}/submitcancel' 1466 | # 1467 | # def _wrapper(_func): 1468 | # @wraps(_func) 1469 | # def handle(): 1470 | # _func(api_key_post(params, path)) 1471 | # 1472 | # return handle 1473 | # 1474 | # return _wrapper 1475 | # 1476 | # def batchcancel_order(self, order_ids: list): 1477 | # """ 1478 | # 批量撤销订单 1479 | # :param order_id: 1480 | # :return: 1481 | # """ 1482 | # assert isinstance(order_ids, list) 1483 | # params = {'order-ids': order_ids} 1484 | # path = f'/v1/order/orders/batchcancel' 1485 | # 1486 | # def _wrapper(_func): 1487 | # @wraps(_func) 1488 | # def handle(): 1489 | # _func(api_key_post(params, path)) 1490 | # 1491 | # return handle 1492 | # 1493 | # return _wrapper 1494 | # 1495 | # def get_order_info(self, order_id): 1496 | # """ 1497 | # 查询某个订单 1498 | # :param order_id: 1499 | # :return: 1500 | # """ 1501 | # params = {} 1502 | # path = f'/v1/order/orders/{order_id}' 1503 | # 1504 | # def _wrapper(_func): 1505 | # @wraps(_func) 1506 | # def handle(): 1507 | # _func(api_key_get(params, path)) 1508 | # 1509 | # return handle 1510 | # 1511 | # return _wrapper 1512 | # 1513 | # def get_order_matchresults(self, order_id): 1514 | # """ 1515 | # 查询某个订单的成交明细 1516 | # :param order_id: 1517 | # :return: 1518 | # """ 1519 | # params = {} 1520 | # path = f'/v1/order/orders/{order_id}/matchresults' 1521 | # 1522 | # def _wrapper(_func): 1523 | # @wraps(_func) 1524 | # def handle(): 1525 | # _func(api_key_get(params, path)) 1526 | # 1527 | # return handle 1528 | # 1529 | # return _wrapper 1530 | # 1531 | # def get_orders_info(self, 1532 | # symbol, 1533 | # states, 1534 | # types=None, 1535 | # start_date=None, 1536 | # end_date=None, 1537 | # _from=None, 1538 | # direct=None, 1539 | # size=None): 1540 | # """ 1541 | # 查询当前委托、历史委托 1542 | # :param symbol: 1543 | # :param states: 可选值 {pre-submitted 准备提交, submitted 已提交, partial-filled 部分成交, partial-canceled 部分成交撤销, filled 完全成交, canceled 已撤销} 1544 | # :param types: 可选值 买卖类型 1545 | # :param start_date: 1546 | # :param end_date: 1547 | # :param _from: 1548 | # :param direct: 可选值{prev 向前,next 向后} 1549 | # :param size: 1550 | # :return: 1551 | # """ 1552 | # params = {'symbol': symbol, 'states': states} 1553 | # 1554 | # if types: 1555 | # params['types'] = types 1556 | # if start_date: 1557 | # sd = parser.parse(start_date).date() 1558 | # params['start-date'] = str(sd) 1559 | # if end_date: 1560 | # ed = parser.parse(end_date).date() 1561 | # params['end_date'] = str(ed) 1562 | # if _from: 1563 | # params['from'] = _from 1564 | # if direct: 1565 | # assert direct in ['prev', 'next'] 1566 | # params['direct'] = direct 1567 | # if size: 1568 | # params['size'] = size 1569 | # path = '/v1/order/orders' 1570 | # 1571 | # def _wrapper(_func): 1572 | # @wraps(_func) 1573 | # def handle(): 1574 | # _func(api_key_get(params, path)) 1575 | # 1576 | # return handle 1577 | # 1578 | # return _wrapper 1579 | # 1580 | # def get_orders_matchresults(self, 1581 | # symbol, 1582 | # types=None, 1583 | # start_date=None, 1584 | # end_date=None, 1585 | # _from=None, 1586 | # direct=None, 1587 | # size=None): 1588 | # """ 1589 | # 查询当前成交、历史成交 1590 | # :param symbol: 1591 | # :param types: 可选值 {buy-market:市价买, sell-market:市价卖, buy-limit:限价买, sell-limit:限价卖} 1592 | # :param start_date: 1593 | # :param end_date: 1594 | # :param _from: 1595 | # :param direct: 可选值{prev 向前,next 向后} 1596 | # :param size: 1597 | # :return: 1598 | # """ 1599 | # params = {'symbol': symbol} 1600 | # 1601 | # if types: 1602 | # params['types'] = types 1603 | # if start_date: 1604 | # sd = parser.parse(start_date).date() 1605 | # params['start-date'] = str(sd) 1606 | # if end_date: 1607 | # ed = parser.parse(end_date).date() 1608 | # params['end_date'] = str(ed) 1609 | # if _from: 1610 | # params['from'] = _from 1611 | # if direct: 1612 | # params['direct'] = direct 1613 | # if size: 1614 | # params['size'] = size 1615 | # path = '/v1/order/matchresults' 1616 | # 1617 | # def _wrapper(_func): 1618 | # @wraps(_func) 1619 | # def handle(): 1620 | # _func(api_key_get(params, path)) 1621 | # 1622 | # return handle 1623 | # 1624 | # return _wrapper 1625 | # 1626 | # def req_withdraw(self, address, amount, currency, fee=0, addr_tag=""): 1627 | # """ 1628 | # 申请提现虚拟币 1629 | # :param address_id: 1630 | # :param amount: 1631 | # :param currency:btc, ltc, bcc, eth, etc ...(火币Pro支持的币种) 1632 | # :param fee: 1633 | # :param addr-tag: 1634 | # :return: { 1635 | # "status": "ok", 1636 | # "data": 700 1637 | # } 1638 | # """ 1639 | # params = { 1640 | # 'address': address, 1641 | # 'amount': amount, 1642 | # 'currency': currency, 1643 | # 'fee': fee, 1644 | # 'addr-tag': addr_tag 1645 | # } 1646 | # path = '/v1/dw/withdraw/api/create' 1647 | # 1648 | # def _wrapper(_func): 1649 | # @wraps(_func) 1650 | # def handle(): 1651 | # _func(api_key_post(params, path)) 1652 | # 1653 | # return handle 1654 | # 1655 | # return _wrapper 1656 | # 1657 | # def cancel_withdraw(self, address_id): 1658 | # """ 1659 | # 申请取消提现虚拟币 1660 | # :param address_id: 1661 | # :return: { 1662 | # "status": "ok", 1663 | # "data": 700 1664 | # } 1665 | # """ 1666 | # params = {} 1667 | # path = f'/v1/dw/withdraw-virtual/{address_id}/cancel' 1668 | # 1669 | # def _wrapper(_func): 1670 | # @wraps(_func) 1671 | # def handle(): 1672 | # _func(api_key_post(params, path)) 1673 | # 1674 | # return handle 1675 | # 1676 | # return _wrapper 1677 | # 1678 | # def get_deposit_withdraw_record(self, currency, _type, _from, size): 1679 | # """ 1680 | # 1681 | # :param currency: 1682 | # :param _type: 1683 | # :param _from: 1684 | # :param size: 1685 | # :return: 1686 | # """ 1687 | # assert _type in ['deposit', 'withdraw'] 1688 | # params = { 1689 | # 'currency': currency, 1690 | # 'type': _type, 1691 | # 'from': _from, 1692 | # 'size': size 1693 | # } 1694 | # path = '/v1/query/deposit-withdraw' 1695 | # 1696 | # def _wrapper(_func): 1697 | # @wraps(_func) 1698 | # def handle(): 1699 | # _func(api_key_get(params, path)) 1700 | # 1701 | # return handle 1702 | # 1703 | # return _wrapper 1704 | # 1705 | # ''' 1706 | # 借贷API 1707 | # ''' 1708 | # 1709 | # def send_margin_order(self, acc_id, amount, symbol, _type, price=0): 1710 | # """ 1711 | # 创建并执行借贷订单 1712 | # :param amount: 1713 | # :param symbol: 1714 | # :param _type: 可选值 {buy-market:市价买, sell-market:市价卖, buy-limit:限价买, sell-limit:限价卖} 1715 | # :param price: 1716 | # :return: 1717 | # """ 1718 | # 1719 | # params = { 1720 | # 'account-id': acc_id, 1721 | # 'amount': amount, 1722 | # 'symbol': symbol, 1723 | # 'type': _type, 1724 | # 'source': 'margin-api' 1725 | # } 1726 | # if price: 1727 | # params['price'] = price 1728 | # 1729 | # path = '/v1/order/orders/place' 1730 | # 1731 | # def _wrapper(_func): 1732 | # @wraps(_func) 1733 | # def handle(): 1734 | # _func(api_key_post(params, path)) 1735 | # 1736 | # return handle 1737 | # 1738 | # return _wrapper 1739 | # 1740 | # def exchange_to_margin(self, symbol, currency, amount): 1741 | # """ 1742 | # 现货账户划入至借贷账户 1743 | # :param amount: 1744 | # :param currency: 1745 | # :param symbol: 1746 | # :return: 1747 | # """ 1748 | # params = {'symbol': symbol, 'currency': currency, 'amount': amount} 1749 | # path = '/v1/dw/transfer-in/margin' 1750 | # 1751 | # def _wrapper(_func): 1752 | # @wraps(_func) 1753 | # def handle(): 1754 | # _func(api_key_post(params, path)) 1755 | # 1756 | # return handle 1757 | # 1758 | # return _wrapper 1759 | # 1760 | # def margin_to_exchange(self, symbol, currency, amount): 1761 | # """ 1762 | # 借贷账户划出至现货账户 1763 | # :param amount: 1764 | # :param currency: 1765 | # :param symbol: 1766 | # :return: 1767 | # """ 1768 | # params = {'symbol': symbol, 'currency': currency, 'amount': amount} 1769 | # 1770 | # path = '/v1/dw/transfer-out/margin' 1771 | # 1772 | # def _wrapper(_func): 1773 | # @wraps(_func) 1774 | # def handle(): 1775 | # _func(api_key_post(params, path)) 1776 | # 1777 | # return handle 1778 | # 1779 | # return _wrapper 1780 | # 1781 | # def apply_loan(self, symbol, currency, amount): 1782 | # """ 1783 | # 申请借贷 1784 | # :param amount: 1785 | # :param currency: 1786 | # :param symbol: 1787 | # :return: 1788 | # """ 1789 | # params = {'symbol': symbol, 'currency': currency, 'amount': amount} 1790 | # path = '/v1/margin/orders' 1791 | # 1792 | # def _wrapper(_func): 1793 | # @wraps(_func) 1794 | # def handle(): 1795 | # _func(api_key_post(params, path)) 1796 | # 1797 | # return handle 1798 | # 1799 | # return _wrapper 1800 | # 1801 | # def repay_loan(self, order_id, amount): 1802 | # """ 1803 | # 归还借贷 1804 | # :param order_id: 1805 | # :param amount: 1806 | # :return: 1807 | # """ 1808 | # params = {'order-id': order_id, 'amount': amount} 1809 | # path = f'/v1/margin/orders/{order_id}/repay' 1810 | # 1811 | # def _wrapper(_func): 1812 | # @wraps(_func) 1813 | # def handle(): 1814 | # _func(api_key_post(params, path)) 1815 | # 1816 | # return handle 1817 | # 1818 | # return _wrapper 1819 | # 1820 | # def get_loan_orders(self, 1821 | # symbol, 1822 | # currency, 1823 | # states=None, 1824 | # start_date=None, 1825 | # end_date=None, 1826 | # _from=None, 1827 | # direct=None, 1828 | # size=None): 1829 | # """ 1830 | # 借贷订单 1831 | # :param symbol: 1832 | # :param currency: 1833 | # :param start_date: 1834 | # :param end_date: 1835 | # :param _from: 1836 | # :param direct: 1837 | # :param size: 1838 | # :return: 1839 | # """ 1840 | # params = {'symbol': symbol, 'currency': currency} 1841 | # if states: 1842 | # params['states'] = states 1843 | # if start_date: 1844 | # sd = parser.parse(start_date).date() 1845 | # params['start-date'] = str(sd) 1846 | # if end_date: 1847 | # ed = parser.parse(end_date).date() 1848 | # params['end_date'] = str(ed) 1849 | # if _from: 1850 | # params['from'] = _from 1851 | # if direct and direct in ['prev', 'next']: 1852 | # params['direct'] = direct 1853 | # if size: 1854 | # params['size'] = size 1855 | # path = '/v1/margin/loan-orders' 1856 | # 1857 | # def _wrapper(_func): 1858 | # @wraps(_func) 1859 | # def handle(): 1860 | # _func(api_key_get(params, path)) 1861 | # 1862 | # return handle 1863 | # 1864 | # return _wrapper 1865 | # 1866 | # def get_margin_balance(self, symbol): 1867 | # """ 1868 | # 借贷账户详情,支持查询单个币种 1869 | # :param symbol: 1870 | # :return: 1871 | # """ 1872 | # params = {} 1873 | # path = '/v1/margin/accounts/balance' 1874 | # if symbol: 1875 | # params['symbol'] = symbol 1876 | # 1877 | # def _wrapper(_func): 1878 | # @wraps(_func) 1879 | # def handle(): 1880 | # _func(api_key_get(params, path)) 1881 | # 1882 | # return handle 1883 | # 1884 | # return _wrapper 1885 | --------------------------------------------------------------------------------