├── _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 | 
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 | [](https://pypi.org/project/huobitrade/)
24 | [](https://github.com/hadrianl/huobi/network)
25 | [](https://github.com/hadrianl/huobi/stargazers)
26 | 
27 | 
28 | 
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 |
74 |
95 |
96 |
99 |
100 |
101 |
102 |
105 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | 账户信息
117 |
118 |
119 |
120 | | 账户ID |
121 | 币种 |
122 | 账户类型 |
123 | 账户余额 |
124 |
125 |
126 |
127 |
128 | | {[a['account-id']]} |
129 | {[a.currency]} |
130 | {[a.type]} |
131 | {[a.balance]} |
132 |
133 |
134 |
135 |
136 | 交易信息
137 |
138 |
139 | | 流水号 |
140 | 订单ID |
141 | 交易对 |
142 | 帐号ID |
143 | 订单数量 |
144 | 订单价格 |
145 | 创建时间 |
146 | 类型 |
147 | 来源 |
148 | 状态 |
149 | Role |
150 | 成交价格 |
151 | 成交数量 |
152 | 未成交数量 |
153 | 成交金额 |
154 | 手续费 |
155 |
156 |
157 |
158 |
159 | | {[t['seq-id']]} |
160 | {[t['order-id']]} |
161 | {[t['symbol']]} |
162 | {[t['account-id']]} |
163 | {[t['order-amount']]} |
164 | {[t['order-price']]} |
165 | {[t['created-at']]} |
166 | {[t['order-type']]} |
167 | {[t['order-source']]} |
168 | {[t['order-state']]} |
169 | {[t['role']]} |
170 | {[t['price']]} |
171 | {[t['filled-amount']]} |
172 | {[t['unfilled-amount']]} |
173 | {[t['filled-cash-amount']]} |
174 | {[t['filled-fees']]} |
175 |
176 |
177 |
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 |
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 |
--------------------------------------------------------------------------------