├── .gitignore ├── README.MD ├── config └── .gitkeep ├── main.py ├── poetry.lock ├── pyproject.toml ├── src ├── coinflex_base.py ├── coinflex_flypig.py ├── coinflex_turtle.py ├── common │ ├── constant.py │ └── global_utils.py ├── websocket_app.py └── websocket_base.py └── study ├── Vdub_FX_SniperVX3.pine ├── a_class.py ├── b_class.py ├── bitmex_auth.py ├── bitmex_market_maker.py ├── bitmex_ws.py ├── coinflex_rest.py ├── coinflex_ws.py ├── data.py ├── huobi ├── huobi_api.py ├── huobi_utils.py ├── market_service.py ├── service_base.py ├── tick_data.py └── trade_service.py ├── indicator.py ├── jupyter-notebook-test.ipynb ├── portfolio.py ├── settings.py ├── settings_1.py ├── strategy.py ├── study_bitmex_lib.ipynb ├── study_bitmex_package.py ├── study_bitmex_ws.py ├── test_utils.py ├── tick_data_base.py ├── utils.py ├── utils_1.py └── web.py /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .note 3 | .vscode 4 | __pycache__ 5 | .pytest_cache 6 | .ipynb_* 7 | *.log 8 | 9 | *.json 10 | 11 | logs -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | ## Daxiang Trading Robot - A Crypto Trading Bot 2 | 3 | ## Disclaimer 4 | 5 | - The project is only for study purpose, nothing contained in the Site constitutes investment, legal or tax advice. Neither the information nor any opinion contained in the Site constitutes a solicitation or an offer to buy or sell any securities, futures, options or other financial instruments. Decisions based on information contained on this site are the sole responsibility of the visitor. 6 | -------------------------------------------------------------------------------- /config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw7613781/daxiang_trade/25880681a689fc567ce3655ea34f375a463ae970/config/.gitkeep -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Daxiang trading robot main entry 3 | ''' 4 | import sys 5 | import argparse 6 | from src.coinflex_flypig import CoinflexFlypig 7 | from src.coinflex_turtle import CoinflexTurtle 8 | 9 | if __name__ == '__main__': 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument("strategy", help="Stratege to run") 12 | parser.add_argument("config", help="Config file for the stratege") 13 | args = parser.parse_args() 14 | 15 | strategy = args.strategy 16 | config_file = args.config 17 | 18 | if strategy == "flypig": 19 | coinflexFlypig = CoinflexFlypig(config_file) 20 | coinflexFlypig.websocket_app.wst.join() 21 | coinflexFlypig.websocket_app.check_thread.join() 22 | 23 | elif strategy == "turtle": 24 | coinflexTurtle = CoinflexTurtle(config_file) 25 | coinflexTurtle.websocket_app.wst.join() 26 | coinflexTurtle.websocket_app.check_thread.join() -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "certifi" 3 | version = "2021.10.8" 4 | description = "Python package for providing Mozilla's CA Bundle." 5 | category = "main" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "charset-normalizer" 11 | version = "2.0.10" 12 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 13 | category = "main" 14 | optional = false 15 | python-versions = ">=3.5.0" 16 | 17 | [package.extras] 18 | unicode_backport = ["unicodedata2"] 19 | 20 | [[package]] 21 | name = "idna" 22 | version = "3.3" 23 | description = "Internationalized Domain Names in Applications (IDNA)" 24 | category = "main" 25 | optional = false 26 | python-versions = ">=3.5" 27 | 28 | [[package]] 29 | name = "requests" 30 | version = "2.27.1" 31 | description = "Python HTTP for Humans." 32 | category = "main" 33 | optional = false 34 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 35 | 36 | [package.dependencies] 37 | certifi = ">=2017.4.17" 38 | charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} 39 | idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} 40 | urllib3 = ">=1.21.1,<1.27" 41 | 42 | [package.extras] 43 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 44 | use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] 45 | 46 | [[package]] 47 | name = "urllib3" 48 | version = "1.26.8" 49 | description = "HTTP library with thread-safe connection pooling, file post, and more." 50 | category = "main" 51 | optional = false 52 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 53 | 54 | [package.extras] 55 | brotli = ["brotlipy (>=0.6.0)"] 56 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 57 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 58 | 59 | [[package]] 60 | name = "websocket-client" 61 | version = "1.2.3" 62 | description = "WebSocket client for Python with low level API options" 63 | category = "main" 64 | optional = false 65 | python-versions = ">=3.6" 66 | 67 | [package.extras] 68 | docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] 69 | optional = ["python-socks", "wsaccel"] 70 | test = ["websockets"] 71 | 72 | [metadata] 73 | lock-version = "1.1" 74 | python-versions = "^3.8" 75 | content-hash = "0254b35a1fab99b175066fc9f536e1cdb55530a04390d7457031edbd63401617" 76 | 77 | [metadata.files] 78 | certifi = [ 79 | {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, 80 | {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, 81 | ] 82 | charset-normalizer = [ 83 | {file = "charset-normalizer-2.0.10.tar.gz", hash = "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"}, 84 | {file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"}, 85 | ] 86 | idna = [ 87 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 88 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 89 | ] 90 | requests = [ 91 | {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, 92 | {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, 93 | ] 94 | urllib3 = [ 95 | {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, 96 | {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, 97 | ] 98 | websocket-client = [ 99 | {file = "websocket-client-1.2.3.tar.gz", hash = "sha256:1315816c0acc508997eb3ae03b9d3ff619c9d12d544c9a9b553704b1cc4f6af5"}, 100 | {file = "websocket_client-1.2.3-py3-none-any.whl", hash = "sha256:2eed4cc58e4d65613ed6114af2f380f7910ff416fc8c46947f6e76b6815f56c0"}, 101 | ] 102 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "daxiang_trade" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Tang Wei "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | requests = "^2.27.1" 10 | websocket-client = "^1.2.3" 11 | 12 | [tool.poetry.dev-dependencies] 13 | 14 | [build-system] 15 | requires = ["poetry-core>=1.0.0"] 16 | build-backend = "poetry.core.masonry.api" 17 | -------------------------------------------------------------------------------- /src/coinflex_base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 2022/2/11 21:42 3 | # @Author : tw7613781 4 | # @Site : 5 | # @File : coinflex.py 6 | # @Software: vscode 7 | 8 | import threading 9 | import hmac 10 | import base64 11 | import hashlib 12 | from urllib.parse import urlencode 13 | 14 | import requests 15 | from src.websocket_app import MyWebSocketApp 16 | from src.common.global_utils import * 17 | 18 | class CoinflexBase(): 19 | 20 | def __init__(self, config_file): 21 | self.exchange = "CoinFLEX" 22 | self.init_finish_event = threading.Event() # 用来控制服务初始化完成才处理请求 23 | 24 | self.con_file = config_file 25 | 26 | self.ws_url = get_json_config(file=self.con_file, section=self.exchange, key="WSURL") 27 | self.account_id = get_json_config(file=self.con_file, section=self.exchange, key="USERID") 28 | self.api_key = get_json_config(file=self.con_file, section=self.exchange, key="APIKEY") 29 | self.secret_key = get_json_config(file=self.con_file, section=self.exchange, key="APISECRET") 30 | 31 | self.market = get_json_config(file=self.con_file, section=self.exchange, key="MARKET") 32 | 33 | self.http_host = get_json_config(file=self.con_file, section=self.exchange, key="HTTPURL") 34 | self.http_path = get_json_config(file=self.con_file, section=self.exchange, key="HTTPPATH") 35 | self.nonce = get_json_config(file=self.con_file, section=self.exchange, key="NONCE") 36 | 37 | self.ping_interval = 10 38 | 39 | self.logger = setup_logger(self.account_id + "_" + self.exchange + "_" + current_time_string(), log_path="./logs") 40 | 41 | self.websocket_app = MyWebSocketApp(self) 42 | 43 | self.init_finish_event.set() 44 | 45 | def on_message(self, msg, ws=None): 46 | """ 47 | 处理websocket的market数据 48 | :param msg: 49 | :return: 50 | """ 51 | try: 52 | msg = json.loads(msg) 53 | self.logger.info(msg) 54 | 55 | except: 56 | self.logger.error("on_message error! %s " % msg) 57 | self.logger.error(traceback.format_exc()) 58 | 59 | def on_error(self, error): 60 | """ Called on fatal websocket errors. We exit on these. """ 61 | try: 62 | self.logger.error("%s __on_error : %s", self.exchange, error) 63 | except: 64 | self.logger.error("on_error Error!!!") 65 | self.logger.error(traceback.format_exc()) 66 | 67 | def on_open(self): 68 | """ Called when the WS opens. """ 69 | try: 70 | self.logger.info("%s websocket opened.", self.exchange) 71 | except: 72 | self.logger.error("on_open Error!!!") 73 | self.logger.error(traceback.format_exc()) 74 | 75 | def on_close(self): 76 | """ Called on websocket close.""" 77 | try: 78 | self.logger.info("%s websocket closed.", self.exchange) 79 | except: 80 | self.logger.error("on_close Error!!!") 81 | self.logger.error(traceback.format_exc()) 82 | 83 | def auth_msg(self): 84 | ts = current_milli_ts() 85 | sig_payload = (ts + 'GET/auth/self/verify').encode('utf-8') 86 | signature = base64.b64encode(hmac.new(self.secret_key.encode('utf-8'), sig_payload, hashlib.sha256).digest()).decode('utf-8') 87 | msg = { 88 | 'op': 'login', 89 | 'tag': 1, 90 | 'data': { 91 | 'apiKey': self.api_key, 92 | 'timestamp': ts, 93 | 'signature': signature 94 | } 95 | } 96 | return json.dumps(msg) 97 | 98 | def subscribe_balance_msg(self): 99 | msg = { 100 | 'op': 'subscribe', 101 | 'args': ['balance:all'], 102 | 'tag': 101 103 | } 104 | return json.dumps(msg) 105 | 106 | def subscribe_orders_msg(self, market): 107 | msg = { 108 | 'op': 'subscribe', 109 | 'args': [f'order:{market}'], 110 | 'tag': 102 111 | } 112 | return json.dumps(msg) 113 | 114 | def subscribe_ticker_msg(self, market): 115 | msg = { 116 | 'op': 'subscribe', 117 | 'tag': 1, 118 | 'args': [f'ticker:{market}'] 119 | } 120 | return json.dumps(msg) 121 | 122 | def subscribe_depth_msg(self, market): 123 | msg = { 124 | "op": "subscribe", 125 | "tag": 103, 126 | "args": [f"depth:{market}"] 127 | } 128 | return json.dumps(msg) 129 | 130 | def place_limit_order_msg(self, market, side, quantity, price, recv_window = 1000): 131 | msg = { 132 | 'op': 'placeorder', 133 | 'tag': 123, 134 | 'data': { 135 | 'timestamp': current_milli_ts(), 136 | 'clientOrderId': 1, 137 | 'marketCode': market, 138 | 'side': side, 139 | 'orderType': 'LIMIT', 140 | 'quantity': float(quantity), 141 | 'price': float(price), 142 | "recvWindow": float(recv_window) 143 | } 144 | } 145 | return json.dumps(msg) 146 | 147 | def modify_limit_order_msg(self, market, order_id, new_quantity, new_price, recv_window = 1000): 148 | msg = { 149 | "op": "modifyorder", 150 | "tag": 1, 151 | "data": { 152 | "timestamp": current_milli_ts(), 153 | "marketCode": market, 154 | "orderId": order_id, 155 | "price": float(new_price), 156 | "quantity": float(new_quantity), 157 | "recvWindow": float(recv_window) 158 | } 159 | } 160 | return json.dumps(msg) 161 | 162 | def cancel_limit_order_msg(self, market, order_id): 163 | msg = { 164 | "op": "cancelorder", 165 | "tag": 456, 166 | "data": { 167 | "marketCode": market, 168 | "orderId": order_id 169 | } 170 | } 171 | return json.dumps(msg) 172 | 173 | def private_http_call(self, method, options={}, action='GET'): 174 | ''' 175 | generate header based on api credential 176 | method: private call method 177 | options: parameters if have,, the format is as below 178 | {'key1': 'value1', 'key2': 'value2'} 179 | ''' 180 | ts = datetime.datetime.utcnow().isoformat() 181 | body = urlencode(options) 182 | if options: 183 | path = method + '?' + body 184 | else: 185 | path = method 186 | msg_string = '{}\n{}\n{}\n{}\n{}\n{}'.format(ts, self.nonce, action, self.http_path, method, body) 187 | sig = base64.b64encode(hmac.new(self.secret_key.encode('utf-8'), msg_string.encode('utf-8'), hashlib.sha256).digest()).decode('utf-8') 188 | 189 | header = {'Content-Type': 'application/json', 'AccessKey': self.api_key, 190 | 'Timestamp': ts, 'Signature': sig, 'Nonce': str(self.nonce)} 191 | 192 | if action == 'GET': 193 | resp = requests.get(self.http_host + path, headers=header) 194 | elif action == 'POST': 195 | resp = requests.post(self.http_host + path, headers=header) 196 | return resp.json() 197 | 198 | def getOrders(self): 199 | ''' 200 | get account's unfilled orders 201 | ''' 202 | try: 203 | endpoint = '/v2/orders' 204 | return(self.private_http_call(endpoint)) 205 | except: 206 | self.logger.error("http getOrders Error!!!") 207 | self.logger.error(traceback.format_exc()) 208 | 209 | def getBalance(self): 210 | ''' 211 | get account balance 212 | ''' 213 | try: 214 | endpoint = '/v2/balances' 215 | return(self.private_http_call(endpoint)) 216 | except: 217 | self.logger.error("http getBalance Error!!!") 218 | self.logger.error(traceback.format_exc()) 219 | 220 | def get_available_balance_by_id(self, instrumentId): 221 | ''' 222 | "USD", "FLEX" 223 | ''' 224 | try: 225 | data = self.getBalance()["data"] 226 | available_balance = "0" 227 | for currency in data: 228 | if currency["instrumentId"] == instrumentId: 229 | available_balance = currency["available"] 230 | return available_balance 231 | except: 232 | self.logger.error("get available balance Error!!!") 233 | self.logger.error(traceback.format_exc()) -------------------------------------------------------------------------------- /src/coinflex_flypig.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 2022/2/11 21:42 3 | # @Author : tw7613781 4 | # @Site : 5 | # @File : coinflex.py 6 | # @Software: vscode 7 | 8 | import math 9 | from decimal import Decimal 10 | 11 | from src.common.global_utils import * 12 | from src.coinflex_base import CoinflexBase 13 | 14 | class CoinflexFlypig(CoinflexBase): 15 | 16 | def __init__(self, config_file): 17 | super(CoinflexFlypig, self).__init__(config_file) 18 | 19 | self.strategyName = "Flypig" 20 | 21 | self.buy_price = str(get_json_config(file=self.con_file, section=self.exchange, key="BUYPRICE")) 22 | self.sell_price = str(get_json_config(file=self.con_file, section=self.exchange, key="SELLPRICE")) 23 | self.buy_volume = str(get_json_config(file=self.con_file, section=self.exchange, key="BUYVOLUME")) 24 | self.sell_volume = str(get_json_config(file=self.con_file, section=self.exchange, key="SELLVOLUME")) 25 | self.min_price_step = str(get_json_config(file=self.con_file, section=self.exchange, key="MINPRICESTEP")) 26 | 27 | self.price_update_interval = int(get_json_config(file=self.con_file, section=self.exchange, key="PRICEUPDATEINTERVAL")) 28 | 29 | self.logger = setup_logger(self.account_id + "_" + self.exchange + "_" + self.strategyName + "_" + current_time_string(), log_path="./logs") 30 | self.logger.info(f'{TERM_GREEN}Config loaded ==> user: {self.account_id}, buy_price: {self.buy_price}, sell_price: {self.sell_price}, buy_volume: {self.buy_volume}, sell_volume: {self.sell_volume}, price_update_interval: {self.price_update_interval}{TERM_NFMT}') 31 | 32 | self.orders = [] 33 | self.last_buy_price_updated_ts = 0 34 | self.last_sell_price_updated_ts = 0 35 | 36 | self.recv_window = 1000 37 | 38 | def on_message(self, msg, ws=None): 39 | """ 40 | 处理websocket的market数据 41 | :param msg: 42 | :return: 43 | """ 44 | try: 45 | msg = json.loads(msg) 46 | 47 | if 'event' in msg and msg['event']=='login': 48 | self.logger.info(f'{TERM_GREEN}Login succeed{TERM_NFMT}') 49 | 50 | if 'event' in msg and msg['event']=='modifyorder': 51 | order_modify_succeed = msg['submitted'] if 'submitted' in msg else False 52 | if not order_modify_succeed: 53 | self.logger.error(msg) 54 | data = msg['data'] 55 | if "recvWindow" in msg["message"]: 56 | self.recv_window +=500 57 | self.websocket_app.send_command(self.modify_limit_order_msg(self.market, data["orderId"], data["quantity"], data["price"], self.recv_window)) 58 | elif "FAILED balance check" in msg["message"]: 59 | quantity = Decimal(str(data['quantity'])) - Decimal("0.1") 60 | self.websocket_app.send_command(self.modify_limit_order_msg(self.market, data["orderId"], quantity, data["price"])) 61 | 62 | if 'table' in msg and msg['table']=='depth': 63 | depth_data = msg['data'][0] 64 | new_buy_price, new_sell_price = self.get_best_price(depth_data, self.buy_volume, self.sell_volume, self.min_price_step) 65 | cur_ts = int(current_milli_ts()) 66 | 67 | # 更新buy_price 68 | if Decimal(new_buy_price) != Decimal(self.buy_price): 69 | self.logger.info(f'Update buy_price: {self.buy_price} => {new_buy_price}, {self.sell_price}') 70 | self.buy_price = new_buy_price 71 | 72 | # 如果更新周期到了,更新orders 73 | if (cur_ts - self.last_buy_price_updated_ts) > self.price_update_interval: 74 | self.last_buy_price_updated_ts = int(current_milli_ts()) 75 | for order in self.get_buy_orders(): 76 | # 更新价格的时候,需要更新量,不然usd会超过拥有的usd 77 | quantity = None 78 | if "remainingQuantity" in order: 79 | quantity = order["remainingQuantity"] 80 | elif "remainQuantity" in order: 81 | quantity = order["remainQuantity"] 82 | else: 83 | quantity = order["quantity"] 84 | new_quantity = str(math.floor(Decimal(str(quantity)) * Decimal(str(order["price"])) / Decimal(self.buy_price) * 10) / 10) 85 | if (Decimal(new_quantity) > 0): 86 | self.websocket_app.send_command(self.modify_limit_order_msg(self.market, order["orderId"], new_quantity, self.buy_price)) 87 | 88 | # 更新sell_price 89 | if Decimal(new_sell_price) != Decimal(self.sell_price): 90 | self.logger.info(f'Update sell_price: {self.buy_price}, {self.sell_price} => {new_sell_price}') 91 | self.sell_price = new_sell_price 92 | 93 | if (cur_ts - self.last_sell_price_updated_ts) > self.price_update_interval: 94 | self.last_sell_price_updated_ts = int(current_milli_ts()) 95 | for order in self.get_sell_orders(): 96 | quantity = None 97 | if "remainingQuantity" in order: 98 | quantity = order["remainingQuantity"] 99 | elif "remainQuantity" in order: 100 | quantity = order["remainQuantity"] 101 | else: 102 | quantity = order["quantity"] 103 | # 直接更新sell order的价格 104 | self.websocket_app.send_command(self.modify_limit_order_msg(self.market, order["orderId"], quantity, self.sell_price)) 105 | 106 | if 'table' in msg and msg['table']=='order': 107 | data = msg['data'][0] 108 | # self.logger.info(f'{TERM_BLUE}{data}{TERM_NFMT}') 109 | if 'notice' in data and data['notice'] == 'OrderOpened': 110 | # 开单,把order加入self.orders列表 111 | self.orders.append(data) 112 | self.logger.info(f'{TERM_BLUE}Update order list, add order: {data["orderId"]} - {data["side"]} - {data["price"]} - {data["quantity"]} {TERM_NFMT}') 113 | self.display_orders() 114 | 115 | if 'notice' in data and data['notice'] == 'OrderClosed': 116 | # 关闭单,把order从self.orders列表删除 117 | for index, order in enumerate(self.orders): 118 | if order['orderId'] == data['orderId']: 119 | del self.orders[index] 120 | break 121 | self.logger.info(f'{TERM_BLUE}Update order list, remove order: {data["orderId"]} - {data["side"]} - {data["price"]} - {data["quantity"]} - {data["status"]} {TERM_NFMT}') 122 | self.display_orders() 123 | 124 | if 'notice' in data and data['notice'] == 'OrderModified': 125 | # 修改单,把原order从self.orders列表删除,然后把此order添加进orders列表 126 | for index, order in enumerate(self.orders): 127 | if order['orderId'] == data['orderId']: 128 | del self.orders[index] 129 | break 130 | self.orders.append(data) 131 | self.logger.info(f'{TERM_BLUE}Update order list, modified order: {data["orderId"]} - {data["side"]} - {data["price"]} - {data["quantity"]} - {data["status"]} {TERM_NFMT}') 132 | self.display_orders() 133 | 134 | 135 | if 'notice' in data and data['notice'] == 'OrderMatched': 136 | 137 | self.logger.info(f'{TERM_RED}Order matched: {data["orderId"]} - {data["side"]} - {(data["price"])} - {data["matchQuantity"]}{TERM_NFMT}') 138 | 139 | if Decimal(data["remainQuantity"]) == Decimal("0"): 140 | for index, order in enumerate(self.orders): 141 | if order['orderId'] == data['orderId']: 142 | self.logger.info(f'{TERM_BLUE}Update order list, remove order: {data["orderId"]} - {data["side"]} - {data["price"]} - {data["quantity"]} - {data["status"]} {TERM_NFMT}') 143 | del self.orders[index] 144 | break 145 | 146 | if data['side'] == 'BUY': 147 | # 买单成交了,要挂卖单 148 | self.websocket_app.send_command(self.place_limit_order_msg(self.market, 'SELL', data['matchQuantity'], self.sell_price)) 149 | self.logger.info(f'{TERM_GREEN}Execute sell order: {self.sell_price} - {data["matchQuantity"]}{TERM_NFMT}') 150 | elif data['side'] == 'SELL': 151 | # 卖单成交了,要挂买单 152 | usd_available = self.get_available_USD_balance() 153 | new_quantity = str(math.floor(Decimal(usd_available) / Decimal(self.buy_price) * 10) / 10) 154 | if (Decimal(new_quantity) > 0): 155 | self.websocket_app.send_command(self.place_limit_order_msg(self.market, "BUY", new_quantity, self.buy_price)) 156 | self.logger.info(f'{TERM_GREEN}Execute buy order: {self.buy_price} - {new_quantity}{TERM_NFMT}') 157 | 158 | except: 159 | self.logger.error("on_message error! %s " % msg) 160 | self.logger.error(traceback.format_exc()) 161 | 162 | def on_open(self): 163 | """ Called when the WS opens. """ 164 | try: 165 | self.logger.info("%s websocket opened.", self.exchange) 166 | ## send auth msg and subscribe msgs 167 | self.websocket_app.send_command(self.auth_msg()) 168 | self.websocket_app.send_command(self.subscribe_orders_msg(self.market)) 169 | self.websocket_app.send_command(self.subscribe_depth_msg(self.market)) 170 | msg = self.getOrders() 171 | # self.logger.info(msg) 172 | if 'event' in msg and msg['event']=='orders' and msg['data']: 173 | self.orders = msg['data'] 174 | self.display_orders() 175 | except: 176 | self.logger.error("on_open Error!!!") 177 | self.logger.error(traceback.format_exc()) 178 | 179 | def get_best_price(self, depth_data, buy_volume, sell_volume, min_price_step): 180 | buy_order_table = depth_data["bids"] 181 | buy_orders = self.get_buy_orders() 182 | 183 | sell_order_table = depth_data["asks"] 184 | sell_orders = self.get_sell_orders() 185 | 186 | # buy price 187 | buy_price = None 188 | buy_accumulated_volume = Decimal("0") 189 | for order in buy_order_table: 190 | buy_accumulated_volume += Decimal(str(order[1])) 191 | for my_buy_order in buy_orders: 192 | if Decimal(str(my_buy_order["price"])) == Decimal(str(order[0])): 193 | buy_accumulated_volume -= Decimal(str(my_buy_order["quantity"])) 194 | if buy_accumulated_volume >= Decimal(buy_volume): 195 | buy_price = str(order[0]) 196 | break 197 | if buy_price == None: 198 | buy_price = str(buy_order_table[-1][0]) 199 | if Decimal(add(buy_price, min_price_step)) < Decimal(str(sell_order_table[0][0])): 200 | buy_price = add(buy_price, min_price_step) 201 | 202 | # sell price 203 | sell_price = None 204 | sell_accumulated_volume = Decimal("0") 205 | for order in sell_order_table: 206 | sell_accumulated_volume += Decimal(str(order[1])) 207 | for my_sell_order in sell_orders: 208 | if Decimal(str(my_sell_order["price"])) == Decimal(str(order[0])): 209 | sell_accumulated_volume -= Decimal(str(my_sell_order["quantity"])) 210 | if sell_accumulated_volume >= Decimal(sell_volume): 211 | sell_price = str(order[0]) 212 | break 213 | if sell_price == None: 214 | sell_price = str(sell_order_table[-1][0]) 215 | if Decimal(sub(sell_price, min_price_step)) > Decimal(str(buy_order_table[0][0])): 216 | sell_price = sub(sell_price, min_price_step) 217 | 218 | return buy_price, sell_price 219 | 220 | def display_orders(self): 221 | for order in self.orders: 222 | self.logger.warn(f'Order: {order["orderId"]} - {order["side"]} - {order["price"]} - {order["quantity"]}') 223 | 224 | def get_buy_orders(self): 225 | return list(filter(lambda order: order["side"] == "BUY", self.orders)) 226 | 227 | def get_sell_orders(self): 228 | return list(filter(lambda order: order["side"] == "SELL", self.orders)) -------------------------------------------------------------------------------- /src/coinflex_turtle.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 2022/2/11 21:42 3 | # @Author : tw7613781 4 | # @Site : 5 | # @File : coinflex.py 6 | # @Software: vscode 7 | 8 | from ast import In 9 | import math 10 | from decimal import Decimal 11 | 12 | from src.common.global_utils import * 13 | from src.coinflex_base import CoinflexBase 14 | 15 | class CoinflexTurtle(CoinflexBase): 16 | 17 | def __init__(self, config_file): 18 | super(CoinflexTurtle, self).__init__(config_file) 19 | 20 | self.strategyName = "Turtle" 21 | 22 | self.buy_price = str(get_json_config(file=self.con_file, section=self.exchange, key="BUYPRICE")) 23 | self.sell_price = str(get_json_config(file=self.con_file, section=self.exchange, key="SELLPRICE")) 24 | self.middle_price = str(get_json_config(file=self.con_file, section=self.exchange, key="MIDDLEPRICE")) 25 | self.steps = str(get_json_config(file=self.con_file, section=self.exchange, key="STEPS")) 26 | 27 | self.logger = setup_logger(self.account_id + "_" + self.exchange + "_" + self.strategyName + "_" + current_time_string(), log_path="./logs") 28 | self.logger.info(f'{TERM_GREEN}Config loaded ==> user: {self.account_id}, buy_price: {self.buy_price}, middle_price: {self.middle_price}, sell_price: {self.sell_price}, steps: {self.steps}{TERM_NFMT}') 29 | 30 | self.orders = [] 31 | self.sell_step = None 32 | self.buy_step = None 33 | self.recv_window = 1000 34 | 35 | def on_message(self, msg, ws=None): 36 | """ 37 | 处理websocket的market数据 38 | :param msg: 39 | :return: 40 | """ 41 | try: 42 | msg = json.loads(msg) 43 | 44 | if 'event' in msg and msg['event']=='login': 45 | self.logger.info(f'{TERM_GREEN}Login succeed{TERM_NFMT}') 46 | 47 | if 'event' in msg and msg['event']=='placeorder': 48 | order_succeed = msg['submitted'] if 'submitted' in msg else False 49 | if not order_succeed: 50 | self.logger.error(msg) 51 | data = msg['data'] 52 | if "recvWindow" in msg["message"]: 53 | self.recv_window +=500 54 | self.websocket_app.send_command(self.place_limit_order_msg(self.market, data["side"], data["quantity"], data["price"], self.recv_window)) 55 | elif "FAILED balance check" in msg["message"]: 56 | quantity = Decimal(str(data['quantity'])) - Decimal("0.1") 57 | self.websocket_app.send_command(self.place_limit_order_msg(self.market, data["side"], quantity, data["price"])) 58 | 59 | if 'table' in msg and msg['table']=='depth': 60 | depth_data = msg['data'][0] 61 | new_buy_price, new_sell_price = self.get_best_price(depth_data) 62 | # self.logger.info(f'{new_buy_price} - {new_sell_price}') 63 | 64 | if 'table' in msg and msg['table']=='order': 65 | data = msg['data'][0] 66 | # self.logger.info(f'{TERM_BLUE}{data}{TERM_NFMT}') 67 | if 'notice' in data and data['notice'] == 'OrderOpened': 68 | # 开单,把order加入self.orders列表 69 | self.orders.append(data) 70 | self.logger.info(f'{TERM_BLUE}Update order list, add order: {data["orderId"]} - {data["side"]} - {data["price"]} - {data["quantity"]} {TERM_NFMT}') 71 | self.display_orders() 72 | 73 | if 'notice' in data and data['notice'] == 'OrderClosed': 74 | # 关闭单,把order从self.orders列表删除 75 | for index, order in enumerate(self.orders): 76 | if order['orderId'] == data['orderId']: 77 | del self.orders[index] 78 | break 79 | self.logger.info(f'{TERM_BLUE}Update order list, remove order: {data["orderId"]} - {data["side"]} - {data["price"]} - {data["quantity"]} - {data["status"]} {TERM_NFMT}') 80 | self.display_orders() 81 | 82 | if 'notice' in data and data['notice'] == 'OrderModified': 83 | # 修改单,把原order从self.orders列表删除,然后把此order添加进orders列表 84 | for index, order in enumerate(self.orders): 85 | if order['orderId'] == data['orderId']: 86 | del self.orders[index] 87 | break 88 | self.orders.append(data) 89 | self.logger.info(f'{TERM_BLUE}Update order list, modified order: {data["orderId"]} - {data["side"]} - {data["price"]} - {data["quantity"]} - {data["status"]} {TERM_NFMT}') 90 | self.display_orders() 91 | 92 | 93 | if 'notice' in data and data['notice'] == 'OrderMatched': 94 | 95 | self.logger.info(f'{TERM_RED}Order matched: {data["orderId"]} - {data["side"]} - {(data["price"])} - {data["matchQuantity"]}{TERM_NFMT}') 96 | 97 | if Decimal(data["remainQuantity"]) == Decimal("0"): 98 | for index, order in enumerate(self.orders): 99 | if order['orderId'] == data['orderId']: 100 | self.logger.info(f'{TERM_BLUE}Update order list, remove order: {data["orderId"]} - {data["side"]} - {data["price"]} - {data["quantity"]} - {data["status"]} {TERM_NFMT}') 101 | del self.orders[index] 102 | break 103 | 104 | if data['side'] == 'BUY': 105 | # 买单成交了,要挂卖单 106 | price = data['price'] 107 | steps = (Decimal(self.middle_price) - Decimal(str(price))) / Decimal(self.buy_step) 108 | price = Decimal(str(self.middle_price)) + steps * Decimal(self.sell_step) 109 | format_price = str(math.floor(price * 1000) / 1000) 110 | self.websocket_app.send_command(self.place_limit_order_msg(self.market, 'SELL', data['matchQuantity'], format_price)) 111 | self.logger.info(f'{TERM_GREEN}Execute sell order: {format_price} - {data["matchQuantity"]}{TERM_NFMT}') 112 | elif data['side'] == 'SELL': 113 | # 卖单成交了,要挂买单 114 | price = data['price'] 115 | steps = (Decimal(str(price)) - Decimal(self.middle_price)) / Decimal(self.sell_step) 116 | price = Decimal(str(self.middle_price)) - steps * Decimal(self.buy_step) 117 | format_price = str(math.floor(price * 1000) / 1000) 118 | 119 | usd_available = Decimal(str(data['price'])) * Decimal(str(data['matchQuantity'])) 120 | new_quantity = str(math.floor(Decimal(usd_available) / Decimal(format_price) * 10) / 10) 121 | if (Decimal(new_quantity) > 0): 122 | self.websocket_app.send_command(self.place_limit_order_msg(self.market, "BUY", new_quantity, format_price)) 123 | self.logger.info(f'{TERM_GREEN}Execute buy order: {format_price} - {new_quantity}{TERM_NFMT}') 124 | 125 | except: 126 | self.logger.error("on_message error! %s " % msg) 127 | self.logger.error(traceback.format_exc()) 128 | 129 | def on_open(self): 130 | """ Called when the WS opens. """ 131 | try: 132 | self.logger.info("%s websocket opened.", self.exchange) 133 | ## send auth msg and subscribe msgs 134 | self.websocket_app.send_command(self.auth_msg()) 135 | self.websocket_app.send_command(self.subscribe_orders_msg(self.market)) 136 | self.websocket_app.send_command(self.subscribe_depth_msg(self.market)) 137 | msg = self.getOrders() 138 | if 'event' in msg and msg['event']=='orders' and msg['data']: 139 | self.orders = msg['data'] 140 | self.display_orders() 141 | self.initial_orders() 142 | except: 143 | self.logger.error("on_open Error!!!") 144 | self.logger.error(traceback.format_exc()) 145 | 146 | def get_best_price(self, depth_data): 147 | buy_order_table = depth_data["bids"] 148 | buy_price = buy_order_table[0][0] 149 | 150 | sell_order_table = depth_data["asks"] 151 | sell_price = sell_order_table[0][0] 152 | 153 | return buy_price, sell_price 154 | 155 | def display_orders(self): 156 | for order in self.orders: 157 | self.logger.warn(f'Order: {order["orderId"]} - {order["side"]} - {order["price"]} - {order["quantity"]}') 158 | 159 | def get_buy_orders(self): 160 | return list(filter(lambda order: order["side"] == "BUY", self.orders)) 161 | 162 | def get_sell_orders(self): 163 | return list(filter(lambda order: order["side"] == "SELL", self.orders)) 164 | 165 | def initial_orders(self): 166 | tokens = self.market.split('-') 167 | 168 | sell_token = tokens[0] 169 | sell_balance = str(self.get_available_balance_by_id(sell_token)) 170 | sell_amount = str(math.floor( Decimal(sell_balance) / Decimal(self.steps) * 10) / 10) 171 | self.sell_step = (Decimal(self.sell_price) - Decimal(self.middle_price)) / Decimal(self.steps) 172 | if (Decimal(sell_amount) > 0): 173 | price = self.middle_price 174 | for i in range(int(self.steps)): 175 | price = Decimal(price) + Decimal(self.sell_step) 176 | format_price = str(math.floor(price * 1000) / 1000) 177 | self.websocket_app.send_command(self.place_limit_order_msg(self.market, "SELL", sell_amount, format_price)) 178 | self.logger.info(f'{TERM_GREEN}Execute sell order: {format_price} - {sell_amount}{TERM_NFMT}') 179 | 180 | buy_token = tokens[1] 181 | buy_balance = self.get_available_balance_by_id(buy_token) 182 | buy_amount = str(math.floor(Decimal(buy_balance) / Decimal(self.steps) * 10) / 10) 183 | self.buy_step = (Decimal(self.middle_price) - Decimal(self.buy_price)) / Decimal(self.steps) 184 | price = self.middle_price 185 | for i in range(int(self.steps)): 186 | price = Decimal(price) - Decimal(self.buy_step) 187 | format_price = str(math.floor(price * 1000) / 1000) 188 | amount = str(math.floor(Decimal(buy_amount) / Decimal(format_price) * 10) / 10) 189 | if (Decimal(amount) > 0): 190 | self.websocket_app.send_command(self.place_limit_order_msg(self.market, "BUY", amount, format_price)) 191 | self.logger.info(f'{TERM_GREEN}Execute buy order: {format_price} - {amount}{TERM_NFMT}') 192 | -------------------------------------------------------------------------------- /src/common/constant.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 目录 5 | 1. 普通-无前缀 6 | 2. 信息-MSG前缀 7 | 3. 交易所-EXCHANGE前缀 8 | 4. 订单相关 9 | 5. tick_data 相关字符串 10 | 6. 进程相关 11 | 7. zmq端口名称 12 | . 标点符号 13 | """ 14 | 15 | """ 16 | _____________________________________________________ 17 | 1. 普通字符串定义 18 | """ 19 | 20 | # id 21 | ACCOUNT_ID = "account_id" 22 | CLIENT_ID = "client_id" 23 | GROUP_ID = "group_id" 24 | ORDER_ID = "order_id" 25 | INIT_ORDER_ID = "init_order_id" 26 | CONTRACT_ID = "contract_id" 27 | 28 | # 交易方向 29 | DIRECTION = "direction" 30 | BUY = "buy" 31 | SELL = "sell" 32 | ASKS = "asks" # 卖 33 | BIDS = "bids" # 买 34 | OPEN_POSITION = "open" # 开仓动作 35 | CLOSE_POSITION = "close" # 平仓动作 36 | 37 | # 价格类型 38 | PRICE = "price" 39 | OPEN_PRICE = "open" 40 | HIGH_PRICE = "high" 41 | LAST_PRICE = "last" 42 | LOW_PRICE = "low" 43 | CLOSE_PRICE = "close" 44 | AVG_PRICE = "avg_price" 45 | LIMIT_HIGH_PRICE = "limit_high" 46 | LIMIT_LOW_PRICE = "limit_low" 47 | LIMIT_PRICE = "limit" 48 | MARKET_PRICE = "market" 49 | 50 | # 服务类型 51 | SERVICE_TYPE = "service_type" 52 | SERVICE_MARKET = "market" 53 | SERVICE_TRADE = "trade" 54 | 55 | # 订单字段 56 | VOLUME = "volume" 57 | SIG_TRADE_VOLUME = "sig_trade_volume" 58 | TRADE_PRICE = "trade_price" 59 | TRADE_VOLUME = "trade_volume" 60 | UNIT_AMOUNT = "unit_amount" 61 | HOLD_AMOUNT = "hold_amount" 62 | LIMIT = "limit" 63 | REPEATS = "repeats" 64 | EXCHANGE = "exchange" 65 | CLIENT = "client" 66 | ORDER = "order" 67 | 68 | 69 | STATE = "state" 70 | STATUS = "status" 71 | SYMBOL = "symbol" 72 | STRATEGY_NAME = "strategy_name" 73 | STRATEGY_TYPE = "strategy_type" 74 | PRICE_TYPE = "price_type" 75 | PRODUCT_TYPE = "product_type" 76 | SYMBOL_TYPE = "symbol_type" 77 | SPOT = "spot" # symbol_type的值 78 | FUTURES = "futures" # symbol_type的值 79 | ARBITRAGE = "arbitrage" 80 | COMB_OFFSET_FLAG = "comb_offset_flag" 81 | MODEL = "model" # 表示订单测试或者实盘的key 82 | MODEL_TEST = "test" # 表示订单测试或者实盘的value: 模拟交易模式 83 | MODEL_REAL = "real" # 表示订单测试或者实盘的value: (默认)实盘交易模式 84 | 85 | WATCH = "watch" 86 | MESSAGE = "message" 87 | CODE = "code" 88 | TICKER = "ticker" 89 | DEPTH = "depth" 90 | 91 | # time 92 | TIMESTAMP = "timestamp" 93 | LOCAL_TIME = "local_time" 94 | PLACE_TIME = "place_time" 95 | PLACE_RSP_TIME = "place_rsp_time" 96 | TICK_TIME = "tick_time" 97 | STRATEGY_PLACE_TIME = "strategy_place_time" 98 | QRY_ORDER_TIME = "qry_order_time" 99 | 100 | # error 101 | ERROR = "error" 102 | ERROR_CODE = "error_code" 103 | ERROR_MESSAGE = "error_message" 104 | 105 | # key 106 | API_KEY ="api_key" 107 | SECRET_KEY ="secret_key" 108 | 109 | # python 110 | QUEUE_SIZE_UNLIMITED = -1 111 | """ 112 | _____________________________________________________ 113 | 2. 信息类型 message type 以MSG前缀 114 | """ 115 | MSG_TYPE = "msg_type" 116 | MSG_MARKET_DATA = "market_data" 117 | MSG_MARKET_STATUS = "market_status" 118 | MSG_SUBSCRIBE = "subscribe" 119 | MSG_PLACE_ORDER = "place_order" 120 | MSG_PLACE_ORDER_RSP = "place_order_rsp" 121 | MSG_CANCEL_ORDER = "cancel_order" 122 | MSG_CANCEL_ORDER_RSP = "cancel_order_rsp" 123 | MSG_CURRENT_ORDER = "current_order" 124 | MSG_CURRENT_ORDER_RSP = "current_order_rsp" 125 | 126 | # balance 127 | MSG_QRY_BALANCE = "qry_balance" 128 | MSG_QRY_BALANCE_RSP = "qry_balance_rsp" 129 | MSG_BALANCE_STATUS = "balance_status" 130 | MSG_TRADE_STATUS = "trade_status" 131 | 132 | # order 133 | MSG_QRY_ORDER = "qry_order" 134 | MSG_QRY_ORDER_RSP = "qry_order_rsp" 135 | MSG_GET_ALL_ORDERS = "qry_all_orders" 136 | MSG_GET_ALL_ORDERS_RSP = "qry_all_orders_rsp" 137 | MSG_ORDER_STATUS = "order_status" 138 | 139 | # position 140 | MSG_QRY_POSITION = "qry_position" 141 | MSG_QRY_POSITION_RSP = "qry_position_rsp" 142 | MSG_POSITION_STATUS = "position_status" 143 | 144 | 145 | 146 | 147 | MSG_INIT_EXCHANGE_SERVICE = "init_exchange_service" # 初始化消息类型 148 | MSG_MARKET_CONNECTED = "market_connected" # 行情通道连接成功的消息类型 149 | MSG_MARKET_DISCONNECTED = "market_disconnected" # 行情通道断开连接的消息类型 150 | MSG_TRADE_CONNECTED = "trade_connected" # 交易通道连接成功的消息类型 151 | MSG_TRADE_DISCONNECTED = "trade_disconnected" # 交易通道断开连接的消息类型 152 | MSG_PLACE_ORDER_ERROR = "place_order_error" # 下单失败广播消息的消息类型 153 | 154 | MSG_UPDATE_LEVERAGE = "update_leverage" # 更新杠杆 155 | MSG_UPDATE_LEVERAGE_RSP = "update_leverage_rsp" 156 | 157 | 158 | MSG_LOGIN = "login" # 用户登录 159 | MSG_LOGIN_RSP = "login_rsp" # 用户登录 160 | MSG_REQUIRE_RUNNING_PROCESS = "require_running_process" # 请求正在运行的进程 161 | MSG_REQUIRE_MARKET_SOCKETS = "require_market_sockets" #请求正在运行的行情socket 162 | MSG_REQUIRE_TRADE_SOCKETS = "require_trade_sockets" #请求正在运行的行情socket 163 | 164 | MSG_RUNNING_PROCESS = "running_process" # 信息类型:正在运行的进程. 165 | MSG_MARKET_SOCKETS = "market_sockets" 166 | MSG_TRADE_SOCKETS = "trade_sockets" 167 | 168 | 169 | """ 170 | _____________________________________________________ 171 | 3. 交易所 以EXCHANGE前缀 172 | """ 173 | EXCHANGE_OKEX ="okex" 174 | EXCHANGE_HUOBI ="huobi" 175 | EXCHANGE_BITMEX ="bitmex" 176 | EXCHANGE_BINANCE ="binance" 177 | EXCHANGE_HITBTC = "hitbtc" 178 | EXCHANGE_BITFINEX = "bitfinex" 179 | EXCHANGE_FCOIN = "fcoin" 180 | EXCHANGE_GDAX = "coinbase" 181 | EXCHANGE_58COIN = "58coin" 182 | EXCHANGE_BIGONE = "bigone" 183 | EXCHANGE_BI = "bi" 184 | EXCHANGE_BITSTAMP = "bitstamp" 185 | EXCHANGE_COINBASE = "coinbase" 186 | 187 | 188 | DEFAULT_MARKET_DEPTH = "20" 189 | # binance 订阅1挡行情 190 | DEFAULT_MARKET_DEPTH_ONE = "1" 191 | # tradeserice rsp返回 192 | MSG = "msg" 193 | EXECUTEDQTY = "executedQty" 194 | ORIGQTY = "origQty" 195 | LOCKED = "locked" 196 | ASSET = "asset" 197 | 198 | SIDE = "side" 199 | TYPE = "type" 200 | FUNDS = "funds" 201 | BALANCES = "balances" 202 | UPDATE_TIME = "update_time" 203 | FREE = "free" 204 | FREEZED = "freezed" 205 | TIME = "time" 206 | CANCEL = "cancel" 207 | SYMBOL_LIST = "symbol_list" 208 | SYMBOL_TYPE_LIST = "symbol_type_list" 209 | DATA = "data" 210 | TABLE = "table" 211 | ACTION = "action" 212 | FILTER = "filter" 213 | SUCCESS = "success" 214 | INSTRUMENT = "instrument" 215 | ORDERBOOK10 = "orderBook10" 216 | MARGIN = "margin" 217 | INSERT = "insert" 218 | UPDATE = "update" 219 | ORDERS = "orders" 220 | POSITION = "position" 221 | """ 222 | _____________________________________________________ 223 | 4. 订单相关 224 | """ 225 | ORDER_STATE_UNKNOWN = -1 # 未知 226 | ORDER_STATE_REJECTED = -2 # 拒绝 227 | ORDER_STATE_PRE_SUBMITTED = 0 # 准备提交 228 | ORDER_STATE_SUBMITTING = 1 # 提交中 229 | ORDER_STATE_SUBMITTED = 2 # 已提交 230 | ORDER_STATE_PARTIAL_FILLED = 3 # 部分成交 231 | ORDER_STATE_FILLED = 4 # 完全成交 232 | ORDER_STATE_CANCELLING = 5 # 撤单处理中 233 | ORDER_STATE_PARTIAL_CANCELED = 6 # 部分成交撤销 234 | ORDER_STATE_CANCELED = 7 # 已撤销 235 | ORDER_STATE_EXPIRED = 8 # 订单过期 236 | ORDER_STATE_SUSPENDED = 9 # 暂停 237 | 238 | """balance type after format""" 239 | BALANCE_TYPE_FREEZED = "freezed" 240 | BALANCE_TYPE_FREE = "free" 241 | BALANCE_TYPE_BORROW = "borrow" 242 | 243 | """status of common rsp""" 244 | COMMON_RSP_STATUS_TRUE = True 245 | COMMON_RSP_STATUS_FALSE = False 246 | 247 | MARKET_DEPTH = "market_depth" 248 | 249 | DEFAULT_DEPTH = 20 250 | """更新资金信息的间隔时间为5分钟""" 251 | UPDATE_BALANCE_INTERVAL_TIME = 5 * 60 252 | 253 | """json section""" 254 | DATA_PROXY = "data_proxy" 255 | 256 | # timer定时器执行间隔,0表示立即执行 257 | TIMER_INTERVAL_NOW = 0 258 | 259 | 260 | """ 261 | _____________________________________________________ 262 | 5. tick_data 相关字符串 263 | """ 264 | 265 | TK_SYMBOL = "symbol" 266 | TK_EXCHANGE = "exchange" 267 | TK_SYMBOL_TYPE = "symbol_type" 268 | TK_OPEN = "open" 269 | TK_HIGH = "high" 270 | TK_LAST = "last" 271 | TK_LOW = "low" 272 | TK_VOLUME = "volume" 273 | TK_LIMIT_HIGH = "limit_high" 274 | TK_LIMIT_LOW = "limit_low" 275 | TK_ASKS = "asks" 276 | TK_BIDS = "bids" 277 | TK_UNIT_AMOUNT = "unit_amount" 278 | TK_HOLD_AMOUNT = "hold_amount" 279 | TK_CONTRACT_ID = "contract_id" 280 | TK_TIMESTAMP = "timestamp" 281 | TK_LOCAL_TIME = "local_time" 282 | 283 | 284 | 285 | """ 286 | _____________________________________________________ 287 | 6. 进程相关 288 | """ 289 | PROCESS = "process" 290 | PROC_DEAD = 0 291 | PROC_ALIVE = 1 292 | SERVICE_MANAGER = "service_manager" 293 | ORDER_MANAGER = "order_manager" 294 | MARKET_SERVICE = "market_service" 295 | TRADE_SERVICE = "trade_service" 296 | IP = "ip" 297 | CIPHER = "cipher" 298 | PROTOCOL = "protocol" 299 | ADDRESS = "address" 300 | NAME = "name" 301 | LOCAL_ADDRESS = "local_address" 302 | PUBLIC_ADDRESS = "public_address" 303 | PARAMETER = "parameter" 304 | 305 | 306 | """ 307 | _____________________________________________________ 308 | 7. zmq端口名称 309 | """ 310 | SERVER_PROXY_MARKET = "server_proxy_market" 311 | SERVER_PROXY_TRADE = "server_proxy_trade" 312 | SERVER_PROXY_REQUEST = "server_proxy_request" 313 | 314 | SERVICE_MANAGER_LOGIN_IN = "service_manager_login_in" 315 | SM_LOGIN_LOCAL = "login_local_ip" 316 | SM_LOGIN_REMOTE ="login_remote_ip" 317 | SM_LOGIN_PORT = 'login_port' 318 | 319 | MANAGER_BROADCAST_PORT = "manager_broadcast_port" 320 | MANAGER_MESSAGE_PORT = "manager_message_port" 321 | 322 | 323 | TCP_LOCAL_ADDRESS_HEAD = "tcp://*:" 324 | 325 | ZMQ_TYPE_PULL = "pull" 326 | ZMQ_TYPE_PUSH = "push" 327 | ZMQ_TYPE_PUB = "pub" 328 | ZMQ_TYPE_SUB = "sub" 329 | ZMQ_TYPE_REQ = "req" 330 | ZMQ_TYPE_REP = "rep" 331 | 332 | 333 | """ 334 | _____________________________________________________ 335 | sql语句 336 | """ 337 | 338 | SQLTABLE_TB_PROCESS = "tb_process" 339 | SQL_ALIVE_PROCESS = "select * from tb_process where status=1" 340 | SQL_ALIVE_PROCESS_IP = "select ip from tb_process where status=1" #找到状态为1(alive)的所有ip. 341 | SQL_ALIVE_PROCESS_NAME = "select process from tb_process where status=1" #找到状态为1(alive)的所有name. 342 | SQL_GET_LOGIN_KEYS = "select public_key, private_key from tb_cipher where name = 'login_demo'" 343 | 344 | TB_ORDER = "tb_order" 345 | TB_TRADE_DETAIL = "tb_trade_detail" 346 | TB_ORDER_CURRENT = "tb_order_current" 347 | TB_ORDER_FINISH = "tb_order_finish" 348 | TB_ORDER_ALL = "tb_order_all" 349 | TB_ORDER_RECORDS = "tb_order_records" 350 | 351 | 352 | 353 | """ 354 | _____________________________________________________ 355 | 标点符号 356 | """ 357 | MARK_COMMA = "," 358 | MARK_PERIOD = "." 359 | MARK_UNDERLINE = "_" 360 | MARK_HYPHEN = "-" 361 | MARK_COLON = ":" 362 | MARK_PLUS = "+" 363 | MARK_MINUS = "-" 364 | MARK_QUESTIONMARK = "?" 365 | MARK_EXCLAMATION = "!" 366 | MARK_SEMICOLON = ";" 367 | MARK_PARENTHESES_L = "(" 368 | MARK_PARENTHESES_R = ")" 369 | MARK_DOLLAR = "$" 370 | MARK_ADDRESS = "@" 371 | MARK_EQUAL = "=" 372 | MARK_SPACE = " " # 空格 373 | MARK_EMPTY_STRING = "" 374 | 375 | PYTHON_EXTENSION = ".py" 376 | 377 | """ 378 | bitmex错误码 379 | """ 380 | SYSTEM_OVERLOADED = "The system is currently overloaded. Please try again later" 381 | """ 382 | bitmex重新下单次数 383 | """ 384 | PLACE_ORDER_NUM = 5 385 | """ 386 | 错误码定义 387 | """ 388 | # 未定义错误 389 | ERROR_CODE_UNKNOW_ERROR = 100000 390 | ERROR_MSG_UNKNOW_ERROR = "unknow error" 391 | # 秘钥不存在 392 | ERROR_CODE_SIGN_NOT_EXIST = 114000 393 | ERROR_MSG_SIGN_NOT_EXIST = "the sign not exist" 394 | # 签名未通过验证 395 | ERROR_CODE_SIGN_AUTH_INVALID = 114001 396 | ERROR_MSG_SIGN_AUTH_INVALID = "the sign is invalid" 397 | # 请求参数异常 例如签名验证的时候参数有误 398 | ERROR_CODE_REQUEST_PARAM_ERROR = 114002 399 | ERROR_MSG_REQUEST_PARAM_ERROR = "the request params is error" 400 | # 交易所系统错误 401 | ERROR_CODE_EXCHANGE_SYSTEM_ERROR = 133000 402 | ERROR_MSG_EXCHANGE_SYSTEM_ERROR = "the exchange system error" 403 | # 访问超时 404 | ERROR_CODE_REQUEST_TIMEOUT = 133001 405 | ERROR_MSG_REQUEST_TIMEOUT = "the request timeout" 406 | # 访问频繁 407 | ERROR_CODE_TOO_MANY_REQUEST = 133002 408 | ERROR_MSG_TOO_MANY_REQUEST = "too many request" 409 | # 交易被冻结 410 | ERROR_CODE_TRADE_IS_FREEZE = 133003 411 | ERROR_MSG_TRADE_IS_FREEZE = "the trade is freeze" 412 | # 系统过载,请再试一次 413 | ERROR_CODE_CURRENTLY_OVERLOADED = 133004 414 | ERROR_MSG_CURRENTLY_OVERLOADED = "The system is currently overloaded. Please try again later" 415 | # 请求的时间戳与服务器时间相差太大 416 | ERROR_CODE_TIMESTAMP_AHEAD_OF_SERVER = 133004 417 | ERROR_MSG_TIMESTAMP_AHEAD_OF_SERVER = "the timestamp was 1000ms ahead of the server's time" 418 | # 请求参数有误 例如签名通过但是下单时候的参数有误,如价格为0或者量为0 419 | ERROR_CODE_ORDER_PARAM_ERROR = 134000 420 | ERROR_MSG_ORDER_PARAM_ERROR = "the order params is error" 421 | # 资金不足 422 | ERROR_CODE_BALANCE_NOT_ENOUGH = 134001 423 | ERROR_MSG_BALANCE_NOT_ENOUGH = "the balance not enough" 424 | # 交易对不存在 425 | ERROR_CODE_SYMBOL_NO_EXISTS = 134002 426 | ERROR_MSG_SYMBOL_NO_EXISTS = "the symbol no exists" 427 | # 下单最小交易量 428 | ERROR_CODE_MIN_VOLUME_NO_SATISFIABLE = 134003 429 | ERROR_MSG_MIN_VOLUME_NO_SATISFIABLE = "the min volume no satisfiable" 430 | # 订单状态错误 431 | ERROR_CODE_ORDER_STATUS_ERROR = 134004 432 | ERROR_MSG_ORDER_STATUS_ERROR = "the order status error" 433 | # 下单精度有误 434 | ERROR_CODE_OUT_OF_PRECISION_LIMIT = 134005 435 | ERROR_MSG_OUT_OF_PRECISION_LIMIT = "the order out of precision limit" 436 | # 下单数量为0 437 | ERROR_CODE_VOLUME_IS_ZERO = 134006 438 | ERROR_MSG_VOLUME_IS_ZERO = "the volume is 0" 439 | # 撤单失败 440 | ERROR_CODE_CANCEL_ORDER_FAILED = 134007 441 | ERROR_MSG_CANCEL_ORDER_FAILED = "cancel order is failed" 442 | # 券商id不存在 443 | ERROR_CODE_SECURITIES_NOT_EXIST = 134008 444 | ERROR_MSG_SECURITIES_NOT_EXIST = "securities id is not exist" 445 | # 没有交易市场信息 446 | ERROR_CODE_TRADE_MARKET_NOT_EXIST = 134009 447 | ERROR_MSG_TRADE_MARKET_NOT_EXIST = "trade market is not exist" 448 | # 必选参数不能为空 449 | ERROR_CODE_REQUIRED_PARAMS_NOT_NULL = 134010 450 | ERROR_MSG_REQUIRED_PARAMS_NOT_NULL = "required params is not null" 451 | # 停止交易 452 | ERROR_CODE_TRADE_IS_STOP= 134011 453 | ERROR_MSG_TRADE_IS_STOP = "trade is stop" 454 | # 价格发现期间您只可下市价单 455 | ERROR_CODE_PLACE_MARKET_ORDER = 134012 456 | ERROR_MSG_PLACE_MARKET_ORDER = "required place market order" 457 | # 价格发现第二阶段您不可以撤单 458 | ERROR_CODE_NOT_CANCEL_ORDER = 134013 459 | ERROR_MSG_NOT_CANCEL_ORDER = "not required cancel order" 460 | # 没有最新行情信息 461 | ERROR_CODE_MARKET_NOT_MSG = 134014 462 | ERROR_MSG_MARKET_NOT_MSG = "the market is not msg" 463 | # 没有K线类型 464 | ERROR_CODE_KLINE_IS_NOT_MSG = 134015 465 | ERROR_MSG_KLINE_IS_NOT_MSG = "the kline is not msg" 466 | # 订单不存在 467 | ERROR_CODE_ORDER_NOT_EXIST = 1340016 468 | ERROR_MSG_ORDER_NOT_EXIST = "the order is not exist" 469 | # 下单失败 470 | ERROR_CODE_PLACE_ORDER_ERROR = 1340017 471 | ERROR_MSG_PLACE_ORDER_ERROR = "place order is error" 472 | # 请求的新订单太多 473 | ERROR_CODE_PLACE_ORDERS_TOO_MANY = 1340018 474 | ERROR_MSG_PLACE_ORDERS_TOO_MANY = "place order too many" 475 | # 期货下单价格有误 476 | ERROR_CODE_FUTURES_PLACE_ORDERS_ERROR = 1340019 477 | ERROR_MSG_FUTURES_PLACE_ORDERS_ERROR = "futures place order error" 478 | # 当前订单已经成交 479 | ERROR_CODE_ORDERS_IS_FILLED = 1340020 480 | ERROR_MSG_ORDERS_IS_FILLED = "the order is filled" 481 | # 当前订单已经撤销 482 | ERROR_CODE_ORDERS_IS_CANCELED = 1340021 483 | ERROR_MSG_ORDERS_IS_CANCELED = "the order is canceled" 484 | # 订单总价精度问题 485 | ERROR_CODE_AMOUNT_PRICE_ERROR = 1340022 486 | ERROR_MSG_AMOUNT_PRICE_ERROR = "the amount price is error" 487 | 488 | 489 | -------------------------------------------------------------------------------- /src/common/global_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import configparser 6 | import logging 7 | import logging.handlers 8 | import json 9 | from threading import Lock 10 | import sys 11 | import traceback 12 | import platform 13 | import threading 14 | import src.common.constant as constant 15 | from getpass import getuser 16 | import time 17 | import datetime 18 | from decimal import Decimal 19 | 20 | # if "Linux" in platform.system(): 21 | # import fcntl 22 | 23 | 24 | EMPTY_RETURN = "" 25 | 26 | #global dict to read json files 27 | JSON_DICT = {} 28 | 29 | #a lock to protect json conf file read and write 30 | JSON_LOCK = Lock() 31 | 32 | TERM_RED = '\033[1;31m' 33 | TERM_NFMT = '\033[0;0m' 34 | TERM_BLUE = '\033[1;34m' 35 | TERM_GREEN = '\033[1;32m' 36 | 37 | 38 | # return true if current system is Windows 39 | def is_windows_system(): 40 | return "Windows" in platform.system() 41 | 42 | # return true if current system is Linux 43 | def is_linux_system(): 44 | return "Linux" in platform.system() 45 | 46 | # return true if current system is MacOS 47 | def is_macos_system(): 48 | return "Darwin" in platform.system() 49 | 50 | def get_transfer_addr(ip): 51 | """get target address based on types: tcp, udp, ipc 52 | 53 | Arguments: 54 | ip {str} -- [string get from the json conf file] 55 | 56 | Returns: 57 | [str] -- [address string] 58 | """ 59 | mark = "://" 60 | mark_index = ip.index(mark) if mark in ip else 0 61 | type = ip[: mark_index] 62 | if not type: 63 | return None 64 | if type.lower() == "tcp" or type.lower() == "udp": 65 | return ip 66 | elif type.lower() == "ipc": 67 | cur_dir = os.path.dirname(os.path.dirname(__file__)) 68 | file_name = ip[mark_index + len("://"):] # the len("://") is 3 69 | path = os.path.join(cur_dir, "ipc") 70 | if not os.path.exists(path): 71 | os.makedirs(path) 72 | path = "ipc://" + os.path.join(path, file_name) 73 | return path 74 | 75 | def get_current_func_name(): 76 | return (sys._getframe().f_code.co_filename + " : " + sys._getframe().f_code.co_name + "()") 77 | 78 | 79 | #TODO 测一下 80 | def get_common_parent_path(file=None): 81 | """ 82 | return to upper level of common 83 | 当前文件在common文件夹, 返回到上一层目录的路径 84 | 85 | file {string} -- "file name if you need to add after parent path/要组装的文件名" 86 | """ 87 | parent = os.path.dirname(os.path.dirname(__file__)) 88 | if file: 89 | result = os.path.join(parent, file) 90 | return result 91 | else: 92 | return parent 93 | 94 | def get_json_config(file, section , key=None, default = EMPTY_RETURN): 95 | """get json file 96 | 97 | Arguments: 98 | file {string} -- absolute file path 99 | section {string} -- level1 key 100 | 101 | Keyword Arguments: 102 | key {string} -- level2 key (default: {None}) 103 | 104 | Returns: 105 | dict -- json dict 106 | """ 107 | try: 108 | global JSON_LOCK 109 | with JSON_LOCK: 110 | global JSON_DICT 111 | if file not in JSON_DICT: 112 | if os.path.exists(file): 113 | if os.path.getsize(file): 114 | with open(file, mode="r", encoding="utf-8") as json_file: 115 | # if is_linux_system(): 116 | # fcntl.flock(json_file, fcntl.LOCK_EX) 117 | global_dict = json.load(json_file) 118 | JSON_DICT[file] = global_dict 119 | else: 120 | JSON_DICT[file] = {} 121 | else: 122 | JSON_DICT[file] = {} 123 | 124 | if section in JSON_DICT[file]: 125 | if key and key in JSON_DICT[file][section]: 126 | data = JSON_DICT[file][section][key] 127 | elif key and key not in JSON_DICT[file][section]: 128 | data = default 129 | else: 130 | data = JSON_DICT[file][section] 131 | else: 132 | data = JSON_DICT[file] 133 | 134 | return data 135 | except: 136 | traceback.print_exc() 137 | return {} 138 | 139 | 140 | def set_json_config(file, section, value, key=None): 141 | """set json file 142 | 143 | Arguments: 144 | file {string} -- absolute file path 145 | section {string} -- level1 key 146 | 147 | Keyword Arguments: 148 | key {string} -- level2 key (default: {None}) 149 | 150 | Returns: 151 | dict -- json dict 152 | """ 153 | try: 154 | global JSON_DICT 155 | global JSON_LOCK 156 | with JSON_LOCK: 157 | if file not in JSON_DICT: 158 | if os.path.exists(file): 159 | if os.path.getsize(file): 160 | with open(file, mode="r", encoding="utf-8") as json_file: 161 | # if is_linux_system(): 162 | # fcntl.flock(json_file, fcntl.LOCK_EX) 163 | global_dict = json.load(json_file) 164 | JSON_DICT[file] = global_dict 165 | else: 166 | JSON_DICT[file] = {} 167 | else: 168 | JSON_DICT[file] = {} 169 | 170 | if section not in JSON_DICT[file]: 171 | JSON_DICT[file][section] = {} 172 | 173 | if key: 174 | JSON_DICT[file][section][key] = value 175 | else: 176 | JSON_DICT[file][section] = value 177 | 178 | with open(file, mode="w", encoding="utf-8") as json_file: 179 | # if is_linux_system(): 180 | # fcntl.flock(json_file, fcntl.LOCK_EX) 181 | data = json.dumps(JSON_DICT[file], ensure_ascii=False, indent=4) 182 | json_file.write(data) 183 | except: 184 | traceback.print_exc() 185 | 186 | def get_ini_config(file, section, key): 187 | try: 188 | config = configparser.ConfigParser() 189 | config.read(file) 190 | return config.get(section, key) 191 | except: 192 | traceback.print_exc() 193 | return EMPTY_RETURN 194 | 195 | 196 | def set_ini_config(file, section, key, value): 197 | try: 198 | config = configparser.ConfigParser() 199 | config.read(file) 200 | config.set(section, key, value) 201 | config.write(open(file, "w")) 202 | except: 203 | traceback.print_exc() 204 | 205 | def setup_logger(log_file_name, log_level=logging.INFO, print_level=logging.INFO, log_path=None, file_size=None): 206 | """ 207 | Init LOG module here 208 | """ 209 | #1.读属于哪个文件夹 210 | LOG_DIR = './logs' 211 | 212 | #2.拼一个文件夹路径 213 | 214 | if is_windows_system(): 215 | #windows下 如果没有配置, 则使用文件夹下log文件夹 216 | if not log_path: 217 | LOG_DIR = os.path.join(get_common_parent_path(), "log") 218 | LOG_FILE = os.path.join(LOG_DIR, log_file_name + ".log") 219 | 220 | else: 221 | LOG_DIR = log_path 222 | LOG_FILE = os.path.join(LOG_DIR, log_file_name+".log") 223 | 224 | elif is_linux_system() or is_macos_system(): 225 | # linux下 如果没有配置, 则使用/tmp文件夹 226 | if not log_path: 227 | LOG_DIR = "/tmp/coin_trade/log/" + getuser() + "/default_log" #配置到统一 /tmp文件夹 228 | LOG_FILE = LOG_DIR + "/" + log_file_name + ".log" 229 | else: 230 | LOG_DIR = log_path 231 | LOG_FILE = os.path.join(LOG_DIR, log_file_name + ".log") 232 | 233 | 234 | if not os.path.exists(LOG_DIR): 235 | os.makedirs(LOG_DIR) 236 | 237 | #加入不同的文件存储方式: size time 238 | if file_size: 239 | handler = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=file_size, backupCount=60) 240 | else: 241 | handler = logging.handlers.TimedRotatingFileHandler(LOG_FILE, when='D', interval=1) 242 | handler.suffix = '%Y-%m-%d.log' 243 | fmt = '%(asctime)s - %(levelname)s - %(filename)s:%(lineno)s ---- %(message)s' 244 | 245 | formatter = logging.Formatter(fmt) # 实例化formatter 246 | handler.setFormatter(formatter) # 为handler添加formatter 247 | 248 | 249 | logger = logging.getLogger(log_file_name) # 获取名为tst的logger 250 | logger.addHandler(handler) # 为logger添加handler 251 | #DEBUG 20180418 252 | logger.setLevel(log_level) 253 | 254 | # Prints logger info to terminal 255 | ch = logging.StreamHandler() 256 | #DEBUG 20180418 257 | ch.setLevel(print_level) 258 | # create formatter 259 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 260 | # add formatter to ch 261 | ch.setFormatter(formatter) 262 | logger.addHandler(ch) 263 | 264 | # 添加日志文件权限 265 | os.chmod(LOG_FILE, 0o777) #0o标志, 777全读写运行权限 266 | os.chmod(LOG_DIR, 0o777) 267 | return logger 268 | 269 | def setup_save_data_logger(file_name, level=logging.INFO, isPrint=False, path=None, file_size=None): 270 | """ 271 | Init LOG module here 272 | """ 273 | #1.读属于哪个文件夹 274 | 275 | #2.拼一个文件夹路径 276 | 277 | if is_windows_system(): 278 | #windows下 如果没有配置, 则使用文件夹下log文件夹 279 | if not path: 280 | LOG_DIR = os.path.join(get_common_parent_path(), "log") 281 | LOG_FILE = os.path.join(LOG_DIR, file_name + ".dict") 282 | 283 | else: 284 | LOG_DIR = path 285 | LOG_FILE = os.path.join(LOG_DIR, file_name + ".dict") 286 | 287 | elif is_linux_system(): 288 | # linux下 如果没有配置, 则使用/tmp文件夹 289 | if not path: 290 | json_dir = os.path.join(get_common_parent_path(), "global.json") 291 | folder_name = get_json_config(file=json_dir, section="path", key="folder_name") 292 | LOG_DIR = "/tmp/coin_trade/log/" + getuser() + "/" + folder_name #配置到统一 /tmp文件夹 293 | LOG_FILE = LOG_DIR + "/" + file_name + ".dict" 294 | else: 295 | LOG_DIR = path 296 | LOG_FILE = os.path.join(LOG_DIR, file_name + ".dict") 297 | 298 | 299 | if not os.path.exists(LOG_DIR): 300 | os.makedirs(LOG_DIR) 301 | 302 | #加入不同的文件存储方式: size time 303 | if file_size: 304 | handler = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=file_size, backupCount=60) 305 | else: 306 | handler = logging.handlers.TimedRotatingFileHandler(LOG_FILE, when='D', interval=1) 307 | handler.suffix = '%Y-%m-%d.dict' 308 | fmt = '%(message)s' 309 | 310 | formatter = logging.Formatter(fmt) # 实例化formatter 311 | handler.setFormatter(formatter) # 为handler添加formatter 312 | 313 | 314 | logger = logging.getLogger(file_name) # 获取名为tst的logger 315 | logger.addHandler(handler) # 为logger添加handler 316 | logger.setLevel(level) 317 | 318 | # Prints logger info to terminal 319 | if isPrint: 320 | ch = logging.StreamHandler() 321 | ch.setLevel(logging.INFO) 322 | # create formatter 323 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 324 | # add formatter to ch 325 | ch.setFormatter(formatter) 326 | logger.addHandler(ch) 327 | 328 | # 添加日志文件权限 329 | os.chmod(LOG_FILE, 0o777) # 0o标志, 777全读写运行权限 330 | os.chmod(LOG_DIR, 0o777) 331 | 332 | return logger 333 | 334 | def print_error(actual_func): 335 | """decorator to print exception 336 | 打印错误的装饰器糖 337 | """ 338 | def my_decorate(*args, **keyargs): 339 | try: 340 | return actual_func(*args, **keyargs) 341 | except: 342 | print("Error execute: {}".format(actual_func.__name__)) 343 | traceback.print_exc() 344 | return my_decorate 345 | 346 | 347 | def call_timer(execute_method=None, args=[]): 348 | """ 349 | 通过timer执行指定函数 350 | :param execute_method: 351 | :param args: 352 | :return: 353 | """ 354 | if execute_method: 355 | try: 356 | timer = threading.Timer(constant.TIMER_INTERVAL_NOW, execute_method, args) 357 | timer.start() 358 | except: 359 | print("Error: call_timer error.") 360 | traceback.print_exc() 361 | else: 362 | print("Error: call_timer error,execute_method is None.") 363 | 364 | 365 | def get_current_time(): 366 | return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) 367 | 368 | 369 | def format_timestamp_to_date(timestamp): 370 | """ 371 | 时间戳(毫秒级、微秒级)转换成日期 372 | :param timestamp: 373 | :return: 374 | """ 375 | ts_str = str(timestamp) 376 | return time.strftime("%Y-%m-%d", time.localtime(int(ts_str[:10]))) 377 | 378 | 379 | def get_yestoday(mytime, format="%Y-%m-%d"): 380 | """ 381 | 获取当前日期的前一天 382 | :param mytime: 383 | :param format: 384 | :return: 385 | """ 386 | myday = datetime.datetime.strptime(mytime, format) 387 | my_yestoday = myday - datetime.timedelta(days=1) 388 | return my_yestoday.strftime(format) 389 | 390 | def get_current_trade_day(): 391 | return get_current_time()[:len("2000-01-01")] 392 | 393 | def get_port_from_address(address): 394 | """ 395 | find the port(str) in a ip address 396 | :param address: 397 | :return: 398 | """ 399 | try: 400 | index1 = address.find("://")+1 401 | index2 = address.find(":", index1)+1 402 | port = address[index2:] 403 | return port 404 | except: 405 | traceback.print_exc() 406 | return None 407 | 408 | 409 | def get_availabel_ip( is_tcp=False): 410 | """读取json得到所有默认可用的ip 411 | 412 | Arguments: 413 | is_tcp {bool} -- 是否是tcp模式, 默认是False(ipc模式) 414 | """ 415 | json_path = get_common_parent_path("global.json") 416 | ip_section = "ip_section" 417 | ip_dict = get_json_config(file=json_path, section=ip_section) 418 | ip = "" #TODO 419 | tcp_ips = ip_dict["tcp"] 420 | ipc_ips = ip_dict["ipc"] 421 | 422 | if is_tcp: 423 | return tcp_ips 424 | else: 425 | return ipc_ips 426 | 427 | def set_default_address(address, is_tcp=False): 428 | """设置json的ip配置项 429 | 430 | Arguments: 431 | address_list {list} -- 可用列表 432 | is_tcp {bool} -- 是否是tcp模式 (default: {False}) 433 | """ 434 | ip_section = 'ip_section' 435 | key = "tcp" if is_tcp else "ipc" 436 | 437 | json_path = get_common_parent_path("global.json") 438 | 439 | colon_index_1 = address.find(":") 440 | colon_index_2 = address.find(":",colon_index_1 + 1) + 1 441 | port = int(address[colon_index_2: ]) 442 | set_json_config(file=json_path, section=ip_section, key=key, value=port) 443 | # 时区转换 utc转换本地时间 444 | def utc_local(utc_st): 445 | now_stamp = time.time() 446 | local_time = datetime.datetime.fromtimestamp(now_stamp) 447 | utc_time = datetime.datetime.utcfromtimestamp(now_stamp) 448 | offset = local_time - utc_time 449 | local_st = utc_st + offset 450 | return local_st 451 | 452 | # def main(): 453 | # # mfile = get_common_parent_path("global.json") 454 | # m = setup_logger(log_file_name="testname") 455 | # # print(get_json_config(file=mfile, section="data_proxy",key="proxy_bind_server_request")) 456 | 457 | # # set_json_config(file=mfile, section="te2st",key="test_key1",value="ddd") 458 | # # print(JSON_DICT) 459 | 460 | def current_milli_ts() -> str: 461 | return str(int(time.time() * 1000)) 462 | 463 | def current_time_string() -> str: 464 | return datetime.datetime.now().strftime("%Y%m%d%H%M%S") 465 | 466 | def add(a, b) -> str: 467 | return str(Decimal(a) + Decimal(b)) 468 | 469 | def sub(a, b) -> str: 470 | return str(Decimal(a) - Decimal(b)) 471 | 472 | def mul(a, b) -> str: 473 | return str(Decimal(a) * Decimal(b)) 474 | 475 | def div(a, b) -> str: 476 | return str(Decimal(a) / Decimal(b)) 477 | 478 | 479 | if __name__ == '__main__': 480 | # get_one_availabel_addr() 481 | pass -------------------------------------------------------------------------------- /src/websocket_app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 2018/7/18 21:42 3 | # @Author : xnbo 4 | # @Site : 5 | # @File : websocket_app.py 6 | # @Software: PyCharm 7 | 8 | import ssl 9 | import threading 10 | import websocket 11 | import traceback 12 | from time import sleep 13 | from src.websocket_base import WebSocketBase 14 | 15 | class MyWebSocketApp(WebSocketBase): 16 | def __init__(self, service_base, header=None): 17 | super(MyWebSocketApp, self).__init__(service_base) 18 | self.header = header 19 | self.service_base = service_base 20 | 21 | self.exited = False # 为false不启动重新检测 22 | 23 | self.ws = None 24 | self.connect(self.url) 25 | # 启动检测线程 26 | self.start_check_thread() 27 | 28 | def exit(self): 29 | self.exited = True 30 | self.ws.close() 31 | 32 | def check_thread_impl(self): 33 | while True: 34 | try: 35 | if (not self.ws.sock or not self.ws.sock.connected) and self.exited: 36 | if self.ws.sock: 37 | self.ws.close() 38 | self.logger.info("%s reconnecting to %s", self.exchange, self.url) 39 | self.connect(self.url) 40 | else: 41 | sleep(1) 42 | except: 43 | self.logger.error(traceback.print_exc()) 44 | 45 | def connect(self, url): 46 | """ Connect to the websocket in a thread.""" 47 | try: 48 | self.exited = True 49 | self.ws = websocket.WebSocketApp(url, 50 | on_message=self.__on_message, 51 | on_close=self.__on_close, 52 | on_open=self.__on_open, 53 | on_error=self.__on_error, 54 | header=self.header) 55 | if self.exchange.lower() == "bitmex": 56 | ssl_defaults = ssl.get_default_verify_paths() 57 | sslopt_ca_certs = {'ca_certs': ssl_defaults.cafile} 58 | self.wst = threading.Thread(target=lambda: self.ws.run_forever(sslopt=sslopt_ca_certs)) 59 | else: 60 | self.wst = threading.Thread(target=lambda: self.ws.run_forever(ping_interval=self.ping_interval)) 61 | self.wst.daemon = True 62 | self.wst.start() 63 | self.logger.info("%s websocket run_forever thread started", self.exchange) 64 | 65 | # Wait for connect before continuing 66 | conn_timeout = 10 67 | while (not self.ws.sock or not self.ws.sock.connected) and conn_timeout: 68 | sleep(1) 69 | conn_timeout -= 1 70 | if not conn_timeout: 71 | self.logger.error("%s couldn't connect to WS! Exiting.", self.exchange) 72 | # self.exit() 73 | self.ws.close() 74 | else: 75 | self.logger.info('Connected to %s WS.', self.exchange) 76 | except: 77 | self.logger.error("%s couldn't connect to WS! Exiting.", self.exchange) 78 | self.logger.error(traceback.print_exc()) 79 | 80 | def send_command(self, command): 81 | """ Send a raw command.""" 82 | if self.service_base.init_finish_event.wait(): 83 | self.ws.send(command) 84 | 85 | def __on_message(self, ws, message): 86 | if self.service_base.init_finish_event.wait(): 87 | self.on_message(message, ws) 88 | 89 | def __on_error(self, ws, error): 90 | if self.service_base.init_finish_event.wait(): 91 | self.on_error(error) 92 | 93 | def __on_open(self, ws): 94 | if self.service_base.init_finish_event.wait(): 95 | self.on_open() 96 | 97 | def __on_close(self, ws): 98 | if self.service_base.init_finish_event.wait(): 99 | self.on_close() 100 | -------------------------------------------------------------------------------- /src/websocket_base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 2018/7/15 11:04 3 | # @Author : xnbo 4 | # @Site : 5 | # @File : websocket_base.py 6 | # @Software: PyCharm 7 | 8 | import threading 9 | 10 | class WebSocketBase(object): 11 | def __init__(self, service_base, url=None): 12 | """ Connect to the websocket and initialize data stores .""" 13 | self.exchange = service_base.exchange 14 | self.logger = service_base.logger 15 | 16 | if url: 17 | self.url = url 18 | else: 19 | self.url = service_base.ws_url 20 | self.on_message = service_base.on_message 21 | self.on_close = service_base.on_close 22 | self.on_open = service_base.on_open 23 | self.on_error = service_base.on_error 24 | self.ping_interval = service_base.ping_interval 25 | 26 | self.exited = False 27 | 28 | def start_check_thread(self): 29 | """ 30 | 启动检测线程 31 | :return: 32 | """ 33 | self.check_thread = threading.Thread(target=lambda: self.check_thread_impl()) 34 | self.check_thread.daemon = True 35 | self.check_thread.start() 36 | 37 | def exit(self): 38 | """ 39 | exit websocket, rewritten by derived classes 40 | :param msg: 41 | :return: 42 | """ 43 | 44 | def check_thread_impl(self): 45 | """ 46 | check websocket connect status, rewritten by derived classes 47 | :param msg: 48 | :return: 49 | """ 50 | 51 | def send_command(self, command): 52 | """ 53 | Send a raw command, rewritten by derived classes 54 | :param msg: 55 | :return: 56 | """ 57 | -------------------------------------------------------------------------------- /study/Vdub_FX_SniperVX3.pine: -------------------------------------------------------------------------------- 1 | //@version=2 2 | //╭╮╱╱╭╮╭╮╱╱╭╮ 3 | //┃╰╮╭╯┃┃┃╱╱┃┃ 4 | //╰╮┃┃╭┻╯┣╮╭┫╰━┳╮╭┳━━╮ 5 | //╱┃╰╯┃╭╮┃┃┃┃╭╮┃┃┃┃━━┫ 6 | //╱╰╮╭┫╰╯┃╰╯┃╰╯┃╰╯┣━━┃ 7 | //╱╱╰╯╰━━┻━━┻━━┻━━┻━━╯ 8 | //╭━━━┳╮╱╱╱╱╱╱╱╭╮ 9 | //┃╭━╮┃┃╱╱╱╱╱╱╱┃┃ 10 | //┃┃╱╰┫╰━┳━━┳━╮╭━╮╭━━┫┃ 11 | //┃┃╱╭┫╭╮┃╭╮┃╭╮┫╭╮┫┃━┫┃ 12 | //┃╰━╯┃┃┃┃╭╮┃┃┃┃┃┃┃┃━┫╰╮ 13 | //╰━━━┻╯╰┻╯╰┻╯╰┻╯╰┻━━┻━╯ 14 | //━╯ 15 | // http://www.vdubus.co.uk/ 16 | study(title='Vdub_FX_SniperVX3_Alert', shorttitle='Alert', overlay=true) 17 | 18 | //Candle body resistance Channel-----------------------------// 19 | len = 34 20 | src = input(close, title="Candle body resistance Channel") 21 | out = sma(src, len) 22 | last8h = highest(close, 13) 23 | lastl8 = lowest(close, 13) 24 | bearish = cross(close,out) == 1 and falling(close, 1) 25 | bullish = cross(close,out) == 1 and rising(close, 1) 26 | channel2=input(false, title="Bar Channel On/Off") 27 | //ul2=plot(channel2?last8h:last8h==nz(last8h[1])?last8h:na, color=black, linewidth=1, style=linebr, title="Candle body resistance level top", offset=0) 28 | //ll2=plot(channel2?lastl8:lastl8==nz(lastl8[1])?lastl8:na, color=black, linewidth=1, style=linebr, title="Candle body resistance level bottom", offset=0) 29 | //fill(ul2, ll2, color=black, transp=95, title="Candle body resistance Channel") 30 | 31 | //-----------------Support and Resistance 32 | RST = input(title='Support / Resistance length:', type=integer, defval=10) 33 | RSTT = valuewhen(high >= highest(high, RST), high, 0) 34 | RSTB = valuewhen(low <= lowest(low, RST), low, 0) 35 | //RT2 = plot(RSTT, color=RSTT != RSTT[1] ? na : red, linewidth=1, offset=+0) 36 | //RB2 = plot(RSTB, color=RSTB != RSTB[1] ? na : green, linewidth=1, offset=0) 37 | 38 | //--------------------Trend colour ema------------------------------------------------// 39 | src0 = close, len0 = input(13, minval=1, title="EMA 1") 40 | ema0 = ema(src0, len0) 41 | direction = rising(ema0, 2) ? +1 : falling(ema0, 2) ? -1 : 0 42 | //plot_color = direction > 0 ? lime: direction < 0 ? red : na 43 | //plot(ema0, title="EMA", style=line, linewidth=1, color = plot_color) 44 | 45 | //-------------------- ema 2------------------------------------------------// 46 | src02 = close, len02 = input(21, minval=1, title="EMA 2") 47 | ema02 = ema(src02, len02) 48 | direction2 = rising(ema02, 2) ? +1 : falling(ema02, 2) ? -1 : 0 49 | //plot_color2 = direction2 > 0 ? lime: direction2 < 0 ? red : na 50 | //plot(ema02, title="EMA Signal 2", style=line, linewidth=1, color = plot_color2) 51 | 52 | //=============Hull MA// 53 | show_hma = input(false, title="Display Hull MA Set:") 54 | hma_src = input(close, title="Hull MA's Source:") 55 | hma_base_length = input(8, minval=1, title="Hull MA's Base Length:") 56 | hma_length_scalar = input(5, minval=0, title="Hull MA's Length Scalar:") 57 | hullma(src, length)=>wma(2*wma(src, length/2)-wma(src, length), round(sqrt(length))) 58 | //plot(not show_hma ? na : hullma(hma_src, hma_base_length+hma_length_scalar*6), color=black, linewidth=2, title="Hull MA") 59 | 60 | //============ signal Generator ==================================// 61 | Piriod=input('720') 62 | ch1 = security(tickerid, Piriod, open) 63 | ch2 = security(tickerid, Piriod, close) 64 | longCondition = crossover(security(tickerid, Piriod, close),security(tickerid, Piriod, open)) 65 | plotchar(longCondition, char='Long', color=green) 66 | alertcondition(longCondition, 'long', 'long') 67 | //if (longCondition) 68 | // strategy.entry("BUY", strategy.long) 69 | shortCondition = crossunder(security(tickerid, Piriod, close),security(tickerid, Piriod, open)) 70 | //if (shortCondition) 71 | // strategy.entry("SELL", strategy.short) 72 | plotchar(shortCondition, char='Short', color=red) 73 | alertcondition(shortCondition, 'short', 'short') 74 | 75 | /////////////////////////////////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /study/a_class.py: -------------------------------------------------------------------------------- 1 | class A: 2 | 3 | name = None 4 | age = None 5 | height = None 6 | 7 | def __init__(self): 8 | self.name = 'test' 9 | self.age = 2 10 | 11 | def func(self): 12 | print(self.height) 13 | 14 | def func_2(self): 15 | a = 888 16 | self.to_be_defined(a) 17 | 18 | def to_be_defined(self, a): 19 | pass -------------------------------------------------------------------------------- /study/b_class.py: -------------------------------------------------------------------------------- 1 | from a_class import A 2 | 3 | class B: 4 | 5 | def __init__(self, a): 6 | self.a = a 7 | self.a.to_be_defined = self.lala 8 | def func_3(self, a): 9 | print(self.a.height) 10 | print('i am func 3: ' + str(a)) 11 | 12 | def display(self): 13 | self.a.height = 10 14 | self.a.func() 15 | self.a.func_2() 16 | 17 | def lala(self, a): 18 | print('hahahah: ', str(a)) 19 | 20 | 21 | a = A() 22 | b = B(a) 23 | b.a.func_2() -------------------------------------------------------------------------------- /study/bitmex_auth.py: -------------------------------------------------------------------------------- 1 | 2 | import time 3 | from requests.auth import AuthBase 4 | import hashlib 5 | import hmac 6 | from urllib.parse import urlparse 7 | 8 | 9 | class AccessTokenAuth(AuthBase): 10 | 11 | """Attaches Access Token Authentication to the given Request object.""" 12 | 13 | def __init__(self, accessToken): 14 | """Init with Token.""" 15 | self.token = accessToken 16 | 17 | def __call__(self, r): 18 | """Called when forming a request - generates access token header.""" 19 | if (self.token): 20 | r.headers['access-token'] = self.token 21 | return r 22 | 23 | class APIKeyAuthWithExpires(AuthBase): 24 | 25 | """Attaches API Key Authentication to the given Request object. This implementation uses `expires`.""" 26 | 27 | def __init__(self, apiKey, apiSecret): 28 | """Init with Key & Secret.""" 29 | self.apiKey = apiKey 30 | self.apiSecret = apiSecret 31 | 32 | def __call__(self, r): 33 | """ 34 | Called when forming a request - generates api key headers. This call uses `expires` instead of nonce. 35 | This way it will not collide with other processes using the same API Key if requests arrive out of order. 36 | For more details, see https://www.bitmex.com/app/apiKeys 37 | """ 38 | # modify and return the request 39 | expires = int(round(time.time()) + 5) # 5s grace period in case of clock skew 40 | r.headers['api-expires'] = str(expires) 41 | r.headers['api-key'] = self.apiKey 42 | r.headers['api-signature'] = generate_signature(self.apiSecret, r.method, r.url, expires, r.body or '') 43 | 44 | return r 45 | 46 | class APIKeyAuth(AuthBase): 47 | 48 | """Attaches API Key Authentication to the given Request object.""" 49 | 50 | def __init__(self, apiKey, apiSecret): 51 | """Init with Key & Secret.""" 52 | self.apiKey = apiKey 53 | self.apiSecret = apiSecret 54 | 55 | def __call__(self, r): 56 | """Called when forming a request - generates api key headers.""" 57 | # modify and return the request 58 | nonce = generate_nonce() 59 | r.headers['api-expires'] = str(nonce) 60 | r.headers['api-key'] = self.apiKey 61 | r.headers['api-signature'] = generate_signature(self.apiSecret, r.method, r.url, nonce, r.body or '') 62 | 63 | return r 64 | 65 | 66 | def generate_nonce(): 67 | return int(round(time.time() + 3600)) 68 | 69 | 70 | # Generates an API signature. 71 | # A signature is HMAC_SHA256(secret, verb + path + nonce + data), hex encoded. 72 | # Verb must be uppercased, url is relative, nonce must be an increasing 64-bit integer 73 | # and the data, if present, must be JSON without whitespace between keys. 74 | # 75 | # For example, in psuedocode (and in real code below): 76 | # 77 | # verb=POST 78 | # url=/api/v1/order 79 | # nonce=1416993995705 80 | # data={"symbol":"XBTZ14","quantity":1,"price":395.01} 81 | # signature = HEX(HMAC_SHA256(secret, 'POST/api/v1/order1416993995705{"symbol":"XBTZ14","quantity":1,"price":395.01}')) 82 | def generate_signature(secret, verb, url, nonce, data): 83 | """Generate a request signature compatible with BitMEX.""" 84 | # Parse the url so we can remove the base and extract just the path. 85 | parsedURL = urlparse(url) 86 | path = parsedURL.path 87 | if parsedURL.query: 88 | path = path + '?' + parsedURL.query 89 | 90 | if isinstance(data, (bytes, bytearray)): 91 | data = data.decode('utf8') 92 | 93 | # print "Computing HMAC: %s" % verb + path + str(nonce) + data 94 | message = verb + path + str(nonce) + data 95 | 96 | signature = hmac.new(bytes(secret, 'utf8'), bytes(message, 'utf8'), digestmod=hashlib.sha256).hexdigest() 97 | return signature 98 | -------------------------------------------------------------------------------- /study/bitmex_market_maker.py: -------------------------------------------------------------------------------- 1 | """BitMEX API Connector.""" 2 | from __future__ import absolute_import 3 | import requests 4 | import time 5 | import datetime 6 | import json 7 | import base64 8 | import uuid 9 | import logging 10 | from market_maker.auth import APIKeyAuthWithExpires 11 | from market_maker.utils import constants, errors 12 | from market_maker.ws.ws_thread import BitMEXWebsocket 13 | 14 | 15 | # https://www.bitmex.com/api/explorer/ 16 | class BitMEX(object): 17 | 18 | """BitMEX API Connector.""" 19 | 20 | def __init__(self, base_url=None, symbol=None, apiKey=None, apiSecret=None, 21 | orderIDPrefix='mm_bitmex_', shouldWSAuth=True, postOnly=False, timeout=7): 22 | """Init connector.""" 23 | self.logger = logging.getLogger('root') 24 | self.base_url = base_url 25 | self.symbol = symbol 26 | self.postOnly = postOnly 27 | if (apiKey is None): 28 | raise Exception("Please set an API key and Secret to get started. See " + 29 | "https://github.com/BitMEX/sample-market-maker/#getting-started for more information." 30 | ) 31 | self.apiKey = apiKey 32 | self.apiSecret = apiSecret 33 | if len(orderIDPrefix) > 13: 34 | raise ValueError("settings.ORDERID_PREFIX must be at most 13 characters long!") 35 | self.orderIDPrefix = orderIDPrefix 36 | self.retries = 0 # initialize counter 37 | 38 | # Prepare HTTPS session 39 | self.session = requests.Session() 40 | # These headers are always sent 41 | self.session.headers.update({'user-agent': 'liquidbot-' + constants.VERSION}) 42 | self.session.headers.update({'content-type': 'application/json'}) 43 | self.session.headers.update({'accept': 'application/json'}) 44 | 45 | # Create websocket for streaming data 46 | self.ws = BitMEXWebsocket() 47 | self.ws.connect(base_url, symbol, shouldAuth=shouldWSAuth) 48 | 49 | self.timeout = timeout 50 | 51 | def __del__(self): 52 | self.exit() 53 | 54 | def exit(self): 55 | self.ws.exit() 56 | 57 | # 58 | # Public methods 59 | # 60 | def ticker_data(self, symbol=None): 61 | """Get ticker data.""" 62 | if symbol is None: 63 | symbol = self.symbol 64 | return self.ws.get_ticker(symbol) 65 | 66 | def instrument(self, symbol): 67 | """Get an instrument's details.""" 68 | return self.ws.get_instrument(symbol) 69 | 70 | def instruments(self, filter=None): 71 | query = {} 72 | if filter is not None: 73 | query['filter'] = json.dumps(filter) 74 | return self._curl_bitmex(path='instrument', query=query, verb='GET') 75 | 76 | def market_depth(self, symbol): 77 | """Get market depth / orderbook.""" 78 | return self.ws.market_depth(symbol) 79 | 80 | def recent_trades(self): 81 | """Get recent trades. 82 | Returns 83 | ------- 84 | A list of dicts: 85 | {u'amount': 60, 86 | u'date': 1306775375, 87 | u'price': 8.7401099999999996, 88 | u'tid': u'93842'}, 89 | """ 90 | return self.ws.recent_trades() 91 | 92 | # 93 | # Authentication required methods 94 | # 95 | def authentication_required(fn): 96 | """Annotation for methods that require auth.""" 97 | def wrapped(self, *args, **kwargs): 98 | if not (self.apiKey): 99 | msg = "You must be authenticated to use this method" 100 | raise errors.AuthenticationError(msg) 101 | else: 102 | return fn(self, *args, **kwargs) 103 | return wrapped 104 | 105 | @authentication_required 106 | def funds(self): 107 | """Get your current balance.""" 108 | return self.ws.funds() 109 | 110 | @authentication_required 111 | def position(self, symbol): 112 | """Get your open position.""" 113 | return self.ws.position(symbol) 114 | 115 | @authentication_required 116 | def isolate_margin(self, symbol, leverage, rethrow_errors=False): 117 | """Set the leverage on an isolated margin position""" 118 | path = "position/leverage" 119 | postdict = { 120 | 'symbol': symbol, 121 | 'leverage': leverage 122 | } 123 | return self._curl_bitmex(path=path, postdict=postdict, verb="POST", rethrow_errors=rethrow_errors) 124 | 125 | @authentication_required 126 | def delta(self): 127 | return self.position(self.symbol)['homeNotional'] 128 | 129 | @authentication_required 130 | def buy(self, quantity, price): 131 | """Place a buy order. 132 | Returns order object. ID: orderID 133 | """ 134 | return self.place_order(quantity, price) 135 | 136 | @authentication_required 137 | def sell(self, quantity, price): 138 | """Place a sell order. 139 | Returns order object. ID: orderID 140 | """ 141 | return self.place_order(-quantity, price) 142 | 143 | @authentication_required 144 | def place_order(self, quantity, price): 145 | """Place an order.""" 146 | if price < 0: 147 | raise Exception("Price must be positive.") 148 | 149 | endpoint = "order" 150 | # Generate a unique clOrdID with our prefix so we can identify it. 151 | clOrdID = self.orderIDPrefix + base64.b64encode(uuid.uuid4().bytes).decode('utf8').rstrip('=\n') 152 | postdict = { 153 | 'symbol': self.symbol, 154 | 'orderQty': quantity, 155 | 'price': price, 156 | 'clOrdID': clOrdID 157 | } 158 | return self._curl_bitmex(path=endpoint, postdict=postdict, verb="POST") 159 | 160 | @authentication_required 161 | def amend_bulk_orders(self, orders): 162 | """Amend multiple orders.""" 163 | # Note rethrow; if this fails, we want to catch it and re-tick 164 | return self._curl_bitmex(path='order/bulk', postdict={'orders': orders}, verb='PUT', rethrow_errors=True) 165 | 166 | @authentication_required 167 | def create_bulk_orders(self, orders): 168 | """Create multiple orders.""" 169 | for order in orders: 170 | order['clOrdID'] = self.orderIDPrefix + base64.b64encode(uuid.uuid4().bytes).decode('utf8').rstrip('=\n') 171 | order['symbol'] = self.symbol 172 | if self.postOnly: 173 | order['execInst'] = 'ParticipateDoNotInitiate' 174 | return self._curl_bitmex(path='order/bulk', postdict={'orders': orders}, verb='POST') 175 | 176 | @authentication_required 177 | def open_orders(self): 178 | """Get open orders.""" 179 | return self.ws.open_orders(self.orderIDPrefix) 180 | 181 | @authentication_required 182 | def http_open_orders(self): 183 | """Get open orders via HTTP. Used on close to ensure we catch them all.""" 184 | path = "order" 185 | orders = self._curl_bitmex( 186 | path=path, 187 | query={ 188 | 'filter': json.dumps({'ordStatus.isTerminated': False, 'symbol': self.symbol}), 189 | 'count': 500 190 | }, 191 | verb="GET" 192 | ) 193 | # Only return orders that start with our clOrdID prefix. 194 | return [o for o in orders if str(o['clOrdID']).startswith(self.orderIDPrefix)] 195 | 196 | @authentication_required 197 | def cancel(self, orderID): 198 | """Cancel an existing order.""" 199 | path = "order" 200 | postdict = { 201 | 'orderID': orderID, 202 | } 203 | return self._curl_bitmex(path=path, postdict=postdict, verb="DELETE") 204 | 205 | @authentication_required 206 | def withdraw(self, amount, fee, address): 207 | path = "user/requestWithdrawal" 208 | postdict = { 209 | 'amount': amount, 210 | 'fee': fee, 211 | 'currency': 'XBt', 212 | 'address': address 213 | } 214 | return self._curl_bitmex(path=path, postdict=postdict, verb="POST", max_retries=0) 215 | 216 | def _curl_bitmex(self, path, query=None, postdict=None, timeout=None, verb=None, rethrow_errors=False, 217 | max_retries=None): 218 | """Send a request to BitMEX Servers.""" 219 | # Handle URL 220 | url = self.base_url + path 221 | 222 | if timeout is None: 223 | timeout = self.timeout 224 | 225 | # Default to POST if data is attached, GET otherwise 226 | if not verb: 227 | verb = 'POST' if postdict else 'GET' 228 | 229 | # By default don't retry POST or PUT. Retrying GET/DELETE is okay because they are idempotent. 230 | # In the future we could allow retrying PUT, so long as 'leavesQty' is not used (not idempotent), 231 | # or you could change the clOrdID (set {"clOrdID": "new", "origClOrdID": "old"}) so that an amend 232 | # can't erroneously be applied twice. 233 | if max_retries is None: 234 | max_retries = 0 if verb in ['POST', 'PUT'] else 3 235 | 236 | # Auth: API Key/Secret 237 | auth = APIKeyAuthWithExpires(self.apiKey, self.apiSecret) 238 | 239 | def exit_or_throw(e): 240 | if rethrow_errors: 241 | raise e 242 | else: 243 | exit(1) 244 | 245 | def retry(): 246 | self.retries += 1 247 | if self.retries > max_retries: 248 | raise Exception("Max retries on %s (%s) hit, raising." % (path, json.dumps(postdict or ''))) 249 | return self._curl_bitmex(path, query, postdict, timeout, verb, rethrow_errors, max_retries) 250 | 251 | # Make the request 252 | response = None 253 | try: 254 | self.logger.info("sending req to %s: %s" % (url, json.dumps(postdict or query or ''))) 255 | req = requests.Request(verb, url, json=postdict, auth=auth, params=query) 256 | prepped = self.session.prepare_request(req) 257 | response = self.session.send(prepped, timeout=timeout) 258 | # Make non-200s throw 259 | response.raise_for_status() 260 | 261 | except requests.exceptions.HTTPError as e: 262 | if response is None: 263 | raise e 264 | 265 | # 401 - Auth error. This is fatal. 266 | if response.status_code == 401: 267 | self.logger.error("API Key or Secret incorrect, please check and restart.") 268 | self.logger.error("Error: " + response.text) 269 | if postdict: 270 | self.logger.error(postdict) 271 | # Always exit, even if rethrow_errors, because this is fatal 272 | exit(1) 273 | 274 | # 404, can be thrown if order canceled or does not exist. 275 | elif response.status_code == 404: 276 | if verb == 'DELETE': 277 | self.logger.error("Order not found: %s" % postdict['orderID']) 278 | return 279 | self.logger.error("Unable to contact the BitMEX API (404). " + 280 | "Request: %s \n %s" % (url, json.dumps(postdict))) 281 | exit_or_throw(e) 282 | 283 | # 429, ratelimit; cancel orders & wait until X-RateLimit-Reset 284 | elif response.status_code == 429: 285 | self.logger.error("Ratelimited on current request. Sleeping, then trying again. Try fewer " + 286 | "order pairs or contact support@bitmex.com to raise your limits. " + 287 | "Request: %s \n %s" % (url, json.dumps(postdict))) 288 | 289 | # Figure out how long we need to wait. 290 | ratelimit_reset = response.headers['X-RateLimit-Reset'] 291 | to_sleep = int(ratelimit_reset) - int(time.time()) 292 | reset_str = datetime.datetime.fromtimestamp(int(ratelimit_reset)).strftime('%X') 293 | 294 | # We're ratelimited, and we may be waiting for a long time. Cancel orders. 295 | self.logger.warning("Canceling all known orders in the meantime.") 296 | self.cancel([o['orderID'] for o in self.open_orders()]) 297 | 298 | self.logger.error("Your ratelimit will reset at %s. Sleeping for %d seconds." % (reset_str, to_sleep)) 299 | time.sleep(to_sleep) 300 | 301 | # Retry the request. 302 | return retry() 303 | 304 | # 503 - BitMEX temporary downtime, likely due to a deploy. Try again 305 | elif response.status_code == 503: 306 | self.logger.warning("Unable to contact the BitMEX API (503), retrying. " + 307 | "Request: %s \n %s" % (url, json.dumps(postdict))) 308 | time.sleep(3) 309 | return retry() 310 | 311 | elif response.status_code == 400: 312 | error = response.json()['error'] 313 | message = error['message'].lower() if error else '' 314 | 315 | # Duplicate clOrdID: that's fine, probably a deploy, go get the order(s) and return it 316 | if 'duplicate clordid' in message: 317 | orders = postdict['orders'] if 'orders' in postdict else postdict 318 | 319 | IDs = json.dumps({'clOrdID': [order['clOrdID'] for order in orders]}) 320 | orderResults = self._curl_bitmex('/order', query={'filter': IDs}, verb='GET') 321 | 322 | for i, order in enumerate(orderResults): 323 | if ( 324 | order['orderQty'] != abs(postdict['orderQty']) or 325 | order['side'] != ('Buy' if postdict['orderQty'] > 0 else 'Sell') or 326 | order['price'] != postdict['price'] or 327 | order['symbol'] != postdict['symbol']): 328 | raise Exception('Attempted to recover from duplicate clOrdID, but order returned from API ' + 329 | 'did not match POST.\nPOST data: %s\nReturned order: %s' % ( 330 | json.dumps(orders[i]), json.dumps(order))) 331 | # All good 332 | return orderResults 333 | 334 | elif 'insufficient available balance' in message: 335 | self.logger.error('Account out of funds. The message: %s' % error['message']) 336 | exit_or_throw(Exception('Insufficient Funds')) 337 | 338 | 339 | # If we haven't returned or re-raised yet, we get here. 340 | self.logger.error("Unhandled Error: %s: %s" % (e, response.text)) 341 | self.logger.error("Endpoint was: %s %s: %s" % (verb, path, json.dumps(postdict))) 342 | exit_or_throw(e) 343 | 344 | except requests.exceptions.Timeout as e: 345 | # Timeout, re-run this request 346 | self.logger.warning("Timed out on request: %s (%s), retrying..." % (path, json.dumps(postdict or ''))) 347 | return retry() 348 | 349 | except requests.exceptions.ConnectionError as e: 350 | self.logger.warning("Unable to contact the BitMEX API (%s). Please check the URL. Retrying. " + 351 | "Request: %s %s \n %s" % (e, url, json.dumps(postdict))) 352 | time.sleep(1) 353 | return retry() 354 | 355 | # Reset retry counter on success 356 | self.retries = 0 357 | 358 | return response.json() -------------------------------------------------------------------------------- /study/bitmex_ws.py: -------------------------------------------------------------------------------- 1 | import websocket 2 | import threading 3 | import traceback 4 | from time import sleep 5 | import json 6 | import logging 7 | import urllib 8 | import math 9 | from bitmex_auth import generate_nonce, generate_signature 10 | import utils as u 11 | import settings 12 | 13 | # Naive implementation of connecting to BitMEX websocket for streaming realtime data. 14 | # The Marketmaker still interacts with this as if it were a REST Endpoint, but now it can get 15 | # much more realtime data without polling the hell out of the API. 16 | # 17 | # The Websocket offers a bunch of data as raw properties right on the object. 18 | # On connect, it synchronously asks for a push of all this data then returns. 19 | # Right after, the MM can start using its data. It will be updated in realtime, so the MM can 20 | # poll really often if it wants. 21 | class BitMEXWebsocket: 22 | ''' 23 | Connect to the websocket and initialize data stores. 24 | endpoint: "https://testnet.bitmex.com/api/v1" or "https://www.bitmex.com/api/v1" 25 | symbol: contract type. only can be one of them, like XBTUSD, XBTM19, ETHUSD 26 | sub_topic: subscription topics. can be many of them, type is list if many otherwise string is OK. like ["execution", "instrument", 27 | "order", "orderBookL2", "position", "quote", "trade", "margin"] 28 | The class is designed for market maker purpose. 29 | ''' 30 | 31 | # Don't grow a table larger than this amount. Helps cap memory usage. 32 | MAX_TABLE_LEN = 200 33 | 34 | def __init__(self, endpoint=settings.BASE_URL, symbol=settings.SYMBOL, sub_topic=settings.SUB_TOPICS, api_key=settings.API_KEY, api_secret=settings.API_SECRET): 35 | self.logger = u.setup_custom_logger(__name__) 36 | self.logger.debug("Initializing WebSocket.") 37 | 38 | self.endpoint = endpoint 39 | self.symbol = symbol 40 | self.sub_topic = [sub_topic] if not isinstance(sub_topic, list) else sub_topic 41 | 42 | if api_key is not None and api_secret is None: 43 | raise ValueError('api_secret is required if api_key is provided') 44 | if api_key is None and api_secret is not None: 45 | raise ValueError('api_key is required if api_secret is provided') 46 | 47 | self.api_key = api_key 48 | self.api_secret = api_secret 49 | 50 | self.data = {} 51 | self.keys = {} 52 | self.exited = False 53 | 54 | # We can subscribe right in the connection querystring, so let's build that. 55 | # Subscribe to all pertinent endpoints 56 | wsURL = self.__get_url() 57 | self.logger.info("Connecting to %s" % wsURL) 58 | self.__connect(wsURL, symbol) 59 | self.logger.info('Connected to WS.') 60 | 61 | # # Connected. Wait for partials 62 | # self.__wait_for_symbol(symbol) 63 | # if api_key: 64 | # self.__wait_for_account() 65 | # self.logger.info('Got all market data. Starting.') 66 | 67 | def exit(self): 68 | '''Call this to exit - will close websocket.''' 69 | self.exited = True 70 | self.ws.close() 71 | 72 | def get_instrument(self): 73 | '''Get the raw instrument data for this symbol.''' 74 | # Turn the 'tickSize' into 'tickLog' for use in rounding 75 | instrument = self.data['instrument'][0] 76 | instrument['tickLog'] = int(math.fabs(math.log10(instrument['tickSize']))) 77 | return instrument 78 | 79 | def get_ticker(self): 80 | ''' 81 | Return a ticker object. Generated from quote and trade. 82 | The ticker is not candle data, but equals to the close price of candle data 83 | ''' 84 | lastQuote = self.data['quote'][-1] 85 | lastTrade = self.data['trade'][-1] 86 | ticker = { 87 | "last": lastTrade['price'], 88 | "buy": lastQuote['bidPrice'], 89 | "sell": lastQuote['askPrice'], 90 | "mid": (float(lastQuote['bidPrice'] or 0) + float(lastQuote['askPrice'] or 0)) / 2 91 | } 92 | 93 | # The instrument has a tickSize. Use it to round values. 94 | instrument = self.data['instrument'][0] 95 | return {k: round(float(v or 0), instrument['tickLog']) for k, v in ticker.items()} 96 | 97 | def funds(self): 98 | '''Get your margin details.''' 99 | return self.data['margin'][0] 100 | 101 | def positions(self): 102 | '''Get your positions.''' 103 | return self.data['position'] 104 | 105 | def market_depth(self): 106 | '''Get market depth (orderbook). Returns all levels.''' 107 | return self.data['orderBookL2'] 108 | 109 | def open_orders(self, clOrdIDPrefix): 110 | '''Get all your open orders.''' 111 | orders = self.data['order'] 112 | # Filter to only open orders and those that we actually placed 113 | return [o for o in orders if str(o['clOrdID']).startswith(clOrdIDPrefix) and order_leaves_quantity(o)] 114 | 115 | def recent_trades(self): 116 | '''Get recent trades.''' 117 | return self.data['trade'] 118 | 119 | # 120 | # End Public Methods 121 | # 122 | 123 | def __connect(self, wsURL, symbol): 124 | '''Connect to the websocket in a thread.''' 125 | self.logger.debug("Starting thread") 126 | 127 | self.ws = websocket.WebSocketApp(wsURL, 128 | on_message=self.__on_message, 129 | on_close=self.__on_close, 130 | on_open=self.__on_open, 131 | on_error=self.__on_error, 132 | header=self.__get_auth()) 133 | 134 | self.wst = threading.Thread(target=lambda: self.ws.run_forever()) 135 | self.wst.daemon = True 136 | self.wst.start() 137 | self.logger.debug("Started thread") 138 | 139 | # Wait for conn_timeout before continuing, throw a error if still cannot establish a connection after timeout 140 | conn_timeout = 10 141 | while not self.ws.sock or not self.ws.sock.connected and conn_timeout: 142 | sleep(1) 143 | conn_timeout -= 1 144 | if not conn_timeout: 145 | self.logger.error("Couldn't connect to WS! Exiting.") 146 | self.exit() 147 | raise websocket.WebSocketTimeoutException('Couldn\'t connect to WS! Exiting.') 148 | 149 | def __get_auth(self): 150 | '''Return auth headers. Will use API Keys if present in settings.''' 151 | if self.api_key: 152 | self.logger.info("Authenticating with API Key.") 153 | # To auth to the WS using an API key, we generate a signature of a nonce and 154 | # the WS API endpoint. 155 | expires = generate_nonce() 156 | return [ 157 | "api-expires: " + str(expires), 158 | "api-signature: " + generate_signature(self.api_secret, 'GET', '/realtime', expires, ''), 159 | "api-key:" + self.api_key 160 | ] 161 | else: 162 | self.logger.info("Not authenticating.") 163 | return [] 164 | 165 | def __get_url(self): 166 | ''' 167 | Generate a connection URL. We can define subscriptions right in the querystring. 168 | Most subscription topics are scoped by the symbol we're listening to. 169 | ''' 170 | 171 | # You can sub to orderBookL2 for all levels, or orderBook10 for top 10 levels & save bandwidth 172 | # symbolSubs = ["execution", "instrument", "order", "orderBookL2", "position", "quote", "trade"] 173 | # genericSubs = ["margin"] 174 | 175 | subscriptions = [sub + ':' + self.symbol for sub in self.sub_topic] 176 | # subscriptions += genericSubs 177 | 178 | urlParts = list(urllib.parse.urlparse(self.endpoint)) 179 | urlParts[0] = urlParts[0].replace('http', 'ws') 180 | urlParts[2] = "/realtime?subscribe={}".format(','.join(subscriptions)) 181 | return urllib.parse.urlunparse(urlParts) 182 | 183 | def __wait_for_account(self): 184 | '''On subscribe, this data will come down. Wait for it.''' 185 | # Wait for the keys to show up from the ws 186 | while not {'margin', 'position', 'order', 'orderBookL2'} <= set(self.data): 187 | sleep(0.1) 188 | 189 | def __wait_for_symbol(self, symbol): 190 | '''On subscribe, this data will come down. Wait for it.''' 191 | while not {'instrument', 'trade', 'quote'} <= set(self.data): 192 | sleep(0.1) 193 | 194 | def __send_command(self, command, args=None): 195 | '''Send a raw command.''' 196 | if args is None: 197 | args = [] 198 | self.ws.send(json.dumps({"op": command, "args": args})) 199 | 200 | def __on_message(self, message): 201 | '''Handler for parsing WS messages.''' 202 | message = json.loads(message) 203 | self.logger.debug(json.dumps(message)) 204 | 205 | table = message['table'] if 'table' in message else None 206 | action = message['action'] if 'action' in message else None 207 | try: 208 | if 'subscribe' in message: 209 | self.logger.debug("Subscribed to %s." % message['subscribe']) 210 | elif action: 211 | 212 | if table not in self.data: 213 | self.data[table] = [] 214 | 215 | # There are four possible actions from the WS: 216 | # 'partial' - full table image 217 | # 'insert' - new row 218 | # 'update' - update row 219 | # 'delete' - delete row 220 | if action == 'partial': 221 | self.logger.debug("%s: partial" % table) 222 | self.data[table] += message['data'] 223 | # Keys are communicated on partials to let you know how to uniquely identify 224 | # an item. We use it for updates. 225 | self.keys[table] = message['keys'] 226 | elif action == 'insert': 227 | self.logger.debug('%s: inserting %s' % (table, message['data'])) 228 | self.data[table] += message['data'] 229 | 230 | # Limit the max length of the table to avoid excessive memory usage. 231 | # Don't trim orders because we'll lose valuable state if we do. 232 | # Write data in loop, remove old data if size is too big. The size currently is 200 lines 233 | if table not in ['order', 'orderBookL2'] and len(self.data[table]) > BitMEXWebsocket.MAX_TABLE_LEN: 234 | self.data[table] = self.data[table][int(BitMEXWebsocket.MAX_TABLE_LEN / 2):] 235 | 236 | elif action == 'update': 237 | self.logger.debug('%s: updating %s' % (table, message['data'])) 238 | # Locate the item in the collection and update it. 239 | for updateData in message['data']: 240 | item = findItemByKeys(self.keys[table], self.data[table], updateData) 241 | if not item: 242 | return # No item found to update. Could happen before push 243 | item.update(updateData) 244 | # Remove cancelled / filled orders 245 | if table == 'order' and not order_leaves_quantity(item): 246 | self.data[table].remove(item) 247 | elif action == 'delete': 248 | self.logger.debug('%s: deleting %s' % (table, message['data'])) 249 | # Locate the item in the collection and remove it. 250 | for deleteData in message['data']: 251 | item = findItemByKeys(self.keys[table], self.data[table], deleteData) 252 | self.data[table].remove(item) 253 | else: 254 | raise Exception("Unknown action: %s" % action) 255 | except: 256 | self.logger.error(traceback.format_exc()) 257 | 258 | def __on_error(self, error): 259 | '''Called on fatal websocket errors. We exit on these.''' 260 | if not self.exited: 261 | self.logger.error("Error : %s" % error) 262 | raise websocket.WebSocketException(error) 263 | 264 | def __on_open(self): 265 | '''Called when the WS opens.''' 266 | self.logger.debug("Websocket Opened.") 267 | 268 | def __on_close(self): 269 | '''Called on websocket close.''' 270 | self.logger.info('Websocket Closed') 271 | 272 | 273 | # Utility method for finding an item in the store. 274 | # When an update comes through on the websocket, we need to figure out which item in the array it is 275 | # in order to match that item. 276 | # 277 | # Helpfully, on a data push (or on an HTTP hit to /api/v1/schema), we have a "keys" array. These are the 278 | # fields we can use to uniquely identify an item. Sometimes there is more than one, so we iterate through all 279 | # provided keys. 280 | def findItemByKeys(keys, table, matchData): 281 | for item in table: 282 | matched = True 283 | for key in keys: 284 | if item[key] != matchData[key]: 285 | matched = False 286 | if matched: 287 | return item 288 | 289 | 290 | def order_leaves_quantity(o): 291 | if o['leavesQty'] is None: 292 | return True 293 | return o['leavesQty'] > 0 294 | 295 | 296 | if __name__ == '__main__': 297 | # Basic use of websocket. 298 | 299 | logger = u.setup_custom_logger('console') 300 | 301 | symbolSubs = ["execution", "instrument", "order", "orderBookL2", "position", "quote", "trade", "margin"] 302 | 303 | # Instantiating the WS will make it connect. Be sure to add your api_key/api_secret. 304 | ws = BitMEXWebsocket(endpoint="https://testnet.bitmex.com/api/v1", symbol="XBTUSD", sub_topic=symbolSubs) 305 | 306 | logger.info("Instrument data: %s" % ws.get_instrument()) 307 | 308 | # Run forever 309 | while(ws.ws.sock.connected): 310 | logger.info("Ticker: %s" % ws.get_ticker()) 311 | if ws.api_key: 312 | logger.info("Funds: %s" % ws.funds()) 313 | logger.info("Market Depth: %s" % ws.market_depth()) 314 | logger.info("Recent Trades: %s\n\n" % ws.recent_trades()) 315 | sleep(10) -------------------------------------------------------------------------------- /study/coinflex_rest.py: -------------------------------------------------------------------------------- 1 | '''Restful API request to coinflex 2 | Return coinflex exchange data requested thru RESTFUL api 3 | 4 | Author: Tang Wei 5 | Created: Jan 21, 2022 6 | ''' 7 | import os 8 | import base64 9 | import hmac 10 | import hashlib 11 | import datetime 12 | import requests 13 | from urllib.parse import urlencode 14 | from dotenv import load_dotenv 15 | from utils import print_error, current_milli_ts 16 | 17 | load_dotenv() 18 | 19 | TERM_RED = '\033[1;31m' 20 | TERM_NFMT = '\033[0;0m' 21 | TERM_BLUE = '\033[1;34m' 22 | TERM_GREEN = '\033[1;32m' 23 | 24 | HOST= 'https://v2api.coinflex.com' 25 | PATH= 'v2api.coinflex.com' 26 | api_key = os.getenv('APIKEY') 27 | api_secret = os.getenv('APISECRET') 28 | nonce = 888888 29 | 30 | def private_call(method, options={}, action='GET'): 31 | ''' 32 | generate header based on api credential 33 | method: private call method 34 | options: parameters if have,, the format is as below 35 | {'key1': 'value1', 'key2': 'value2'} 36 | ''' 37 | ts = datetime.datetime.utcnow().isoformat() 38 | body = urlencode(options) 39 | if options: 40 | path = method + '?' + body 41 | else: 42 | path = method 43 | msg_string = '{}\n{}\n{}\n{}\n{}\n{}'.format(ts, nonce, action, PATH, method, body) 44 | sig = base64.b64encode(hmac.new(api_secret.encode('utf-8'), msg_string.encode('utf-8'), hashlib.sha256).digest()).decode('utf-8') 45 | 46 | header = {'Content-Type': 'application/json', 'AccessKey': api_key, 47 | 'Timestamp': ts, 'Signature': sig, 'Nonce': str(nonce)} 48 | 49 | if action == 'GET': 50 | resp = requests.get(HOST + path, headers=header) 51 | elif action == 'POST': 52 | resp = requests.post(HOST + path, headers=header) 53 | print(HOST + path) 54 | return resp.json() 55 | 56 | def isAlive() -> bool: 57 | 'public GET /v2/ping' 58 | try: 59 | endpoint = '/v2/ping' 60 | response = requests.get(HOST + endpoint) 61 | data = response.json() 62 | return data['success'] 63 | except Exception as err: 64 | print_error('isAlive', err) 65 | 66 | def getOrderBook(market, depth): 67 | 'get order books of specific trading market with specific depth' 68 | try: 69 | endpoint = f'/v2/depth/{market}/{depth}' 70 | response = requests.get(HOST + endpoint) 71 | return response.json() 72 | except Exception as err: 73 | print_error('getOrderBook', err) 74 | 75 | def getBalance(): 76 | ''' 77 | get account balance 78 | ''' 79 | try: 80 | endpoint = '/v2/balances' 81 | return(private_call(endpoint)) 82 | except Exception as err: 83 | print_error('getBalance', err) 84 | 85 | def getBalanceBySymbol(symbol): 86 | ''' 87 | get account balance by specific symbol 88 | ''' 89 | try: 90 | endpoint = f'/v2/balances/{symbol}' 91 | return(private_call(endpoint)) 92 | except Exception as err: 93 | print_error('getBalanceBySymbol', err) 94 | 95 | def getPositions(): 96 | ''' 97 | get account positions 98 | ''' 99 | try: 100 | endpoint = '/v2/positions' 101 | return(private_call(endpoint)) 102 | except Exception as err: 103 | print_error('getPositions', err) 104 | 105 | def getPositionsBySymbol(symbol): 106 | ''' 107 | get account position by specific symbol 108 | ''' 109 | try: 110 | endpoint = f'/v2/positions/{symbol}' 111 | return(private_call(endpoint)) 112 | except Exception as err: 113 | print_error('getPositionsBySymbol', err) 114 | 115 | def getOrders(): 116 | ''' 117 | get account's unfilled orders 118 | ''' 119 | try: 120 | endpoint = '/v2/orders' 121 | return(private_call(endpoint)) 122 | except Exception as err: 123 | print_error('getOrders', err) 124 | 125 | def getOrdersByMarket(market): 126 | ''' 127 | get account all orders in specific market 128 | ''' 129 | try: 130 | endpoint = '/v2.1/orders' 131 | return(private_call(endpoint, { 132 | 'marketCode': market 133 | })) 134 | except Exception as err: 135 | print_error('getOrdersByMarket', err) 136 | 137 | # def placeLimitOrder(market, side, quantity, price): 138 | # ''' 139 | # place a order with options 140 | # ''' 141 | # try: 142 | # endpoint = '/v2/orders/place' 143 | # return(private_call(endpoint, { 144 | # 'responseType': 'FULL', 145 | # 'orders': [ 146 | # { 147 | # 'clientOrderId': str(current_milli_ts()), 148 | # 'marketCode': market, 149 | # 'side': side, 150 | # 'quantity': quantity, 151 | # 'orderType': 'LIMIT', 152 | # 'price': price 153 | # } 154 | # ] 155 | # }, 'POST')) 156 | # except Exception as err: 157 | # print_error('placeLimitOrder', err) 158 | 159 | 160 | if __name__ == '__main__': 161 | 162 | # print(placeLimitOrder('FLEX-USD', 'BUY', '1', '4.5')) 163 | 164 | print(f'{TERM_BLUE}1. public /v2/ping{TERM_NFMT}') 165 | print(isAlive()) 166 | 167 | print(f'{TERM_BLUE}2. public /v2/depth/FLEX-USD/10{TERM_NFMT}') 168 | print(getOrderBook('FLEX-USD', 10)) 169 | 170 | print(f'{TERM_BLUE}3. private /v2/balances{TERM_NFMT}') 171 | print(getBalance()) 172 | 173 | print(f'{TERM_BLUE}4. private /v2/balances/USD{TERM_NFMT}') 174 | print(getBalanceBySymbol('USD')) 175 | 176 | print(f'{TERM_BLUE}5. private /v2/positions{TERM_NFMT}') 177 | print(getPositions()) 178 | 179 | print(f'{TERM_BLUE}6. private /v2/positions/ETH{TERM_NFMT}') 180 | print(getPositionsBySymbol('ETH-USD-SWAP-LIN')) 181 | 182 | print(f'{TERM_BLUE}7. private /v2/orders{TERM_NFMT}') 183 | print(getOrders()) 184 | 185 | print(f'{TERM_BLUE}8. private /v2.1/orders?marketCode=FLEX-USD{TERM_NFMT}') 186 | print(getOrdersByMarket('FLEX-USD')) 187 | -------------------------------------------------------------------------------- /study/coinflex_ws.py: -------------------------------------------------------------------------------- 1 | import websockets 2 | import asyncio 3 | import hmac 4 | import base64 5 | import hashlib 6 | import json 7 | import sys 8 | import math 9 | from datetime import datetime 10 | from utils import current_milli_ts, TERM_BLUE, TERM_NFMT, TERM_GREEN, TERM_RED 11 | 12 | from global_utils import * 13 | 14 | config_path = sys.argv[-1] 15 | config = get_json_config(config_path, "config") 16 | 17 | 18 | api_key = config['APIKEY'] 19 | api_secret = config['APISECRET'] 20 | user_id = config['USERID'] 21 | 22 | sell_price = float(config['SELLPRICE']) 23 | buy_price = float(config['BUYPRICE']) 24 | depth_amount = float(config['DEPTHAMOUNT']) 25 | 26 | websocket_endpoint = 'wss://v2api.coinflex.com/v2/websocket' 27 | 28 | logger = setup_logger(__file__) 29 | 30 | def auth_msg(): 31 | ts = current_milli_ts() 32 | sig_payload = (ts + 'GET/auth/self/verify').encode('utf-8') 33 | signature = base64.b64encode(hmac.new(api_secret.encode('utf-8'), sig_payload, hashlib.sha256).digest()).decode('utf-8') 34 | return { 35 | 'op': 'login', 36 | 'tag': 1, 37 | 'data': { 38 | 'apiKey': api_key, 39 | 'timestamp': ts, 40 | 'signature': signature 41 | } 42 | } 43 | 44 | def subscribe_balance_msg(): 45 | return { 46 | 'op': 'subscribe', 47 | 'args': ['balance:all'], 48 | 'tag': 101 49 | } 50 | 51 | def subscribe_orders_msg(market): 52 | return { 53 | 'op': 'subscribe', 54 | 'args': [f'order:{market}'], 55 | 'tag': 102 56 | } 57 | 58 | def subscribe_ticker_msg(market): 59 | return { 60 | 'op': 'subscribe', 61 | 'tag': 1, 62 | 'args': [f'ticker:{market}'] 63 | } 64 | 65 | def subscribe_depth_msg(market): 66 | return { 67 | "op": "subscribe", 68 | "tag": 103, 69 | "args": [f"depth:{market}"] 70 | } 71 | 72 | def place_limit_order_msg(market, side, quantity, price): 73 | return { 74 | 'op': 'placeorder', 75 | 'tag': 123, 76 | 'data': { 77 | 'timestamp': current_milli_ts(), 78 | 'clientOrderId': 1, 79 | 'marketCode': market, 80 | 'side': side, 81 | 'orderType': 'LIMIT', 82 | 'quantity': quantity, 83 | 'price': price 84 | } 85 | } 86 | 87 | def get_best_price(depth_data, diff): 88 | best_price = None 89 | accumulated_amount = 0 90 | for i in range(len(depth_data)): 91 | accumulated_amount += depth_data[i][1] 92 | if accumulated_amount > diff: 93 | if i-1 >=0: 94 | best_price = depth_data[i-1][0] 95 | break 96 | 97 | return best_price 98 | 99 | async def initial_conn(ws): 100 | await ws.send(json.dumps(auth_msg())) 101 | await ws.send(json.dumps(subscribe_orders_msg('FLEX-USD'))) 102 | await ws.send((json.dumps(subscribe_depth_msg('FLEX-USD')))) 103 | # await ws.send(json.dumps(place_limit_order_msg('FLEX-USD', 'BUY', 1, 4.5))) 104 | 105 | async def process(): 106 | async with websockets.connect(websocket_endpoint) as ws: 107 | global buy_price 108 | global sell_price 109 | await initial_conn(ws) 110 | while ws.open: 111 | resp = await ws.recv() 112 | msg = json.loads(resp) 113 | # print(msg) 114 | if 'event' in msg and msg['event']=='login': 115 | ts = msg['timestamp'] 116 | logger.info(f'{TERM_GREEN}Login succeed{TERM_NFMT}') 117 | if 'table' in msg and msg['table']=='depth': 118 | depth_data = msg['data'][0] 119 | new_buy_price = get_best_price(depth_data['bids'], depth_amount) 120 | new_sell_price = get_best_price(depth_data['asks'], depth_amount) 121 | if (new_buy_price != None and new_buy_price != buy_price): 122 | logger.info(f'{TERM_GREEN}Update buy_price: {buy_price} => {new_buy_price}, {sell_price} => {new_sell_price}{TERM_NFMT}') 123 | buy_price = new_buy_price 124 | if (new_sell_price != None and new_sell_price != sell_price): 125 | logger.info(f'{TERM_GREEN}Update sell_price: {buy_price} => {new_buy_price}, {sell_price} => {new_sell_price}{TERM_NFMT}') 126 | sell_price = new_sell_price 127 | if 'table' in msg and msg['table']=='order': 128 | data = msg['data'][0] 129 | logger.info(f'{TERM_BLUE}{data}{TERM_NFMT}') 130 | if 'notice' in data and data['notice'] == 'OrderMatched': 131 | side = data['side'] 132 | quantity = float(data['matchQuantity']) 133 | price = float(data['price']) 134 | if side == 'BUY': 135 | # 买单成交了,要挂卖单 136 | logger.info(f'{TERM_GREEN}Execute sell order: {quantity} - {sell_price}{TERM_NFMT}') 137 | await ws.send(json.dumps(place_limit_order_msg('FLEX-USD', 'SELL', quantity, sell_price))) 138 | elif side == 'SELL': 139 | # 卖单成交了,要挂买单 140 | logger.info(f'{TERM_GREEN}Execute bull order: {math.floor(quantity * price / buy_price * 10) / 10} - {buy_price}{TERM_NFMT}') 141 | await ws.send(json.dumps(place_limit_order_msg('FLEX-USD', 'BUY', math.floor(quantity * price / buy_price * 10) / 10, buy_price))) 142 | 143 | 144 | if __name__ == '__main__': 145 | logger.info(f'{TERM_GREEN}Config loaded, user: {user_id}, buy_price: {buy_price}, sell_price: {sell_price}{TERM_NFMT}') 146 | 147 | while True: 148 | try: 149 | loop = asyncio.get_event_loop() 150 | loop.run_until_complete(process()) 151 | loop.close() 152 | except Exception as err: 153 | logger.error(f"{TERM_RED}Caught exception: {err}{TERM_NFMT}") 154 | -------------------------------------------------------------------------------- /study/data.py: -------------------------------------------------------------------------------- 1 | ''' 2 | it communicates with Bitmex exchange thru restfull api and websocket, fetch all necessary origin info. 3 | ''' 4 | 5 | import hashlib 6 | import hmac 7 | import json 8 | import time 9 | import traceback 10 | import urllib 11 | import ssl 12 | import sys 13 | import websocket 14 | import threading 15 | import pandas as pd 16 | from datetime import datetime, timezone, timedelta 17 | import bitmex 18 | import src.settings as s 19 | import src.utils as u 20 | 21 | logger = u.get_logger(__name__) 22 | 23 | def generate_nonce(): 24 | return int(round(time.time() * 1000)) 25 | 26 | def generate_signature(secret, verb, url, nonce, data): 27 | """Generate a request signature compatible with BitMEX.""" 28 | # Parse the url so we can remove the base and extract just the path. 29 | parsedURL = urllib.parse.urlparse(url) 30 | path = parsedURL.path 31 | if parsedURL.query: 32 | path = path + '?' + parsedURL.query 33 | 34 | # print "Computing HMAC: %s" % verb + path + str(nonce) + data 35 | message = (verb + path + str(nonce) + data).encode('utf-8') 36 | 37 | signature = hmac.new(secret.encode('utf-8'), message, digestmod=hashlib.sha256).hexdigest() 38 | return signature 39 | 40 | class Data: 41 | 42 | is_running = True 43 | # below values will be updated by bitmex ws 44 | excess_margin = None 45 | wallet_balance = None 46 | market_price = None 47 | current_qty = None 48 | avg_engry_price = None 49 | data = None 50 | ohlcv_len = None 51 | bin_size = None 52 | 53 | def __init__(self): 54 | self.ohlcv_len = 100 55 | self.bin_size = s.BIN_SIZE 56 | self.testnet = s.TEST 57 | if self.testnet: 58 | domain = 'testnet.bitmex.com' 59 | else: 60 | domain = 'www.bitmex.com' 61 | self.endpoint = 'wss://' + domain + f'/realtime?subscribe=instrument:{s.SYMBOL},' \ 62 | f'margin,position:{s.SYMBOL},tradeBin1m:{s.SYMBOL}' 63 | # bitmex restful api client 64 | self.client = bitmex.bitmex(test=self.testnet, api_key=s.API_KEY, api_secret=s.API_SECRET) 65 | # bitmex websocket api 66 | self.__wsconnect() 67 | 68 | def __wsconnect(self): 69 | ssl_defaults = ssl.get_default_verify_paths() 70 | sslopt_ca_certs = {'ca_certs': ssl_defaults.cafile} 71 | self.ws = websocket.WebSocketApp(self.endpoint, 72 | on_message=self.__on_message, 73 | on_error=self.__on_error, 74 | on_close=self.__on_close, 75 | on_open=self.__on_open, 76 | header=self.__get_auth()) 77 | self.wst = threading.Thread(target=lambda: self.ws.run_forever(sslopt=sslopt_ca_certs)) 78 | self.wst.daemon = True 79 | self.wst.start() 80 | 81 | # Wait for connect before continuing 82 | conn_timeout = 5 83 | while (not self.ws.sock or not self.ws.sock.connected) and conn_timeout: 84 | time.sleep(1) 85 | conn_timeout -= 1 86 | 87 | def get_margin(self): 88 | ''' 89 | account balance summary 90 | 当前的账户余额相关信息 91 | ''' 92 | return u.retry(lambda: self.client.User.User_getMargin(currency="XBt").result()) 93 | 94 | def get_excess_margin(self): 95 | ''' 96 | get excess margin 97 | ''' 98 | if self.excess_margin: 99 | return self.excess_margin 100 | else: 101 | return self.get_margin()['excessMargin'] 102 | 103 | def get_wallet_balance(self): 104 | ''' 105 | get account balance 106 | ''' 107 | if self.wallet_balance: 108 | return self.wallet_balance 109 | return self.get_margin()['walletBalance'] 110 | 111 | def get_position(self): 112 | ''' 113 | current order position including open and close position, return None if there is no position 114 | 当前的仓位,如果没有的话,返回None 115 | ''' 116 | ret = u.retry(lambda: self.client.Position.Position_get(filter=json.dumps({"symbol": s.SYMBOL})).result()) 117 | if ret: return ret[0] 118 | else: return None 119 | 120 | def get_current_qty(self): 121 | ''' 122 | get currentQty of position, can be positive and nagative 123 | ''' 124 | if self.current_qty: 125 | return self.current_qty 126 | else: 127 | ret = self.get_position() 128 | if ret: 129 | return ret['currentQty'] 130 | else: return 0 131 | 132 | def get_avg_entry_price(self): 133 | ''' 134 | get avgEntryPrice 135 | ''' 136 | if self.avg_engry_price: 137 | return self.avg_engry_price 138 | else: 139 | ret = self.get_position() 140 | if ret: 141 | return ret['avgEntryPrice'] 142 | else: return 0 143 | 144 | def get_market_price(self): 145 | ''' 146 | current close price for settings symbol 147 | 当前设置的交易对收盘价格 148 | ''' 149 | if self.market_price: 150 | return self.market_price 151 | else: 152 | return u.retry(lambda: self.client.Instrument.Instrument_get(symbol=s.SYMBOL).result())[0]["lastPrice"] 153 | 154 | def set_leverage(self, leverage): 155 | ''' 156 | set leverage to position 157 | ''' 158 | return u.retry(lambda: self.client.Position.Position_updateLeverage(symbol=s.SYMBOL, leverage=leverage).result()) 159 | 160 | def fetch_ohlcv(self, start_time, end_time): 161 | ''' 162 | get data for open-high-low-close-volumn array 163 | 获取open-high-low-close-volumn数组数据 164 | ''' 165 | left_time = start_time 166 | right_time = end_time 167 | data = u.to_data_frame([]) 168 | 169 | while True: 170 | source = u.retry(lambda: self.client.Trade.Trade_getBucketed(symbol=s.SYMBOL, binSize="1m", 171 | startTime=left_time, endTime=right_time, count=500, partial=False).result()) 172 | if len(source) == 0: 173 | break 174 | 175 | source = u.to_data_frame(source) 176 | data = pd.concat([data, source]) 177 | 178 | if right_time > source.iloc[-1].name + timedelta(minutes=1): 179 | left_time = source.iloc[-1].name + timedelta(minutes=1) 180 | time.sleep(1) 181 | else: 182 | break 183 | return data 184 | 185 | def portfolio(self, ohlcv): 186 | ''' 187 | implemented by portfolio class 188 | ''' 189 | pass 190 | 191 | def update_balance(self): 192 | ''' 193 | implemented by portfolio class 194 | ''' 195 | 196 | def update_ohlcv(self, update): 197 | # initial data from RESTAPI 198 | if self.data is None: 199 | end_time = datetime.now(timezone.utc) 200 | start_time = end_time - timedelta(minutes=self.ohlcv_len * s.INTERVAL[self.bin_size][0]) 201 | self.data = self.fetch_ohlcv(start_time, end_time) 202 | # update data from WS 203 | else: 204 | self.data = pd.concat([self.data, update])[1:] 205 | logger.debug(self.data[-1:]) 206 | re_sample_data = u.resample(self.data, self.bin_size) 207 | if self.data.iloc[-1].name == re_sample_data.iloc[-1].name + timedelta(minutes=s.INTERVAL[self.bin_size][0]): 208 | logger.debug(re_sample_data[-1:]) 209 | self.portfolio(re_sample_data) 210 | self.update_balance() 211 | 212 | def order(self, orderQty, stop=0): 213 | ''' 214 | This is 'Market' order 215 | 'buy' if orderQty is positive 216 | 'sell' if orderQty is nagative 217 | ''' 218 | clOrdID = 'Daxiang_' + u.random_str() 219 | side = 'Buy' if orderQty>0 else 'Sell' 220 | if stop == 0: 221 | # market order 222 | orderType = 'Market' 223 | u.retry(lambda: self.client.Order.Order_new(symbol=s.SYMBOL, ordType=orderType, clOrdID=clOrdID, 224 | side=side, orderQty=orderQty).result()) 225 | u.logging_order(id=clOrdID, type=orderType, side=side, 226 | qty=orderQty, price=self.get_market_price()) 227 | else: 228 | # stop order 229 | orderType = 'Stop' 230 | u.retry(lambda: self.client.Order.Order_new(symbol=s.SYMBOL, ordType=orderType, clOrdID=clOrdID, 231 | side=side, orderQty=orderQty, stopPx=stop).result()) 232 | u.logging_order(id=clOrdID, type=orderType, side=side, 233 | qty=orderQty, stop=stop) 234 | 235 | def buy(self, orderQty): 236 | self.order(orderQty) 237 | 238 | def sell(self, orderQty): 239 | self.order(-orderQty) 240 | 241 | def stop_buy(self, orderQty, stop): 242 | self.order(orderQty, stop) 243 | 244 | def stop_sell(self, orderQty, stop): 245 | self.order(-orderQty, stop) 246 | 247 | def get_open_orders(self): 248 | """ 249 | fetch my all open orders 250 | """ 251 | open_orders = u.retry(lambda: self.client 252 | .Order.Order_getOrders(filter=json.dumps({"symbol": s.SYMBOL, "open": True})) 253 | .result()) 254 | open_orders = [o for o in open_orders if o["clOrdID"].startswith('Daxiang')] 255 | if len(open_orders) > 0: 256 | return open_orders 257 | else: 258 | return None 259 | 260 | def cancel_all(self): 261 | """ 262 | cancel all orders, including stop orders 263 | """ 264 | orders = u.retry(lambda: self.client.Order.Order_cancelAll().result()) 265 | for order in orders: 266 | logger.info(f"Cancel Order : (orderID, orderType, side, orderQty, limit, stop) = " 267 | f"({order['clOrdID']}, {order['ordType']}, {order['side']}, {order['orderQty']}, " 268 | f"{order['price']}, {order['stopPx']})") 269 | logger.info(f"Cancel All Order") 270 | 271 | def __get_auth(self): 272 | api_key = s.API_KEY 273 | api_secret = s.API_SECRET 274 | nonce = generate_nonce() 275 | return [ 276 | "api-nonce: " + str(nonce), 277 | "api-signature: " + generate_signature(api_secret, 'GET', '/realtime', nonce, ''), 278 | "api-key:" + api_key 279 | ] 280 | 281 | def __on_error(self, ws, message): 282 | logger.error(message) 283 | logger.error(traceback.format_exc()) 284 | 285 | def __on_message(self, ws, message): 286 | try: 287 | obj = json.loads(message) 288 | if 'table' in obj: 289 | if len(obj['data']) <= 0: 290 | return 291 | table = obj['table'] 292 | data = obj['data'] 293 | 294 | if table.startswith("trade"): 295 | data[0]['timestamp'] = datetime.strptime(data[0]['timestamp'][:-5], '%Y-%m-%dT%H:%M:%S') 296 | self.update_ohlcv(u.to_data_frame(data)) 297 | 298 | if table.startswith("instrument"): 299 | if 'lastPrice' in data[0]: 300 | self.market_price = data[0]['lastPrice'] 301 | 302 | elif table.startswith("margin"): 303 | if 'excessMargin' in data[0]: 304 | self.excess_margin = data[0]['excessMargin'] 305 | if 'walletBalance' in data[0]: 306 | self.wallet_balance = data[0]['walletBalance'] 307 | 308 | elif table.startswith("position"): 309 | if 'currentQty' in data[0]: 310 | self.current_qty = data[0]['currentQty'] 311 | if 'avgEntryPrice' in data[0]: 312 | self.avg_engry_price = data[0]['avgEntryPrice'] 313 | 314 | except Exception as e: 315 | logger.error(e) 316 | logger.error(traceback.format_exc()) 317 | 318 | def __on_close(self, ws): 319 | if self.is_running: 320 | logger.info("Websocket restart") 321 | self.__wsconnect() 322 | 323 | def __on_open(self, ws): 324 | logger.info('bitmex websocket opened') 325 | 326 | def close(self): 327 | logger.info('bitmex websocket closed') 328 | self.is_running = False 329 | self.ws.close() -------------------------------------------------------------------------------- /study/huobi/huobi_api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 2018/8/7 10:56 3 | # @Author : zxlong 4 | # @Site : 5 | # @File : huobi_api.py 6 | # @Software: PyCharm 7 | 8 | 9 | from exchange.rest_service import RestService 10 | import base64 11 | import datetime 12 | import hashlib 13 | import hmac 14 | import urllib 15 | import urllib.parse 16 | import urllib.request 17 | 18 | GET_ACCOUNTS_PATH = "/v1/account/accounts" 19 | GET_BALANCE_PATH = "/v1/account/accounts/{0}/balance" 20 | SEND_ORDER_PATH = "/v1/order/orders/place" 21 | CANCEL_ORDER_PATH = "/v1/order/orders/{0}/submitcancel" 22 | ORDER_INFO_PATH = "/v1/order/orders/{0}" 23 | 24 | 25 | class HuobiApi(RestService): 26 | 27 | def __init__(self, market_url, trade_url, api_key=None, api_secret=None, logger=None): 28 | self.logger = logger 29 | # 行情相关接口的url 暂未使用 30 | self.market_url = market_url 31 | # 交易相关接口的url 32 | self.trade_url = trade_url 33 | self.api_key = api_key 34 | self.api_secret = api_secret 35 | # {'id': 2214685, 'type': 'spot', 'subtype': '', 'state': 'working'}, {'id': 3299764, 'type': 'otc', 'subtype': '', 'state': 'working'}, {'id': 3950464, 'type': 'point', 'subtype': '', 'state': 'working'} 36 | accounts = self.get_accounts() 37 | if accounts is not None: 38 | self.account_id = accounts['data'][0]['id'] 39 | else: 40 | self.account_id = None 41 | 42 | def get_accounts(self): 43 | """ 44 | 获取account_id 45 | :return: 46 | """ 47 | request_path = GET_ACCOUNTS_PATH 48 | params = {} 49 | return self.__get(params, request_path) 50 | 51 | def get_balance(self, acct_id=None): 52 | """ 53 | 获取当前账户资产 54 | :param acct_id 55 | :return: 56 | """ 57 | 58 | if not acct_id: 59 | # accounts = self.get_accounts() 60 | # acct_id = accounts['data'][0]['id']; 61 | acct_id = self.account_id 62 | 63 | request_path = GET_BALANCE_PATH.format(acct_id) 64 | params = {"account-id": acct_id} 65 | return self.__get(params, request_path) 66 | 67 | def send_order(self, amount, source, symbol, _type, price=0): 68 | """ 69 | 创建并执行订单(下单接口) 70 | :param account_id: 71 | :param amount: 72 | :param source: api如果使用借贷资产交易,请在下单接口,请求参数source中填写'margin-api' 73 | :param symbol: 74 | :param _type: 可选值 {buy-market:市价买, sell-market:市价卖, buy-limit:限价买, sell-limit:限价卖} 75 | :param price: 76 | :return: 77 | """ 78 | params = {"account-id": self.account_id, 79 | "amount": amount, 80 | "symbol": symbol, 81 | "type": _type, 82 | "source": source} 83 | if price: 84 | params["price"] = price 85 | 86 | request_path = SEND_ORDER_PATH 87 | return self.__post(params, request_path) 88 | 89 | def cancel_order(self, order_id): 90 | """ 91 | 撤单操作,针对单个订单 92 | :param order_id: 93 | :return: 94 | """ 95 | params = {} 96 | request_path = CANCEL_ORDER_PATH.format(order_id) 97 | return self.__post(params, request_path) 98 | 99 | def order_info(self, order_id): 100 | """ 101 | 查询某个订单 102 | :param order_id: 103 | :return: 104 | """ 105 | params = {} 106 | request_path = ORDER_INFO_PATH.format(order_id) 107 | return self.__get(params, request_path) 108 | 109 | def __get(self, params, request_path, need_auth=True): 110 | """ 111 | 需要鉴权的get请求 112 | :param params: 113 | :param request_path: 114 | :param need_auth: 115 | :return: 116 | """ 117 | method = 'GET' 118 | host_url = self.trade_url 119 | # 需要鉴权的请求 120 | if need_auth: 121 | timestamp = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S') 122 | params.update({'AccessKeyId': self.api_key, 123 | 'SignatureMethod': 'HmacSHA256', 124 | 'SignatureVersion': '2', 125 | 'Timestamp': timestamp}) 126 | host_name = urllib.parse.urlparse(host_url).hostname 127 | host_name = host_name.lower() 128 | params['Signature'] = self.__create_sign(params, method, host_name, request_path) 129 | 130 | # url = host_url + request_path 131 | return self.handle_request(host_url, request_path, verb=method, query=params, logger=self.logger) 132 | 133 | # return self.http_get_request(url, params) 134 | 135 | def __post(self, params, request_path, need_auth=True): 136 | """ 137 | 需要鉴权的post请求 138 | :param params: 139 | :param request_path: 140 | :param need_auth: 141 | :return: 142 | """ 143 | method = 'POST' 144 | headers = {'Accept': 'application/json', 'Content-Type': 'application/json'} 145 | host_url = self.trade_url 146 | # 需要鉴权的请求 147 | if need_auth: 148 | timestamp = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S') 149 | params_to_sign = {'AccessKeyId': self.api_key, 150 | 'SignatureMethod': 'HmacSHA256', 151 | 'SignatureVersion': '2', 152 | 'Timestamp': timestamp} 153 | host_name = urllib.parse.urlparse(host_url).hostname 154 | host_name = host_name.lower() 155 | params_to_sign['Signature'] = self.__create_sign(params_to_sign, method, host_name, request_path) 156 | 157 | # url = host_url + request_path + '?' + urllib.parse.urlencode(params_to_sign) 158 | # huobi的签名机制,跟签名相关的数据,都需要编码到URI中 159 | request_path = request_path + '?' + urllib.parse.urlencode(params_to_sign) 160 | return self.handle_request(host_url, request_path, verb=method, headers=headers, json=params, logger=self.logger) 161 | 162 | # return self.http_post_request(url, params) 163 | 164 | def __create_sign(self, params, method, host_url, request_path): 165 | """ 166 | 签名算法 167 | :param params: 168 | :param method: 169 | :param host_url: 170 | :param request_path: 171 | :return: 172 | """ 173 | sorted_params = sorted(params.items(), key=lambda d: d[0], reverse=False) 174 | encode_params = urllib.parse.urlencode(sorted_params) 175 | payload = [method, host_url, request_path, encode_params] 176 | payload = '\n'.join(payload) 177 | payload = payload.encode(encoding='UTF8') 178 | secret_key = self.api_secret.encode(encoding='UTF8') 179 | 180 | digest = hmac.new(secret_key, payload, digestmod=hashlib.sha256).digest() 181 | signature = base64.b64encode(digest) 182 | signature = signature.decode() 183 | return signature 184 | -------------------------------------------------------------------------------- /study/huobi/huobi_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 2018/8/07 16:21 3 | # @Author : zxlong 4 | # @Site : 5 | # @File : huobi_utils.py 6 | # @Software: PyCharm 7 | 8 | from common.constant import * 9 | 10 | 11 | class HuobiUtils: 12 | # 交易所返回的订单状态 13 | HUOBI_STATE_PRE_SUBMITTED = 'pre-submitted' # 准备提交 14 | HUOBI_STATE_SUBMITTING = 'submitting' # 提交中 15 | HUOBI_STATE_SUBMITTED = 'submitted' # 已提交 16 | HUOBI_STATE_PARTIAL_FILLED = 'partial-filled' # 部分成交 17 | HUOBI_STATE_PARTIAL_CANCELED = 'partial-canceled' # 部分成交撤销 18 | HUOBI_STATE_FILLED = 'filled' # 完全成交 19 | HUOBI_STATE_CANCELED = 'canceled' # 已撤销 20 | 21 | '''balance type in huobi''' 22 | HUOBI_BALANCE_TYPE_TRADE = 'trade' 23 | HUOBI_BALANCE_TYPE_FROZEN = 'frozen' 24 | 25 | """huobi下单接口调用状态""" 26 | API_RSP_STATUS_OK = 'ok' 27 | 28 | PRICE_TYPE_IOC = 'ioc' 29 | 30 | @staticmethod 31 | def generate_sub_id(service_type, sub_id=0): 32 | """ 33 | 生成订阅channel的ID 34 | :param service_type: trade or market 35 | :param sub_id: 自然数,建议是顺序增长 36 | :return: 37 | """ 38 | return service_type + '%s' % sub_id 39 | 40 | @staticmethod 41 | def convert_order_state(order_state): 42 | """ 43 | 订单状态转换 44 | :param order_state: 45 | :return: 46 | """ 47 | int_state = ORDER_STATE_UNKNOWN 48 | if order_state == HuobiUtils.HUOBI_STATE_PRE_SUBMITTED: 49 | int_state = ORDER_STATE_PRE_SUBMITTED 50 | elif order_state == HuobiUtils.HUOBI_STATE_SUBMITTING: 51 | int_state = ORDER_STATE_SUBMITTING 52 | elif order_state == HuobiUtils.HUOBI_STATE_SUBMITTED: 53 | int_state = ORDER_STATE_SUBMITTED 54 | elif order_state == HuobiUtils.HUOBI_STATE_PARTIAL_FILLED: 55 | int_state = ORDER_STATE_PARTIAL_FILLED 56 | elif order_state == HuobiUtils.HUOBI_STATE_PARTIAL_CANCELED: 57 | int_state = ORDER_STATE_PARTIAL_CANCELED 58 | elif order_state == HuobiUtils.HUOBI_STATE_FILLED: 59 | int_state = ORDER_STATE_FILLED 60 | elif order_state == HuobiUtils.HUOBI_STATE_CANCELED: 61 | int_state = ORDER_STATE_CANCELED 62 | 63 | return int_state 64 | 65 | @staticmethod 66 | def is_market(trade_type): 67 | if trade_type: 68 | return trade_type.find(SERVICE_MARKET) >= 0 69 | 70 | @staticmethod 71 | def is_limit(trade_type): 72 | if trade_type: 73 | return trade_type.find(LIMIT) >= 0 74 | 75 | @staticmethod 76 | def is_ioc(trade_type): 77 | if trade_type: 78 | return trade_type.find(HuobiUtils.PRICE_TYPE_IOC) >= 0 79 | 80 | @staticmethod 81 | def is_sell(trade_type): 82 | if trade_type: 83 | return trade_type.find(SELL) >= 0 84 | 85 | @staticmethod 86 | def is_buy(trade_type): 87 | if trade_type: 88 | return trade_type.find(BUY) >= 0 89 | 90 | @staticmethod 91 | def get_direction(trade_type): 92 | """" 93 | 获取订单的买卖方向,根据trade_type中是否包含"buy"或"sell"进行判断 94 | """ 95 | if HuobiUtils.is_buy(trade_type): 96 | return BUY 97 | elif HuobiUtils.is_sell(trade_type): 98 | return SELL 99 | else: 100 | return '' 101 | 102 | @staticmethod 103 | def get_price_type(trade_type): 104 | """" 105 | 获取订单的价格类型,根据trade_type中是否包含"limit"或"market"或"ioc"进行判断 106 | """ 107 | if HuobiUtils.is_market(trade_type): 108 | return SERVICE_MARKET 109 | elif HuobiUtils.is_limit(trade_type): 110 | return LIMIT 111 | elif HuobiUtils.is_ioc(trade_type): 112 | return HuobiUtils.PRICE_TYPE_IOC 113 | else: 114 | return '' 115 | @staticmethod 116 | def convert_error_code(rsp={}): 117 | # error_code = int(rsp.get("err-code", 100000)) 118 | error_msg = rsp.get("err-msg", "") 119 | if "base-symbol-error" in error_msg: # 交易对不存在 120 | code = ERROR_CODE_SYMBOL_NO_EXISTS 121 | msg = ERROR_MSG_SYMBOL_NO_EXISTS 122 | elif "base-date-error" in error_msg: # 错误的日期格式 123 | code = ERROR_CODE_REQUEST_PARAM_ERROR 124 | msg = ERROR_MSG_REQUEST_PARAM_ERROR 125 | elif "account-transfer-balance-insufficient-error" in error_msg: # 余额不足无法冻结 126 | code = ERROR_CODE_BALANCE_NOT_ENOUGH 127 | msg = ERROR_MSG_BALANCE_NOT_ENOUGH 128 | elif "bad-argument" in error_msg: # 无效参数 129 | code = ERROR_CODE_ORDER_PARAM_ERROR 130 | msg = ERROR_MSG_ORDER_PARAM_ERROR 131 | elif "api-signature-not-valid" in error_msg: # API签名错误 132 | code = ERROR_CODE_SIGN_AUTH_INVALID 133 | msg = ERROR_MSG_SIGN_AUTH_INVALID 134 | elif "gateway-internal-error" in error_msg: # 系统繁忙,请稍后再试 135 | code = ERROR_CODE_EXCHANGE_SYSTEM_ERROR 136 | msg = ERROR_MSG_EXCHANGE_SYSTEM_ERROR 137 | elif "security-require-assets-password" in error_msg: # 需要输入资金密码 138 | code = ERROR_CODE_REQUEST_PARAM_ERROR 139 | msg = ERROR_MSG_REQUEST_PARAM_ERROR 140 | elif "audit-failed" in error_msg: # 下单失败 141 | code = ERROR_CODE_PLACE_ORDER_ERROR 142 | msg = ERROR_MSG_PLACE_ORDER_ERROR 143 | elif "order-accountbalance-error" in error_msg: # 账户余额不足 144 | code = ERROR_CODE_BALANCE_NOT_ENOUGH 145 | msg = ERROR_MSG_BALANCE_NOT_ENOUGH 146 | elif "order-limitorder-price-error" in error_msg: # 限价单下单价格超出限制 147 | code = ERROR_CODE_ORDER_PARAM_ERROR 148 | msg = ERROR_MSG_ORDER_PARAM_ERROR 149 | elif "order-limitorder-amount-error" in error_msg: # 限价单下单数量超出限制 150 | code = ERROR_CODE_ORDER_PARAM_ERROR 151 | msg = ERROR_MSG_ORDER_PARAM_ERROR 152 | elif "order-orderprice-precision-error" in error_msg: # 下单价格超出精度限制 153 | code = ERROR_CODE_OUT_OF_PRECISION_LIMIT 154 | msg = ERROR_MSG_OUT_OF_PRECISION_LIMIT 155 | elif "order-orderamount-precision-error" in error_msg: 156 | code = ERROR_CODE_SYMBOL_NO_EXISTS 157 | msg = ERROR_MSG_SYMBOL_NO_EXISTS 158 | elif "order-queryorder-invalid" in error_msg: # 查询不到此条订单 159 | code = ERROR_CODE_ORDER_NOT_EXIST 160 | msg = ERROR_MSG_ORDER_NOT_EXIST 161 | elif "order-orderstate-error" in error_msg: # 订单状态错误 162 | code = ERROR_CODE_ORDER_STATUS_ERROR 163 | msg = ERROR_MSG_ORDER_STATUS_ERROR 164 | elif "order-datelimit-error" in error_msg: # 查询超出时间限制 165 | code = ERROR_CODE_TIMESTAMP_AHEAD_OF_SERVER 166 | msg = ERROR_MSG_TIMESTAMP_AHEAD_OF_SERVER 167 | elif "order-update-error" in error_msg: # 订单更新出错 168 | code = ERROR_CODE_ORDER_STATUS_ERROR 169 | msg = ERROR_MSG_ORDER_STATUS_ERROR 170 | else: 171 | code = ERROR_CODE_UNKNOW_ERROR 172 | msg = ERROR_MSG_UNKNOW_ERROR 173 | 174 | return code, msg 175 | 176 | 177 | -------------------------------------------------------------------------------- /study/huobi/market_service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 2018/8/2 10:56 3 | # @Author : xnbo 4 | # @Site : 5 | # @File : market_service.py 6 | # @Software: PyCharm 7 | from os import path 8 | import sys 9 | root_dir = path.dirname(path.dirname(path.dirname(__file__))) 10 | common_dir = path.join(root_dir, "common") 11 | exchange_dir = path.join(root_dir, "exchange") 12 | sys.path.append(root_dir) 13 | sys.path.append(common_dir) 14 | sys.path.append(exchange_dir) 15 | 16 | from exchange.service_base import ServiceBase 17 | from common.constant import * 18 | from exchange.websocket_app import MyWebSocketApp 19 | import json 20 | import gzip 21 | import re 22 | import traceback 23 | from os import path 24 | from common.global_utils import * 25 | from exchange.huobi.huobi_utils import HuobiUtils as huobi_utils 26 | from exchange.huobi.tick_data import TickData 27 | 28 | 29 | class MarketService(ServiceBase): 30 | 31 | ID_COUNT = 0 32 | DEFAULT_MARKET_STEP = '1' 33 | 34 | def __init__(self): 35 | self.con_file = path.join(path.split(path.realpath(__file__))[0], "exchange.json") 36 | super(MarketService, self).__init__(EXCHANGE_HUOBI, SERVICE_MARKET, self.con_file) 37 | url = get_json_config(file=self.con_file, section=self.exchange, key="ws_url") 38 | self.url = url 39 | self.webscoket_app = MyWebSocketApp(self) 40 | self.channel_map_symbol = {} 41 | 42 | self.tick_data = {} 43 | 44 | market_step = get_json_config(file=self.con_file, section=self.exchange, key="market_step") 45 | self.market_step = MarketService.DEFAULT_MARKET_STEP 46 | if market_step: 47 | self.market_step = market_step 48 | 49 | self.init_finish_event.set() 50 | json_msg = {MSG_TYPE: MSG_INIT_EXCHANGE_SERVICE, EXCHANGE: self.exchange, 51 | SERVICE_TYPE: self.service_type, 52 | TIMESTAMP: round(time.time())} 53 | self.broadcast(json_msg) 54 | self.check_market_thread.start() 55 | 56 | def on_message(self, msg, ws=None): 57 | """ 58 | 处理websocket的market数据 59 | :param msg: 60 | :return: 61 | """ 62 | try: 63 | # self.logger.info("ON_MESSAGE={}".format(msg)) 64 | msg = gzip.decompress(msg).decode('utf-8') 65 | msg = json.loads(msg) 66 | if 'ping' in msg: 67 | pong = {"pong": msg['ping']} 68 | self.webscoket_app.send_command(json.dumps(pong)) 69 | return 70 | 71 | channel = msg['ch'] if 'ch' in msg else '' 72 | if re.search(r'market.(.*).kline', channel, re.I): 73 | self.tick_data[self.channel_map_symbol[channel]].put_tick_data(msg) 74 | elif re.search(r'market.(.*).depth', channel, re.I): 75 | self.tick_data[self.channel_map_symbol[channel]].put_depth_data(msg) 76 | except: 77 | self.logger.error("on_message error! %s " % msg) 78 | self.logger.error(traceback.format_exc()) 79 | 80 | def subscribe(self, msg): 81 | """ 82 | 订阅行情 83 | :param msg: json格式消息 84 | :return: 85 | """ 86 | try: 87 | self.logger.info("subscribe method is called, params is: %s" % msg) 88 | symbol_list = msg.get('symbol_list', []) 89 | symbol_type_list = msg.get('symbol_type_list', []) 90 | self.__subscribe_symbol_list(symbol_list, symbol_type_list) 91 | except: 92 | self.logger.error("subscribe Error!!! %s" % msg) 93 | self.logger.error(traceback.format_exc()) 94 | 95 | def __subscribe_symbol_list(self, symbol_list, symbol_type_list): 96 | """ 97 | 订阅指定symbol的行情 98 | :param symbol_list: 99 | :return: 100 | """ 101 | for index, symbol in enumerate(symbol_list): 102 | if symbol not in self.subscribe_symbol_list: 103 | # 没有订阅过的symbol,需要初始化TickData信息 104 | # 如果该symbol的tick对象存在,则不再创建,避免反复重连时的内存泄漏风险 105 | symbol_type = symbol_type_list[index] 106 | if symbol not in self.tick_data: 107 | # 如果需要存储动态盈亏数据,则注册on_depth_update 108 | self.tick_data[symbol] = TickData(symbol, self.exchange, market_broadcast=self.broadcast, on_depth_update=self.on_depth_update, symbol_type=symbol_type) 109 | self.logger.info("subscribe market data for symbol [%s]." % symbol) 110 | self.subscribe_symbol_list.append(symbol) 111 | self.subscribe_symbol_type_list.append(symbol_type) 112 | 113 | # 订阅 KLine 数据 114 | MarketService.ID_COUNT += 1 115 | msg = {"sub": "market." + symbol + ".kline.1min", 116 | "id": huobi_utils.generate_sub_id(SERVICE_MARKET, MarketService.ID_COUNT)} 117 | self.webscoket_app.send_command(json.dumps(msg)) 118 | self.channel_map_symbol[msg['sub']] = symbol 119 | # 订阅 Market Depth 数据 120 | # 目前不支持自定义行情深度,但合并深度行情分step0到step5六种,step0返回150档行情,其他返回20档 121 | # 通过对比,step5相对step0有明显的精度损失,step1相对没有step0明显差别,故通过step1订阅深度行情 122 | MarketService.ID_COUNT += 1 123 | msg = {"sub": "market." + symbol + ".depth.step" + self.market_step, 124 | "id": huobi_utils.generate_sub_id(SERVICE_MARKET, MarketService.ID_COUNT)} 125 | self.webscoket_app.send_command(json.dumps(msg)) 126 | self.channel_map_symbol[msg['sub']] = symbol 127 | 128 | self.logger.info("finished subscribe data, subscribeList is %s." % self.subscribe_symbol_list) 129 | return ','.join(self.subscribe_symbol_list) 130 | 131 | if __name__ == '__main__': 132 | service = MarketService() 133 | service.exchange_msg_handle_thread.join() 134 | -------------------------------------------------------------------------------- /study/huobi/tick_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 2018/7/13 11:17 3 | # @Author : xnbo 4 | # @Site : 5 | # @File : tick_data.py 6 | # @Software: PyCharm 7 | 8 | import time 9 | from common.constant import * 10 | from exchange.tick_data_base import TickDataBase 11 | 12 | 13 | class TickData(TickDataBase): 14 | def __init__(self, symbol, exchange, market_broadcast, on_depth_update=None, symbol_type=SPOT): 15 | super(TickData, self).__init__(symbol, exchange, market_broadcast, on_depth_update, symbol_type) 16 | 17 | def _update_tick_data(self, tick_data): 18 | if 'tick' in tick_data: 19 | self.high = tick_data.get('tick', {}).get('high', 0) 20 | self.last = tick_data.get('tick', {}).get('close', 0) 21 | self.low = tick_data.get('tick', {}).get('low', 0) 22 | self.volume = tick_data.get('tick', {}).get('amount', 0) 23 | self.open = tick_data.get('tick', {}).get('open', 0) 24 | self.timestamp = tick_data.get('ts', round(time.time() * 1000)) 25 | 26 | def _update_depth_data(self, depth_data): 27 | if 'tick' in depth_data: 28 | self.local_time = depth_data[LOCAL_TIME] 29 | self.asks = depth_data.get('tick', {}).get('asks', []) 30 | self.bids = depth_data.get('tick', {}).get('bids', []) 31 | self.timestamp = depth_data.get('ts', round(time.time() * 1000)) 32 | -------------------------------------------------------------------------------- /study/huobi/trade_service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 2018/8/2 10:52 3 | # @Author : xnbo 4 | # @Site : 5 | # @File : trade_service.py 6 | # @Software: PyCharm 7 | from os import path 8 | import sys 9 | root_dir = path.dirname(path.dirname(path.dirname(__file__))) 10 | common_dir = path.join(root_dir, "common") 11 | exchange_dir = path.join(root_dir, "exchange") 12 | sys.path.append(root_dir) 13 | sys.path.append(common_dir) 14 | sys.path.append(exchange_dir) 15 | 16 | from exchange.service_base import ServiceBase 17 | from exchange.websocket_app import MyWebSocketApp 18 | from common.constant import * 19 | import json 20 | import gzip 21 | import re 22 | import traceback 23 | import common.global_utils as global_utils 24 | from exchange.huobi.huobi_utils import HuobiUtils as huobi_utils 25 | import copy 26 | import time 27 | from exchange.huobi.huobi_api import HuobiApi 28 | from os import path 29 | from common.global_utils import * 30 | 31 | 32 | class TradeService(ServiceBase): 33 | ID_COUNT = 0 34 | 35 | def __init__(self): 36 | self.con_file = path.join(path.split(path.realpath(__file__))[0], "exchange.json") 37 | super(TradeService, self).__init__(EXCHANGE_HUOBI, SERVICE_TRADE, self.con_file) 38 | self.channel_map_symbol = {} 39 | 40 | # rest接口初始化 41 | self.huobi_api = HuobiApi(None, self.trade_url, self.api_key, self.secret_key, logger=self.logger) 42 | 43 | # 初始化资金信息 44 | # if self.huobi_api: 45 | # msg = {MSG_TYPE: MSG_QRY_BALANCE, EXCHANGE: EXCHANGE_HUOBI} 46 | # self.self_queue.put(msg) 47 | self.init_finish_event.set() 48 | json_msg = {MSG_TYPE: MSG_INIT_EXCHANGE_SERVICE, EXCHANGE: self.exchange, 49 | SERVICE_TYPE: self.service_type, 50 | TIMESTAMP: round(time.time())} 51 | self.broadcast(json_msg) 52 | # 通知父类通用的内部查询线程开始查询 53 | self.qry_balance_event.set() 54 | 55 | def on_message(self, msg, ws=None): 56 | """ 57 | 处理websocket的trade数据 58 | :param msg: 59 | :return: 60 | """ 61 | try: 62 | msg = gzip.decompress(msg).decode('utf-8') 63 | msg = json.loads(msg) 64 | if 'ping' in msg: 65 | pong = {"pong": msg['ping']} 66 | ws.send(json.dumps(pong)) 67 | return 68 | 69 | channel = msg['ch'] if 'ch' in msg else '' 70 | if re.search(r'market.(.*).trade.detail', channel, re.I): 71 | # broadcast中,针对symbol,price,direction判断是否需要主动查询order信息 72 | self.logger.debug('Recieved trade detail data:%s' % msg) 73 | if 'tick' in msg: 74 | data_list = msg['tick']['data'] if 'data' in msg['tick'] else [] 75 | for json_trade_data in data_list: 76 | json_temp_trade_data = {} 77 | json_temp_trade_data[SYMBOL] = self.channel_map_symbol[channel] 78 | json_temp_trade_data[PRICE] = json_trade_data[PRICE] 79 | json_temp_trade_data[DIRECTION] = json_trade_data[DIRECTION] 80 | json_temp_trade_data[MSG_TYPE] = MSG_TRADE_STATUS 81 | # 丢到交易所数据处理队列中处理,在队列处理之后,调用broadcast广播到上层 82 | self.exchange_queue.put(json_temp_trade_data) 83 | except: 84 | self.logger.error("on_message error! %s " % msg) 85 | self.logger.error(traceback.format_exc()) 86 | 87 | def subscribe(self, json_msg): 88 | """ 89 | 订阅交易数据 90 | :param json_msg: json格式消息 91 | :return: 92 | """ 93 | try: 94 | # 暂时屏蔽huobi订阅接口 95 | pass 96 | # self.logger.info("subscribe method is called, params is: %s" % json_msg) 97 | # symbol_list = json_msg.get(SYMBOL_LIST, []) 98 | # symbol_type_list = json_msg.get(SYMBOL_TYPE_LIST, []) 99 | # self.__subscribe_symbol_list(symbol_list, symbol_type_list) 100 | except: 101 | self.logger.error("subscribe Error!!! %s" % json_msg) 102 | self.logger.error(traceback.format_exc()) 103 | 104 | def __subscribe_symbol_list(self, symbol_list, symbol_type_list): 105 | """ 106 | 订阅指定symbol的交易数据 107 | :param symbol_list: 108 | :return: 109 | """ 110 | for index, symbol in enumerate(symbol_list): 111 | if symbol not in self.subscribe_symbol_list: 112 | self.logger.info("subscribe trade data for symbol [%s]." % symbol) 113 | self.subscribe_symbol_list.append(symbol) 114 | 115 | if symbol not in self.subscribe_symbol_type_list: 116 | self.subscribe_symbol_type_list.append(symbol_type_list[index]) 117 | # 订阅 Trade Detail 数据 118 | TradeService.ID_COUNT += 1 119 | msg = {"sub": "market." + symbol + ".trade.detail", 120 | "id": huobi_utils.generate_sub_id(SERVICE_TRADE, TradeService.ID_COUNT)} 121 | # self.webscoket_app.send_command(json.dumps(msg)) 122 | self.channel_map_symbol[msg['sub']] = symbol 123 | self.logger.info("finished subscribe data, subscribeList is %s." % self.subscribe_symbol_list) 124 | return ','.join(self.subscribe_symbol_list) 125 | 126 | def handle_exchange_trade_data(self, json_msg): 127 | """ 128 | 处理交易所推送的trade数据 129 | :param json_msg: 130 | :return: 131 | """ 132 | if json_msg: 133 | # self.logger.info("ON_EXCHANGE_MSG={}".format(json_msg)) 134 | for (order_id, json_order) in self.order_dict.items(): 135 | # json_msg中的price是字符串 136 | # 由于trade信息中,price的精度表示与下单时price不一致,所以先转换成float再对比 137 | # 比如:'0.32300000000'和'0.323',通过字符串对比返回False,转换成float再对比返回True 138 | if json_order[SYMBOL] == json_msg[SYMBOL] and json_order[DIRECTION] == json_msg[ 139 | DIRECTION] and float(json_order[PRICE]) == float(json_msg[PRICE]): 140 | # 以下单记录列表中的order作为查询的消息主体,能保留下单请求的相关信息,同时增加订单查询的结果信息 141 | self.logger.info("__on_trade method will resolve data: " + json.dumps(json_msg)) 142 | json_order[ORDER_ID] = order_id 143 | self.broadcast(copy.deepcopy(json_order)) 144 | # global_utils.call_timer(self.broadcast, [copy.deepcopy(json_order)]) 145 | break 146 | 147 | def place_order(self, json_msg): 148 | try: 149 | symbol = json_msg.get(SYMBOL, '') 150 | price = json_msg.get(PRICE, '') 151 | volume = json_msg.get(VOLUME, '') 152 | price_type = json_msg.get(PRICE_TYPE, MARKET_PRICE) 153 | direction = json_msg.get(DIRECTION, '') 154 | trade_type = '{}-{}'.format(direction, price_type) 155 | 156 | # 下单时间 微秒级时间戳 157 | json_msg[PLACE_TIME] = round(time.time() * 1000 * 1000) 158 | # account_id在调用服务时传入(ServiceBase的__args_setup函数) 159 | rsp = self.huobi_api.send_order(volume, 'api', symbol, trade_type, price) 160 | # 下单返回时间 微秒级时间戳 161 | json_msg[PLACE_RSP_TIME] = round(time.time() * 1000 * 1000) 162 | self.logger.info("place_order rsp is: %s" % rsp) 163 | # 为None或空,都不处理 164 | if not rsp: 165 | json_msg[STATUS] = COMMON_RSP_STATUS_FALSE 166 | self.broadcast(json_msg) 167 | return None 168 | # API返回的status为"ok",统一转换为"true" 169 | if STATUS in rsp and huobi_utils.API_RSP_STATUS_OK == rsp[STATUS]: 170 | json_msg[STATUS] = COMMON_RSP_STATUS_TRUE 171 | json_msg[STATE] = ORDER_STATE_SUBMITTED 172 | order_id = rsp.get('data', None) 173 | json_msg[ORDER_ID] = order_id 174 | # 订单记录管理 175 | # 如果订单号重复,不重复记录 176 | if order_id and not self.order_dict.__contains__(str(order_id)): 177 | self.order_dict[str(order_id)] = copy.deepcopy(json_msg) 178 | else: 179 | self.logger.info("order_id is None or repeated order_id [%s]." % order_id) 180 | else: 181 | json_msg[STATUS] = COMMON_RSP_STATUS_FALSE 182 | # 返回错误码和错误信息 183 | code, msg = huobi_utils.convert_error_code(rsp) 184 | json_msg[ERROR_CODE] = code 185 | json_msg[ERROR_MESSAGE] = msg 186 | 187 | # 下单出错,广播下单错误消息 188 | error_rsp = {MSG_TYPE: MSG_PLACE_ORDER_ERROR, EXCHANGE: EXCHANGE_HUOBI, 189 | TIME: round(time.time() * 1000 * 1000), 190 | ERROR_CODE:json_msg[ERROR_CODE], ERROR_MESSAGE: json_msg[ERROR_MESSAGE]} 191 | self.broadcast(error_rsp) 192 | self.broadcast(json_msg) 193 | return json_msg 194 | except: 195 | # 下单出错,广播下单错误消息 196 | error_rsp = {MSG_TYPE: MSG_PLACE_ORDER_ERROR, EXCHANGE: EXCHANGE_HUOBI, 197 | "time": round(time.time() * 1000 * 1000)} 198 | self.broadcast(error_rsp) 199 | 200 | json_msg[STATUS] = COMMON_RSP_STATUS_FALSE 201 | self.broadcast(json_msg) 202 | self.logger.error("placeOrder Error!!!") 203 | self.logger.error(traceback.format_exc()) 204 | return json_msg 205 | 206 | def cancel_order(self, json_msg): 207 | try: 208 | order_id = json_msg.get(ORDER_ID, '') 209 | # 如果订单已经不在order_dict中,则已经成交或撤单 210 | if order_id not in self.order_dict: 211 | json_msg[STATUS] = COMMON_RSP_STATUS_FALSE 212 | json_msg[ERROR_CODE] = ERROR_CODE_ORDER_NOT_EXIST 213 | json_msg[ERROR_MESSAGE] = ERROR_MSG_ORDER_NOT_EXIST 214 | self.broadcast(json_msg) 215 | return 216 | 217 | rsp = self.huobi_api.cancel_order(order_id) 218 | self.logger.info("cancel_order rsp is: %s" % rsp) 219 | # 为None或空,都不处理 220 | if not rsp: 221 | json_msg[STATUS] = COMMON_RSP_STATUS_FALSE 222 | self.broadcast(json_msg) 223 | return 224 | if STATUS in rsp and huobi_utils.API_RSP_STATUS_OK == rsp[STATUS]: 225 | # 请求成功 {STATUS: 'ok', 'data': '5254993691'} 226 | json_msg[STATUS] = COMMON_RSP_STATUS_TRUE 227 | json_msg[STATE] = ORDER_STATE_CANCELLING 228 | json_msg['client_order_id'] = rsp.get('data', "") 229 | self.order_update(order_id, json_msg) 230 | else: 231 | # 请求失败 {STATUS: 'error', 'err-code': 'order-orderstate-error', 'err-msg': 'the order state is error', 'data': None} 232 | json_msg[STATUS] = COMMON_RSP_STATUS_FALSE 233 | code, msg = huobi_utils.convert_error_code(rsp) 234 | json_msg[ERROR_CODE] = code 235 | json_msg[ERROR_MESSAGE] = msg 236 | self.broadcast(json_msg) 237 | except: 238 | json_msg[STATUS] = COMMON_RSP_STATUS_FALSE 239 | self.broadcast(json_msg) 240 | self.logger.error("__cancelOrder Error!!!") 241 | self.logger.error(traceback.format_exc()) 242 | 243 | def qry_balance(self, msg): 244 | """ 245 | 查询资金信息 246 | :param msg: 247 | :return: 248 | """ 249 | try: 250 | # 系统启动时将初始化资金信息,此后,交易所资金变动信息推送过来时,将更新此资金信息 251 | # 查询时,优先从本地内存取,如果最后更新时间在五分钟以前,将从交易所查询 252 | # 此处理避免多策略频繁查询接口,超出交易所查询次数限制 253 | # 统一修改为time.time()获取时间戳,精确到秒 254 | msg_type = msg.get(MSG_TYPE,"") 255 | if not self.account_id: 256 | self.account_id = msg.get(ACCOUNT_ID, None) 257 | # msg_type为MSG_BALANCE_STATUS时,表明订单状态更新时进行的查询(注意:内部消息处理线程更改了msg_type字段的值) 258 | # 需要强制向交易所查询最新数据,作为推送消息返回给调用方 259 | # 如果self.balances中不包含funds,直接去查API 260 | if msg_type != MSG_BALANCE_STATUS and self.balances and FUNDS in self.balances and (round(time.time()) - self.qry_balances_time) < UPDATE_BALANCE_INTERVAL_TIME: 261 | # 只共享funds信息,其他信息原样返回 262 | msg[FUNDS] = self.balances[FUNDS] 263 | msg[STATUS] = COMMON_RSP_STATUS_TRUE 264 | # msg[MSG_TYPE] = MSG_QRY_BALANCE_RSP 265 | if self.account_id: 266 | msg[ACCOUNT_ID] = self.account_id 267 | self.broadcast(msg) 268 | else: 269 | # 不需要传入self.account_id,此处的account_id与查询资金所需account_id没有任何关系 270 | rsp = self.huobi_api.get_balance() 271 | self.logger.info("qry_balance rsp is: %s" % rsp) 272 | # 为None或空,都不处理 273 | if not rsp: 274 | msg[STATUS] = COMMON_RSP_STATUS_FALSE 275 | if self.account_id: 276 | msg[ACCOUNT_ID] = self.account_id 277 | self.broadcast(msg) 278 | return 279 | # API返回status为"ok",统一转换为'true' 280 | if STATUS in rsp and huobi_utils.API_RSP_STATUS_OK == rsp[STATUS]: 281 | msg[STATUS] = COMMON_RSP_STATUS_TRUE 282 | funds = {} 283 | data = rsp.get('data', {}) 284 | rsp_balance_list = data.get('list', "") 285 | if rsp_balance_list is not None: 286 | for balance in rsp_balance_list: 287 | funds_temp = {} 288 | # 如果funds不包含当前currency key,则将该key添加到funds中,value在后面的逻辑更新 289 | if balance['currency'] not in funds: 290 | funds[balance['currency']] = {} 291 | 292 | funds_temp = funds[balance['currency']] 293 | # 目前huobi只有trade(交易余额)和frozen(冻结余额)两种状态 294 | if balance[TYPE] == huobi_utils.HUOBI_BALANCE_TYPE_TRADE: 295 | funds_temp[BALANCE_TYPE_FREE] = float(balance.get('balance', 0.00)) 296 | elif balance[TYPE] == huobi_utils.HUOBI_BALANCE_TYPE_FROZEN: 297 | funds_temp[BALANCE_TYPE_FREEZED] = float(balance.get('balance', 0.00)) 298 | else: 299 | funds_temp['other'] = float(balance.get('balance', 0.00)) 300 | 301 | funds_rsp = {} 302 | funds_rsp.update(msg) 303 | funds_rsp[FUNDS] = funds 304 | 305 | # 记录资金信息和最后更新时间 306 | self.qry_balances_time = round(time.time()) 307 | self.balances = funds_rsp 308 | if self.broadcast and self.account_id: 309 | funds_rsp[ACCOUNT_ID] = self.account_id 310 | self.broadcast(funds_rsp) 311 | else: 312 | msg[STATUS] = COMMON_RSP_STATUS_FALSE 313 | code, mssage = huobi_utils.convert_error_code(rsp) 314 | msg[ERROR_CODE] = code 315 | msg[ERROR_MESSAGE] = mssage 316 | if self.account_id: 317 | msg[ACCOUNT_ID] = self.account_id 318 | self.broadcast(msg) 319 | 320 | except: 321 | msg[STATUS] = COMMON_RSP_STATUS_FALSE 322 | if self.account_id: 323 | msg[ACCOUNT_ID] = self.account_id 324 | self.broadcast(msg) 325 | # self.broadcast(msg) 326 | self.logger.error("__qry_balance Error!!!") 327 | self.logger.error(traceback.format_exc()) 328 | 329 | def qry_order(self, json_msg): 330 | """ 331 | 目前不主动查询订单信息,通过主动上报获取订单 reviewed by xnb 332 | :param msg: 333 | :return: 334 | """ 335 | try: 336 | order_id = json_msg.get(ORDER_ID, None) 337 | if order_id is None or order_id == 'None': 338 | return 339 | # 当前订单刚开始加载到order_dict后查询订单trade还没初始化或者初始化还未初始化完成 340 | if not self.huobi_api: 341 | return 342 | rsp = self.huobi_api.order_info(order_id) 343 | self.logger.info("qry_order rsp is: %s" % rsp) 344 | 345 | # 为None或空,都不处理 346 | if not rsp: 347 | json_msg[STATUS] = COMMON_RSP_STATUS_FALSE 348 | self.broadcast(json_msg) 349 | # self.broadcast(msg) 350 | return 351 | if "err-msg" in rsp: 352 | json_msg[STATUS] = COMMON_RSP_STATUS_FALSE 353 | code, msg = huobi_utils.convert_error_code(rsp) 354 | json_msg[ERROR_CODE] = code 355 | json_msg[ERROR_MESSAGE] = msg 356 | self.broadcast(json_msg) 357 | return 358 | # 订单状态:pre-submitted 准备提交, submitting , submitted 已提交, partial-filled 部分成交, partial-canceled 部分成交撤销, filled 完全成交, canceled 已撤销 359 | json_order_data = rsp.get('data', {}) 360 | json_msg[STATE] = huobi_utils.convert_order_state(json_order_data.get(STATE, "")) 361 | # 返回信息只有成交总金额和成交量,无成交价,无均价,目前用总金额除以成交量,作为成交价和均价 362 | temp_price = 0.0 363 | if float(json_order_data['field-amount']) != 0.0: 364 | temp_price = float(json_order_data['field-cash-amount']) / float(json_order_data['field-amount']) 365 | json_msg[TRADE_PRICE] = temp_price 366 | json_msg[TRADE_VOLUME] = float(json_order_data['field-amount']) 367 | json_msg[AVG_PRICE] = temp_price 368 | # 订单状态更新后的查询资金和删除放在父类service_base中__self_qry_order_thread处理 369 | self.order_update(order_id, json_msg) 370 | self.broadcast(json_msg) 371 | except: 372 | json_msg[STATUS] = COMMON_RSP_STATUS_FALSE 373 | self.broadcast(json_msg) 374 | self.logger.error("qry_order Error!!!") 375 | self.logger.error(traceback.format_exc()) 376 | 377 | 378 | 379 | if __name__ == '__main__': 380 | service = TradeService() 381 | service.exchange_msg_handle_thread.join() 382 | 383 | -------------------------------------------------------------------------------- /study/indicator.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | def rsi(prices, n=14): 4 | ''' 5 | params: 6 | prices: python list type, close price of list of time series candles 7 | n: rsi params, default is 14 8 | return: 9 | rsi: python list type, rsi value of prices 10 | ''' 11 | pass -------------------------------------------------------------------------------- /study/jupyter-notebook-test.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "name": "stderr", 10 | "output_type": "stream", 11 | "text": [ 12 | "/Users/maxtang/Code/daxiang_trade/env/lib/python3.7/site-packages/swagger_spec_validator/validator20.py:53: SwaggerValidationWarning: Found \"$ref: #/definitions/UserPreferences\" with siblings that will be overwritten. See https://stackoverflow.com/a/48114924 for more information. (path #/definitions/User/properties/preferences)\n", 13 | " ref_dict['$ref'], '/'.join(path),\n" 14 | ] 15 | } 16 | ], 17 | "source": [ 18 | "from data_livetrade import Data\n", 19 | "data = Data()" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": 2, 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "ohlcv = data.get_latest_ohlcv('1h', 50)" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 3, 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "close = ohlcv.close" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 4, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "from utils import macd, ema, sma, rsi" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": 5, 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [ 55 | "from strategy import Strategy" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 6, 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "s = Strategy()" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 7, 70 | "metadata": {}, 71 | "outputs": [ 72 | { 73 | "data": { 74 | "text/plain": [ 75 | "'Nothing'" 76 | ] 77 | }, 78 | "execution_count": 7, 79 | "metadata": {}, 80 | "output_type": "execute_result" 81 | } 82 | ], 83 | "source": [ 84 | "s.MACD(ohlcv)" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": 8, 90 | "metadata": {}, 91 | "outputs": [ 92 | { 93 | "data": { 94 | "text/plain": [ 95 | "'Buy'" 96 | ] 97 | }, 98 | "execution_count": 8, 99 | "metadata": {}, 100 | "output_type": "execute_result" 101 | } 102 | ], 103 | "source": [ 104 | "s.RSI(ohlcv)" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": null, 110 | "metadata": {}, 111 | "outputs": [], 112 | "source": [] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": null, 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": null, 124 | "metadata": {}, 125 | "outputs": [], 126 | "source": [] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": null, 131 | "metadata": {}, 132 | "outputs": [], 133 | "source": [] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": null, 138 | "metadata": {}, 139 | "outputs": [], 140 | "source": [] 141 | }, 142 | { 143 | "cell_type": "code", 144 | "execution_count": null, 145 | "metadata": {}, 146 | "outputs": [], 147 | "source": [] 148 | }, 149 | { 150 | "cell_type": "code", 151 | "execution_count": null, 152 | "metadata": {}, 153 | "outputs": [], 154 | "source": [] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": null, 159 | "metadata": {}, 160 | "outputs": [], 161 | "source": [] 162 | }, 163 | { 164 | "cell_type": "code", 165 | "execution_count": null, 166 | "metadata": {}, 167 | "outputs": [], 168 | "source": [] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": null, 173 | "metadata": {}, 174 | "outputs": [], 175 | "source": [] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "execution_count": null, 180 | "metadata": {}, 181 | "outputs": [], 182 | "source": [] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": null, 187 | "metadata": {}, 188 | "outputs": [], 189 | "source": [] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": null, 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [] 197 | }, 198 | { 199 | "cell_type": "code", 200 | "execution_count": null, 201 | "metadata": {}, 202 | "outputs": [], 203 | "source": [] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": null, 208 | "metadata": {}, 209 | "outputs": [], 210 | "source": [] 211 | }, 212 | { 213 | "cell_type": "code", 214 | "execution_count": null, 215 | "metadata": {}, 216 | "outputs": [], 217 | "source": [] 218 | }, 219 | { 220 | "cell_type": "code", 221 | "execution_count": null, 222 | "metadata": {}, 223 | "outputs": [], 224 | "source": [] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": null, 229 | "metadata": {}, 230 | "outputs": [], 231 | "source": [] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": null, 236 | "metadata": {}, 237 | "outputs": [], 238 | "source": [] 239 | }, 240 | { 241 | "cell_type": "code", 242 | "execution_count": null, 243 | "metadata": {}, 244 | "outputs": [], 245 | "source": [] 246 | }, 247 | { 248 | "cell_type": "code", 249 | "execution_count": null, 250 | "metadata": {}, 251 | "outputs": [], 252 | "source": [] 253 | } 254 | ], 255 | "metadata": { 256 | "kernelspec": { 257 | "display_name": "Python 3", 258 | "language": "python", 259 | "name": "python3" 260 | }, 261 | "language_info": { 262 | "codemirror_mode": { 263 | "name": "ipython", 264 | "version": 3 265 | }, 266 | "file_extension": ".py", 267 | "mimetype": "text/x-python", 268 | "name": "python", 269 | "nbconvert_exporter": "python", 270 | "pygments_lexer": "ipython3", 271 | "version": "3.7.1" 272 | } 273 | }, 274 | "nbformat": 4, 275 | "nbformat_minor": 2 276 | } 277 | -------------------------------------------------------------------------------- /study/portfolio.py: -------------------------------------------------------------------------------- 1 | ''' 2 | By utilizing Data class and stratedy methods, the Portfolio will manage risk and be a portal to the system as well 3 | ''' 4 | from datetime import datetime as t 5 | import src.settings as s 6 | import src.utils as u 7 | import math 8 | 9 | logger = u.get_logger(__name__) 10 | 11 | class Portfolio: 12 | 13 | def __init__(self, strategy, data): 14 | ''' 15 | initial portfolio instance with strategy instance and data instance 16 | ''' 17 | self.data = data 18 | self.strategy = strategy 19 | self.rate = s.RATE 20 | self.leverage = s.LEVERAGE 21 | self.bin = s.BIN_SIZE 22 | self.balance = [(t.now(), self.data.get_wallet_balance(), 0, 0)] 23 | self.set_leverage(self.leverage) 24 | self.data.portfolio = self.portfolio_macd 25 | self.data.update_balance = self.update_balance 26 | 27 | def set_leverage(self, leverage): 28 | self.data.set_leverage(leverage) 29 | 30 | def get_qty(self): 31 | ''' 32 | calculate order quatity based on initial settings and current position quatity 33 | ''' 34 | margin = self.data.get_excess_margin() 35 | price = self.data.get_market_price() 36 | return math.floor(margin / 100000000 * price * self.leverage * self.rate) 37 | 38 | def portfolio_macd(self, ohlcv): 39 | ''' 40 | main process of portfolio 41 | ''' 42 | signal = self.strategy.MACD(ohlcv) 43 | logger.debug(f'signal: {signal}') 44 | current_position = self.data.get_current_qty() 45 | if signal == 'Buy': 46 | if current_position != 0: 47 | self.data.order(-current_position) 48 | qty = self.get_qty() 49 | self.data.buy(qty) 50 | elif signal == 'Sell': 51 | if current_position != 0: 52 | self.data.order(-current_position) 53 | qty = self.get_qty() 54 | self.data.sell(qty) 55 | else: pass 56 | 57 | def portfolio_rsi(self, ohlcv): 58 | ''' 59 | alternative portfolio 60 | ''' 61 | logger.debug(f'close price is: {ohlcv.close.values[-1]}') 62 | signal = self.strategy.RSI(ohlcv) 63 | logger.info(f'signal: {signal}') 64 | current_position = self.data.get_current_qty() 65 | if signal == 'Buy': 66 | if current_position < 0: 67 | self.data.order(-current_position) 68 | qty = self.get_qty() 69 | self.data.buy(qty) 70 | elif signal == 'Sell': 71 | if current_position > 0: 72 | self.data.order(-current_position) 73 | qty = self.get_qty() 74 | self.data.sell(qty) 75 | else: pass 76 | 77 | def update_balance(self): 78 | current_balance = self.data.get_wallet_balance() 79 | previous_balance = self.balance[-1][1] 80 | if current_balance != previous_balance: 81 | self.balance.append( 82 | (t.now(), 83 | current_balance, 84 | u.change_rate(previous_balance, current_balance), 85 | u.change_rate(self.balance[0][1], current_balance))) 86 | 87 | def portfolio_info(self): 88 | ''' 89 | 返回收益和持仓 90 | return profit and current position 91 | ''' 92 | return self.balance, self.data.get_current_qty(), self.data.get_avg_entry_price() 93 | -------------------------------------------------------------------------------- /study/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | 5 | class dotdict(dict): 6 | ''' 7 | dot.notation access to dictionary attributes 8 | ''' 9 | def __getattr__(self, attr): 10 | return self.get(attr) 11 | __setattr__ = dict.__setitem__ 12 | __delattr__ = dict.__delitem__ 13 | 14 | 15 | ######################################################################################################################## 16 | # Connection/Auth 17 | ######################################################################################################################## 18 | 19 | # API URL. 20 | BASE_URL = "https://testnet.bitmex.com/api/v1/" 21 | # BASE_URL = "https://www.bitmex.com/api/v1/" # Once you're ready, uncomment this. 22 | 23 | # The BitMEX API requires permanent API keys. Go to https://testnet.bitmex.com/app/apiKeys to fill these out. 24 | API_KEY = "faQczjkhb9UQ5nv09KTjyTRQ" 25 | API_SECRET = "God1eB-ywL0CfhXwflkyfcB9z7XV36sbwss_JkEvf1RQqF2E" 26 | 27 | 28 | ######################################################################################################################## 29 | # Target 30 | ######################################################################################################################## 31 | 32 | # Instrument to market make on BitMEX. 33 | SYMBOL = "XBTUSD" 34 | SUB_TOPICS = "tradeBin5m" 35 | 36 | 37 | ######################################################################################################################## 38 | # Order Size & Spread 39 | ######################################################################################################################## 40 | 41 | # How many pairs of buy/sell orders to keep open 42 | ORDER_PAIRS = 6 43 | 44 | # ORDER_START_SIZE will be the number of contracts submitted on level 1 45 | # Number of contracts from level 1 to ORDER_PAIRS - 1 will follow the function 46 | # [ORDER_START_SIZE + ORDER_STEP_SIZE (Level -1)] 47 | ORDER_START_SIZE = 100 48 | ORDER_STEP_SIZE = 100 49 | 50 | # Distance between successive orders, as a percentage (example: 0.005 for 0.5%) 51 | INTERVAL = 0.005 52 | 53 | # Minimum spread to maintain, in percent, between asks & bids 54 | MIN_SPREAD = 0.01 55 | 56 | # If True, market-maker will place orders just inside the existing spread and work the interval % outwards, 57 | # rather than starting in the middle and killing potentially profitable spreads. 58 | MAINTAIN_SPREADS = True 59 | 60 | # This number defines far much the price of an existing order can be from a desired order before it is amended. 61 | # This is useful for avoiding unnecessary calls and maintaining your ratelimits. 62 | # 63 | # Further information: 64 | # Each order is designed to be (INTERVAL*n)% away from the spread. 65 | # If the spread changes and the order has moved outside its bound defined as 66 | # abs((desired_order['price'] / order['price']) - 1) > settings.RELIST_INTERVAL) 67 | # it will be resubmitted. 68 | # 69 | # 0.01 == 1% 70 | RELIST_INTERVAL = 0.01 71 | 72 | 73 | ######################################################################################################################## 74 | # Trading Behavior 75 | ######################################################################################################################## 76 | 77 | # Position limits - set to True to activate. Values are in contracts. 78 | # If you exceed a position limit, the bot will log and stop quoting that side. 79 | CHECK_POSITION_LIMITS = False 80 | MIN_POSITION = -10000 81 | MAX_POSITION = 10000 82 | 83 | # If True, will only send orders that rest in the book (ExecInst: ParticipateDoNotInitiate). 84 | # Use to guarantee a maker rebate. 85 | # However -- orders that would have matched immediately will instead cancel, and you may end up with 86 | # unexpected delta. Be careful. 87 | POST_ONLY = False 88 | 89 | ######################################################################################################################## 90 | # Misc Behavior, Technicals 91 | ######################################################################################################################## 92 | 93 | # If true, don't set up any orders, just say what we would do 94 | # DRY_RUN = True 95 | DRY_RUN = False 96 | 97 | # How often to re-check and replace orders. 98 | # Generally, it's safe to make this short because we're fetching from websockets. But if too many 99 | # order amend/replaces are done, you may hit a ratelimit. If so, email BitMEX if you feel you need a higher limit. 100 | LOOP_INTERVAL = 5 101 | 102 | # Wait times between orders / errors 103 | API_REST_INTERVAL = 1 104 | API_ERROR_INTERVAL = 10 105 | TIMEOUT = 7 106 | 107 | # If we're doing a dry run, use these numbers for BTC balances 108 | DRY_BTC = 50 109 | 110 | # Available levels: logging.(DEBUG|INFO|WARN|ERROR) 111 | LOG_LEVEL = logging.DEBUG 112 | 113 | # To uniquely identify orders placed by this bot, the bot sends a ClOrdID (Client order ID) that is attached 114 | # to each order so its source can be identified. This keeps the market maker from cancelling orders that are 115 | # manually placed, or orders placed by another bot. 116 | # 117 | # If you are running multiple bots on the same symbol, give them unique ORDERID_PREFIXes - otherwise they will 118 | # cancel each others' orders. 119 | # Max length is 13 characters. 120 | ORDERID_PREFIX = "mm_bitmex_" 121 | 122 | ######################################################################################################################## 123 | # BitMEX Portfolio 124 | ######################################################################################################################## 125 | 126 | # Specify the contracts that you hold. These will be used in portfolio calculations. 127 | CONTRACTS = ['XBTUSD'] 128 | -------------------------------------------------------------------------------- /study/settings_1.py: -------------------------------------------------------------------------------- 1 | ######################################################################################################################## 2 | # Connection/Auth 3 | ######################################################################################################################## 4 | 5 | # Testnet: "https://testnet.bitmex.com/api/v1/" 6 | # Live Network: "https://www.bitmex.com/api/v1/" 7 | # TEST = True --> use Testnet, TEST = False --> use Live Network 8 | TEST= True 9 | 10 | # The BitMEX API requires permanent API keys. Go to https://testnet.bitmex.com/app/apiKeys or https://www.bitmex.com/app/apiKeys to fill these out. 11 | API_KEY = "faQczjkhb9UQ5nv09KTjyTRQ" 12 | API_SECRET = "God1eB-ywL0CfhXwflkyfcB9z7XV36sbwss_JkEvf1RQqF2E" 13 | 14 | # web info for dashboard 15 | # put your host public ip and port here, such as: '52.78.117.239' 16 | DASHBOARD_HOST = '127.0.0.1' 17 | DASHBOARD_PORT = 8080 18 | 19 | ######################################################################################################################## 20 | # Target 21 | ######################################################################################################################## 22 | 23 | # Instrument. 24 | SYMBOL = "XBTUSD" 25 | # Candle interval for ohlcv data 26 | # Available size:'1m','5m','1h','1d' 27 | BIN_SIZE = "5m" 28 | # Interval 29 | INTERVAL = { 30 | '1m': [1, '1T'], 31 | '5m': [5 * 1, '5T'], 32 | '15m': [15 * 1, '15T'], 33 | '30m': [30 * 1, '30T'], 34 | '45m': [45 *1, '45T'], 35 | '1h': [60 * 1, '1H'] 36 | } 37 | # Leverage x 38 | LEVERAGE= 5 39 | # rate = order amount / total balance 40 | RATE = 0.5 41 | 42 | ######################################################################################################################## 43 | # Others 44 | ######################################################################################################################## 45 | 46 | # Logging Level 47 | # CRITICAL, ERROR, WARNING, INFO, DEBUG 48 | LOG_LEVEL = 'DEBUG' -------------------------------------------------------------------------------- /study/strategy.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Provide different strategys. if want to add a new one, just put here. 3 | params: pandas dataframe with ohlcv data 4 | return: signal string, 'Buy', or 'Sell', or 'Nothing' 5 | ''' 6 | import src.utils as u 7 | 8 | logger = u.get_logger(__name__) 9 | 10 | class Strategy: 11 | 12 | def __init__(self): 13 | pass 14 | 15 | def MACD(self, df): 16 | ''' 17 | strategy function read data from ohlcv array with length 50, gives a long or short or nothing signal 18 | ''' 19 | df = u.macd(df) 20 | logger.debug(f'DIF: {str(df.macd.values[-2])} - {str(df.macd.values[-1])}') 21 | logger.debug(f'DEA: {str(df.macd_signal.values[-2])} - {str(df.macd_signal.values[-1])}') 22 | logger.debug(f'BAR: {str(df.macd_diff.values[-2])} - {str(df.macd_diff.values[-1])}') 23 | if u.crossover(df.macd.values, df.macd_signal.values): 24 | return 'Buy' 25 | elif u.crossunder(df.macd.values, df.macd_signal.values): 26 | return 'Sell' 27 | else: 28 | return 'Nothing' 29 | 30 | def RSI(self, df, fast=12, slow=24): 31 | df = u.rsi(df) 32 | sma_fast = u.sma(df.close, fast).values[-1] 33 | sma_slow = u.sma(df.close, slow).values[-1] 34 | current_rsi = df.rsi.values[-1] 35 | if current_rsi < 30 and sma_fast > sma_slow: 36 | return 'Buy' 37 | elif current_rsi > 70: 38 | return 'Sell' 39 | else: return 'Nothing' -------------------------------------------------------------------------------- /study/study_bitmex_package.py: -------------------------------------------------------------------------------- 1 | import bitmex 2 | import settings as s 3 | 4 | client = bitmex.bitmex(api_key=s.API_KEY, api_secret=s.API_SECRET) 5 | 6 | data = client.Trade.Trade_getBucketed( 7 | binSize = '5m', 8 | symbol='XBTUSD', 9 | count=100, 10 | reverse=True 11 | ).result() -------------------------------------------------------------------------------- /study/study_bitmex_ws.py: -------------------------------------------------------------------------------- 1 | import settings as s 2 | from bitmex_ws import BitMEXWebsocket 3 | ''' 4 | study the data format received from bitmex, method to run the code 5 | 6 | open terminal 7 | cd to_dir_contain_the_file 8 | python3 9 | from study_bitmex_ws import trade_data 10 | trade_data() 11 | ''' 12 | 13 | def trade_data(): 14 | ''' 15 | "trade" subscription data format => real time trade info(成交), including Buy and Sell info 16 | [{'timestamp': '2019-04-19T08:03:27.802Z', 'symbol': 'XBTUSD', 'side': 'Sell', 'size': 100, 'price': 5243, 'tickDirection': 'ZeroMinusTick', 'trdMatchID': 'f362e74c-886c-5ebe-ad35-ef1fdf316af4', 'grossValue': 1907300, 'homeNotional': 0.019073, 'foreignNotional': 100}] 17 | "tradeBin1m": => candle data for 1 minutes 18 | [{"timestamp": "2019-04-19T08:10:00.000Z", "symbol": "XBTUSD", "open": 5239.5, "high": 5239.5, "low": 5238.5, "close": 5238.5, "trades": 165, "volume": 488144, "vwap": 5239.4425, "lastSize": 3, "turnover": 9317171239, "homeNotional": 93.17171238999997, "foreignNotional": 488144}] 19 | "tradeBin5m": => candle data for 5 minutes 20 | "tradeBin1h": => candle data for 1 hour 21 | "tradeBin1d": => candle data for 1 day 22 | ''' 23 | endpoint = s.BASE_URL 24 | symbol = 'XBTUSD' 25 | sub_topic = 'tradeBin5m' 26 | API_KEY = s.API_KEY 27 | API_SECRET = s.API_SECRET 28 | 29 | ws = BitMEXWebsocket(endpoint=endpoint, symbol=symbol, sub_topic=sub_topic, api_key=API_KEY, api_secret=API_SECRET) 30 | 31 | 32 | def quote_data(): 33 | ''' 34 | "quote" subscription data format => real time quote info (报价), including Buy and Sell info 35 | [{"timestamp": "2019-04-19T08:20:53.129Z", "symbol": "XBTUSD", "bidSize": 106061, "bidPrice": 5234, "askPrice": 5234.5, "askSize": 945501}, {"timestamp": "2019-04-19T08:20:53.412Z", "symbol": "XBTUSD", "bidSize": 106061, "bidPrice": 5234, "askPrice": 5234.5, "askSize": 955501}, {"timestamp": "2019-04-19T08:20:53.706Z", "symbol": "XBTUSD", "bidSize": 106061, "bidPrice": 5234, "askPrice": 5234.5, "askSize": 1650443}, {"timestamp": "2019-04-19T08:20:53.721Z", "symbol": "XBTUSD", "bidSize": 106061, "bidPrice": 5234, "askPrice": 5234.5, "askSize": 1655443}, {"timestamp": "2019-04-19T08:20:53.908Z", "symbol": "XBTUSD", "bidSize": 106061, "bidPrice": 5234, "askPrice": 5234.5, "askSize": 1670443}]"tradeBin1m": => candle data for 1 minutes 36 | "quoteBin1m": => candle data for 1 minutes, feels like the average bid and ask info over the minutes 37 | [{"timestamp": "2019-04-19T08:26:00.000Z", "symbol": "XBTUSD", "bidSize": 1487579, "bidPrice": 5231, "askPrice": 5231.5, "askSize": 452199}]} 38 | "quoteBin5m": => candle data for 1 hour 39 | "quoteBin1h": => candle data for 1 hour 40 | "quoteBin1d": => candle data for 1 day 41 | ''' 42 | endpoint = s.BASE_URL 43 | symbol = 'XBTUSD' 44 | sub_topic = 'quoteBin1m' 45 | API_KEY = s.API_KEY 46 | API_SECRET = s.API_SECRET 47 | 48 | ws = BitMEXWebsocket(endpoint=endpoint, symbol=symbol, sub_topic=sub_topic, api_key=API_KEY, api_secret=API_SECRET) 49 | 50 | def margin_data(): 51 | ''' 52 | A margin is the money borrowed from a brokerage firm to purchase an investment 53 | ''' 54 | endpoint = s.BASE_URL 55 | symbol = 'XBTUSD' 56 | sub_topic = 'margin' 57 | API_KEY = s.API_KEY 58 | API_SECRET = s.API_SECRET 59 | 60 | ws = BitMEXWebsocket(endpoint=endpoint, symbol=symbol, sub_topic=sub_topic, api_key=API_KEY, api_secret=API_SECRET) 61 | 62 | def position_data(): 63 | ''' 64 | 65 | ''' 66 | endpoint = s.BASE_URL 67 | symbol = 'XBTUSD' 68 | sub_topic = 'position' 69 | API_KEY = s.API_KEY 70 | API_SECRET = s.API_SECRET 71 | 72 | ws = BitMEXWebsocket(endpoint=endpoint, symbol=symbol, sub_topic=sub_topic, api_key=API_KEY, api_secret=API_SECRET) 73 | 74 | def instrument_data(): 75 | endpoint = s.BASE_URL 76 | symbol = 'XBTUSD' 77 | sub_topic = 'instrument' 78 | API_KEY = s.API_KEY 79 | API_SECRET = s.API_SECRET 80 | 81 | ws = BitMEXWebsocket(endpoint=endpoint, symbol=symbol, sub_topic=sub_topic, api_key=API_KEY, api_secret=API_SECRET) 82 | 83 | def execution_data(): 84 | endpoint = s.BASE_URL 85 | symbol = 'XBTUSD' 86 | sub_topic = 'execution' 87 | API_KEY = s.API_KEY 88 | API_SECRET = s.API_SECRET 89 | 90 | ws = BitMEXWebsocket(endpoint=endpoint, symbol=symbol, sub_topic=sub_topic, api_key=API_KEY, api_secret=API_SECRET) 91 | -------------------------------------------------------------------------------- /study/test_utils.py: -------------------------------------------------------------------------------- 1 | from utils import toNearest 2 | 3 | def test_toNearest(): 4 | # exact half way numbers are rounded to the nearest even result 5 | assert toNearest(1.45, 0.1) == 1.4 6 | assert toNearest(1.55, 0.1) == 1.6 -------------------------------------------------------------------------------- /study/tick_data_base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 2018/7/13 11:17 3 | # @Author : xnbo 4 | # @Site : 5 | # @File : tick_data_base.py 6 | # @Software: PyCharm 7 | 8 | import time 9 | import queue 10 | import threading 11 | import copy 12 | import traceback 13 | 14 | from common.constant import * 15 | 16 | 17 | class TickDataBase(object): 18 | def __init__(self, symbol, exchange, market_broadcast, on_depth_update=None, symbol_type=SPOT): 19 | self.symbol = symbol 20 | self.exchange = exchange # (string)交易平台 21 | self.symbol_type = symbol_type # (string)none:不区分类型 spot:币币 futures:期货 22 | self.open = 0 # (double)开盘价格 23 | self.high = 0 # (double)最高价格 24 | self.last = 0 # (double)最新成交价 25 | self.low = 0 # (double)最低价格 26 | self.volume = 0 # (double)成交量(最近的24小时) 27 | self.limit_high = 0 # (double)最高买入限制价格 28 | self.limit_low = 0 # (double)最低卖出限制价格 29 | self.asks = [] # [[double,double]]卖方深[价格,量] 30 | self.bids = [] # [[double,double]]买方深度[价格,量] 31 | self.unit_amount = 0 # (double)合约价值 32 | self.hold_amount = 0 # (double)当前持仓量 33 | self.contract_id = 0 # (long)合约ID 34 | self.timestamp = 0 # (long)时间戳 35 | self.local_time = 0 # (long)本地时间戳 36 | 37 | # 存储已回调数据,用来判断行情是否发生变化需要回调 38 | self.old_last = 0 # (double)最新成交价 39 | self.old_asks = [] # [[double,double]]卖方深[价格,量] 40 | self.old_bids = [] # [[double,double]]买方深度[价格,量] 41 | 42 | # 注册on_market回调函数 43 | self.__market_broadcast = market_broadcast 44 | 45 | # 注册深度行情更新回调函数 46 | if on_depth_update: 47 | self.__on_depth_update = on_depth_update 48 | 49 | # 每个交易对都对应一个数据队列,存储交易所推送的ticker和深度行情数据 50 | self.__tick_data_queue = queue.Queue() 51 | # 每个交易对都对应一个数据队列,存储交易所推送的ticker和深度行情数据 52 | self.__depth_data_queue = queue.Queue() 53 | 54 | # ticker和深度行情分两个线程处理 reviewed by xnb 55 | # 初始化tick_data实体类时,开启ticker数据队列处理线程 56 | self.__tick_data_thread_handle = threading.Thread(target=lambda: self.__tick_data_thread()) 57 | self.__tick_data_thread_handle.daemon = True 58 | self.__tick_data_thread_handle.start() 59 | 60 | # 初始化tick_data实体类时,开启深度行情数据队列处理线程 61 | self.__depth_data_thread_handle = threading.Thread(target=lambda: self.__depth_data_thread()) 62 | self.__depth_data_thread_handle.daemon = True 63 | self.__depth_data_thread_handle.start() 64 | 65 | def get_tick_data(self): 66 | """ 67 | 获取实体类的字典形式数据 68 | :return: 69 | """ 70 | # 增加了msg_type,避免在broadcast函数中更新此字段 71 | dict_data = {MSG_TYPE: MSG_MARKET_DATA, TK_SYMBOL: self.symbol, TK_EXCHANGE: self.exchange, TK_SYMBOL_TYPE: self.symbol_type, TK_OPEN: self.open, TK_HIGH: self.high, 72 | TK_LAST: self.last, TK_LOW: self.low, TK_VOLUME: self.volume, TK_LIMIT_HIGH: self.limit_high, TK_LIMIT_LOW: self.limit_low, 73 | TK_ASKS: self.asks, TK_BIDS: self.bids, TK_UNIT_AMOUNT: self.unit_amount, TK_HOLD_AMOUNT: self.hold_amount, TK_CONTRACT_ID: self.contract_id, 74 | TK_TIMESTAMP: self.timestamp, TK_LOCAL_TIME: self.local_time} 75 | return dict_data 76 | 77 | def put_tick_data(self, json_msg): 78 | """ 79 | 往数据队列插入ticker数据 80 | :param json_msg: 81 | :return: 82 | """ 83 | if json_msg: 84 | self.__tick_data_queue.put(json_msg) 85 | 86 | def __tick_data_thread(self): 87 | """ 88 | ticker数据队列处理线程 89 | :return: 90 | """ 91 | while True: 92 | try: 93 | json_msg = self.__tick_data_queue.get() 94 | if json_msg: 95 | self._update_tick_data(json_msg) 96 | except: 97 | print(traceback.format_exc()) 98 | 99 | def put_depth_data(self, json_msg): 100 | """ 101 | 往数据队列插入深度行情数据 102 | :param json_msg: 103 | :return: 104 | """ 105 | if json_msg: 106 | json_msg[LOCAL_TIME] = round(time.time() * 1000000) 107 | self.__depth_data_queue.put(json_msg) 108 | 109 | def __depth_data_thread(self): 110 | """ 111 | 深度行情数据队列处理线程 112 | :return: 113 | """ 114 | while True: 115 | try: 116 | json_msg = self.__depth_data_queue.get() 117 | if json_msg: 118 | self._update_depth_data(json_msg) 119 | if self.old_last != self.last or self.old_asks != self.asks or self.old_bids != self.bids: 120 | self.old_last = copy.deepcopy(self.last) 121 | self.old_asks = copy.deepcopy(self.asks) 122 | self.old_bids = copy.deepcopy(self.bids) 123 | json_data = self.get_tick_data() 124 | self.__market_broadcast(json_data) 125 | if self.__on_depth_update: 126 | self.__on_depth_update(json_data) 127 | except: 128 | print(traceback.format_exc()) 129 | 130 | def _update_tick_data(self, tick_data): 131 | """ 132 | update tick data, rewritten by derived classes 133 | :param tick_data: tick data dict 134 | :return: 135 | """ 136 | 137 | def _update_depth_data(self, depth_data): 138 | """ 139 | update depth data, rewritten by derived classes 140 | :param depth_data: depth data dict 141 | :return: 142 | """ 143 | 144 | -------------------------------------------------------------------------------- /study/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | import importlib 5 | from decimal import Decimal 6 | import settings 7 | 8 | def import_path(fullpath): 9 | """ 10 | Import a file with full path specification. Allows one to 11 | import from anywhere, something __import__ does not do. 12 | """ 13 | path, filename = os.path.split(fullpath) 14 | filename, _ = os.path.splitext(filename) 15 | sys.path.insert(0, path) 16 | module = importlib.import_module(filename, path) 17 | importlib.reload(module) # Might be out of date 18 | del sys.path[0] 19 | return module 20 | 21 | def setup_custom_logger(name, log_level=settings.LOG_LEVEL): 22 | ''' 23 | customize logger format 24 | ''' 25 | formatter = logging.Formatter(fmt='%(asctime)s - %(levelname)s - %(module)s - %(message)s') 26 | 27 | handler = logging.StreamHandler() 28 | handler.setFormatter(formatter) 29 | 30 | logger = logging.getLogger(name) 31 | logger.setLevel(log_level) 32 | logger.addHandler(handler) 33 | return logger 34 | 35 | def toNearest(num, tickSize): 36 | ''' 37 | Given a number, round it to the nearest tick. Very useful for sussing float error 38 | out of numbers: e.g. toNearest(401.46, 0.01) -> 401.46, whereas processing is 39 | normally with floats would give you 401.46000000000004. 40 | Use this after adding/subtracting/multiplying numbers. 41 | ''' 42 | tickDec = Decimal(str(tickSize)) 43 | return float((Decimal(round(num / tickSize, 0)) * tickDec)) -------------------------------------------------------------------------------- /study/utils_1.py: -------------------------------------------------------------------------------- 1 | import time 2 | # import os 3 | # import base64 4 | # import logging 5 | # import pandas as pd 6 | 7 | TERM_RED = '\033[1;31m' 8 | TERM_NFMT = '\033[0;0m' 9 | TERM_BLUE = '\033[1;34m' 10 | TERM_GREEN = '\033[1;32m' 11 | 12 | def print_error(func_name, err): 13 | ''' 14 | highlight error info 15 | ''' 16 | print(f'{TERM_RED}{func_name} - {err}{TERM_NFMT}') 17 | 18 | def current_milli_ts() -> str: 19 | return str(int(time.time() * 1000)) 20 | 21 | # ######################################################################################################################## 22 | # # Logging relates 23 | # ######################################################################################################################## 24 | # def get_logger(name, log_level=s.LOG_LEVEL): 25 | # ''' 26 | # customize logger format 27 | # ''' 28 | # formatter = logging.Formatter(fmt='%(asctime)s - %(levelname)s - %(message)s') 29 | # file_handler = logging.FileHandler('daxiang_robot.log') 30 | # file_handler.setFormatter(formatter) 31 | # console_handler = logging.StreamHandler() 32 | # console_handler.setFormatter(formatter) 33 | # logger = logging.getLogger(name) 34 | # logger.setLevel(log_level) 35 | # logger.addHandler(file_handler) 36 | # logger.addHandler(console_handler) 37 | # return logger 38 | 39 | # def read_log(file): 40 | # ''' 41 | # read a log file line by line, return a html formatted string 42 | # ''' 43 | # text = '' 44 | # if not os.path.isfile(file): return text 45 | # with open(file,'r') as f: 46 | # lines = f.readlines() 47 | # for line in lines: 48 | # text += line 49 | # text += '
' 50 | # return text 51 | 52 | # def read_recent_log(file, offset): 53 | # ''' 54 | # read log from botton with offset 55 | # offset: should be negative, and it refers bytes 56 | # ''' 57 | # text = '' 58 | # if not os.path.isfile(file): return text 59 | # with open(file, 'rb') as f: 60 | # try: 61 | # f.seek(offset, os.SEEK_END) 62 | # lines = f.readlines() 63 | # lines = lines[::-1] 64 | # for line in lines: 65 | # text += line.decode() 66 | # text += '
' 67 | # except OSError: 68 | # lines = f.readlines() 69 | # lines = lines[::-1] 70 | # for line in lines: 71 | # text += line.decode() 72 | # text += '
' 73 | # return text 74 | 75 | # def href_wrapper(file): 76 | # ''' 77 | # return a html formatted string for href 78 | # ''' 79 | # return f'{file}' 80 | 81 | # def logging_order(id, type, side, qty, price=None, stop=None): 82 | # logger.info(f"========= New Order ==============") 83 | # logger.info(f"ID : {id}") 84 | # logger.info(f"Type : {type}") 85 | # logger.info(f"Side : {side}") 86 | # logger.info(f"Qty : {qty}") 87 | # logger.info(f"Price : {price}") 88 | # logger.info(f"Stop : {stop}") 89 | # logger.info(f"======================================") 90 | 91 | # ######################################################################################################################## 92 | # # Network relates 93 | # ######################################################################################################################## 94 | # def retry(func, count=5): 95 | # ''' 96 | # Bitmex http request wrapper function for robust purpose. 97 | # For 503 case ("The system is currently overloaded. Please try again later."), 98 | # will not increase index, make request until succeed. 99 | # ''' 100 | # err = None 101 | # i = 0 102 | # while i < count: 103 | # try: 104 | # ret, res = func() 105 | # rate_limit = int(res.headers['X-RateLimit-Limit']) 106 | # rate_remain = int(res.headers['X-RateLimit-Remaining']) 107 | # if rate_remain < 10: 108 | # time.sleep(5 * 60 * (1 + rate_limit - rate_remain) / rate_limit) 109 | # return ret 110 | # except HTTPError as error: 111 | # status_code = error.status_code 112 | # err = error 113 | # if status_code == 503: 114 | # time.sleep(0.5) 115 | # continue 116 | # elif status_code >= 500: 117 | # time.sleep(pow(2, i + 1)) 118 | # i = i+1 119 | # continue 120 | # elif status_code == 400 or \ 121 | # status_code == 401 or \ 122 | # status_code == 402 or \ 123 | # status_code == 403 or \ 124 | # status_code == 404 or \ 125 | # status_code == 429: 126 | # logger.error(Exception(error)) 127 | # raise Exception(error) 128 | # else: 129 | # i = i+1 130 | # logger.error(Exception(err)) 131 | # raise Exception(err) 132 | 133 | # ######################################################################################################################## 134 | # # List or string process 135 | # ######################################################################################################################## 136 | # def to_data_frame(data, reverse = False): 137 | # ''' 138 | # convert ohlcv data list to pandas frame 139 | # reverse the frame if latest come first 140 | # ''' 141 | # data_frame = pd.DataFrame(data, columns=["timestamp", "high", "low", "open", "close", "volume"]) 142 | # data_frame = data_frame.set_index("timestamp") 143 | # data_frame = data_frame.tz_localize(None).tz_localize('UTC', level=0) 144 | # if reverse: 145 | # data_frame = data_frame.iloc[::-1] 146 | # return data_frame 147 | 148 | # def resample(data_frame, bin_size): 149 | # resample_time = s.INTERVAL[bin_size][1] 150 | # return data_frame.resample(resample_time, closed='right').agg({ 151 | # "open": "first", 152 | # "high": "max", 153 | # "low": "min", 154 | # "close": "last", 155 | # "volume": "sum", 156 | # }) 157 | 158 | # def random_str(): 159 | # ''' 160 | # generate a random string 161 | # ''' 162 | # return base64.b64encode(os.urandom(5)).decode() 163 | 164 | # def change_rate(a, b): 165 | # ''' 166 | # calculate change rate from a to b 167 | # return percentage with 2 digits 168 | # ''' 169 | # return round(float((b-a)/a * 100), 2) 170 | 171 | # ######################################################################################################################## 172 | # # Basic technical analysis 173 | # ######################################################################################################################## 174 | # def crossover(a, b): 175 | # return a[-2] < b[-2] and a[-1] > b[-1] 176 | 177 | # def crossunder(a, b): 178 | # return a[-2] > b[-2] and a[-1] < b[-1] 179 | 180 | # def ema(series, periods): 181 | # return series.ewm(span=periods, adjust=False).mean() 182 | 183 | # def sma(series, periods): 184 | # return series.rolling(periods).mean() 185 | 186 | # def macd(df, n_fast=12, n_slow=26, n_signal=9): 187 | # """Calculate MACD, MACD Signal and MACD difference 188 | # :param df: pandas.DataFrame 189 | # :param n_fast: 190 | # :param n_slow: 191 | # :param n_signal: 192 | # :return: pandas.DataFrame 193 | # """ 194 | # EMAfast = ema(df.close, n_fast) 195 | # EMAslow = ema(df.close, n_slow) 196 | # MACD = pd.Series(EMAfast - EMAslow, name='macd') 197 | # MACD_signal = pd.Series(ema(MACD, n_signal), name='macd_signal') 198 | # MACD_diff = pd.Series(MACD - MACD_signal, name='macd_diff') 199 | # df = df.join(MACD) 200 | # df = df.join(MACD_signal) 201 | # df = df.join(MACD_diff) 202 | # return df 203 | 204 | # def rsi(df, n=14): 205 | # close = df.close 206 | # diff = close.diff(1) 207 | # which_dn = diff < 0 208 | # up, dn = diff, diff*0 209 | # up[which_dn], dn[which_dn] = 0, -up[which_dn] 210 | # emaup = ema(up, n) 211 | # emadn = ema(dn, n) 212 | # RSI = pd.Series(100 * emaup / (emaup + emadn), name='rsi') 213 | # df = df.join(RSI) 214 | # return df -------------------------------------------------------------------------------- /study/web.py: -------------------------------------------------------------------------------- 1 | ''' 2 | a http server to display system info 3 | ''' 4 | import threading 5 | import src.utils as u 6 | from flask import Flask 7 | from gevent.pywsgi import WSGIServer 8 | from datetime import datetime as t 9 | 10 | class Web: 11 | 12 | def __init__(self, portfolio): 13 | ''' 14 | initial with a portfolio instance, so the web only display the portfolio info and log info 15 | ''' 16 | self.p = portfolio 17 | self.app = Flask(__name__) 18 | self.register_route() 19 | self.start_time = t.now() 20 | self.start_web_server() 21 | 22 | def _start_web_server(self): 23 | ''' 24 | start the web server with WSGI standard 25 | ''' 26 | self.server = WSGIServer(('', 8080), self.app, log=None) 27 | self.server.serve_forever() 28 | 29 | def start_web_server(self): 30 | ''' 31 | create a threading to host web server 32 | ''' 33 | t = threading.Thread(target=self._start_web_server, args=()) 34 | t.daemon = True 35 | t.start() 36 | 37 | def register_route(self): 38 | ''' 39 | register route without decorator 40 | ''' 41 | self.app.add_url_rule('/', 'index', self.index) 42 | self.app.add_url_rule('/log', 'log', self.log) 43 | 44 | def log(self): 45 | return u.read_log('daxiang_robot.log') 46 | 47 | def index(self): 48 | balance, position, entry_price = self.p.portfolio_info() 49 | text = f'Daxiang Trading Robot - Uptime {t.now() - self.start_time}

' 50 | text += f'Current Position: {position}, Average Price: {entry_price}
' 51 | text += 'Profit History:
' 52 | text += '---------------------- Balance(XBT) -- Change_Rate -- Total_Change_Rate
' 53 | for b in balance: 54 | text += f'{b[0]}: {b[1]/100000000}, {b[2]}%, {b[3]}%
' 55 | text += '

' 56 | text += 'Recent System Log:
' 57 | text += u.read_recent_log('daxiang_robot.log', -1024*10) 58 | text += '
' 59 | text += 'Full System Log:
' 60 | text += u.href_wrapper('daxiang_robot.log') 61 | text += '
' 62 | return text --------------------------------------------------------------------------------