├── .gitignore ├── LICENSE ├── __init__.py ├── _settings_base.py ├── auth ├── APIKeyAuth.py ├── APIKeyAuthWithExpires.py ├── AccessTokenAuth.py └── __init__.py ├── bitmex.py ├── custom_strategy.py ├── custom_strategy_V1.0.py ├── custom_strategy_V2.0.py ├── custom_strategy_V2.1.py ├── custom_strategy_V2.2.py ├── custom_strategy_V3.0.py ├── custom_strategy_V3.1.py ├── custom_strategy_V4.py ├── custom_test.py ├── market_maker.py ├── settings.py ├── tele_bot_msg.py ├── telegram_msg.py ├── utils ├── __init__.py ├── constants.py ├── dotdict.py ├── errors.py ├── log.py └── math.py └── ws ├── __init__.py └── ws_thread.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 judgelight 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | 4 | import shutil 5 | 6 | 7 | __version__ = 'v1.5' 8 | 9 | 10 | def run(): 11 | parser = argparse.ArgumentParser(description='sample BitMEX market maker') 12 | parser.add_argument('command', nargs='?', help='Instrument symbol on BitMEX or "setup" for first-time config') 13 | args = parser.parse_args() 14 | 15 | if args.command is not None and args.command.strip().lower() == 'setup': 16 | copy_files() 17 | 18 | else: 19 | # import market_maker here rather than at the top because it depends on settings.py existing 20 | try: 21 | from market_maker import market_maker 22 | market_maker.run() 23 | except ImportError: 24 | print('Can\'t find settings.py. Run "marketmaker setup" to create project.') 25 | 26 | 27 | def copy_files(): 28 | package_base = os.path.dirname(__file__) 29 | 30 | if not os.path.isfile(os.path.join(os.getcwd(), 'settings.py')): 31 | shutil.copyfile(os.path.join(package_base, '_settings_base.py'), 'settings.py') 32 | 33 | try: 34 | shutil.copytree(package_base, os.path.join(os.getcwd(), 'market_maker')) 35 | print('Created marketmaker project.\n**** \nImportant!!!\nEdit settings.py before starting the bot.\n****') 36 | except FileExistsError: 37 | print('Market Maker project already exists!') 38 | -------------------------------------------------------------------------------- /_settings_base.py: -------------------------------------------------------------------------------- 1 | from os.path import join 2 | import logging 3 | 4 | ######################################################################################################################## 5 | # Connection/Auth 6 | ######################################################################################################################## 7 | 8 | # API URL. 9 | BASE_URL = "https://testnet.bitmex.com/api/v1/" 10 | # BASE_URL = "https://www.bitmex.com/api/v1/" # Once you're ready, uncomment this. 11 | 12 | # The BitMEX API requires permanent API keys. Go to https://testnet.bitmex.com/app/apiKeys to fill these out. 13 | API_KEY = "" 14 | API_SECRET = "" 15 | 16 | 17 | ######################################################################################################################## 18 | # Target 19 | ######################################################################################################################## 20 | 21 | # Instrument to market make on BitMEX. 22 | SYMBOL = "XBTUSD" 23 | 24 | 25 | ######################################################################################################################## 26 | # Order Size & Spread 27 | ######################################################################################################################## 28 | 29 | # How many pairs of buy/sell orders to keep open 30 | ORDER_PAIRS = 6 31 | 32 | # ORDER_START_SIZE will be the number of contracts submitted on level 1 33 | # Number of contracts from level 1 to ORDER_PAIRS - 1 will follow the function 34 | # [ORDER_START_SIZE + ORDER_STEP_SIZE (Level -1)] 35 | ORDER_START_SIZE = 100 36 | ORDER_STEP_SIZE = 0 #无效了 37 | 38 | # 追加自定义参数ORDER_PIN_SIZE, 设置判断期货插针后接针的仓位数量 39 | ORDER_PIN_SIZE = 100 40 | 41 | # Distance between successive orders, as a percentage (example: 0.005 for 0.5%) 42 | INTERVAL = 0.005 43 | 44 | # 追加自定义参数INTERVAL2, 设置get_price_offset3的价格与最近初始位置的差价 45 | INTERVAL2 = 0 46 | 47 | # 追加自定义参数OFFSET_SPREAD, 设置订单之间最小差价, 默认为1, 若起始价格为3800, 则订单价格一次为3800,3801,3803,3807,3815,3831,3863,3927... 48 | #OFFSET_SPREAD = 1 新版不需要,固定差价0.5, 1, 2, 3, 5, 7, 11, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 105, 110, 115, 120, 125, 130, 135, 140, 145, 150, 155 49 | 50 | # Minimum spread to maintain, in percent, between asks & bids 51 | MIN_SPREAD = 0.01 52 | 53 | # If True, market-maker will place orders just inside the existing spread and work the interval % outwards, 54 | # rather than starting in the middle and killing potentially profitable spreads. 55 | MAINTAIN_SPREADS = True 56 | 57 | # This number defines far much the price of an existing order can be from a desired order before it is amended. 58 | # This is useful for avoiding unnecessary calls and maintaining your ratelimits. 59 | # 60 | # Further information: 61 | # Each order is designed to be (INTERVAL*n)% away from the spread. 62 | # If the spread changes and the order has moved outside its bound defined as 63 | # abs((desired_order['price'] / order['price']) - 1) > settings.RELIST_INTERVAL) 64 | # it will be resubmitted. 65 | # 66 | # 0.01 == 1% 67 | RELIST_INTERVAL = 0.01 68 | 69 | 70 | ######################################################################################################################## 71 | # Trading Behavior 72 | ######################################################################################################################## 73 | 74 | # Position limits - set to True to activate. Values are in contracts. 75 | # If you exceed a position limit, the bot will log and stop quoting that side. 76 | CHECK_POSITION_LIMITS = False 77 | MIN_POSITION = -10000 78 | MAX_POSITION = 10000 79 | 80 | # If True, will only send orders that rest in the book (ExecInst: ParticipateDoNotInitiate). 81 | # Use to guarantee a maker rebate. 82 | # However -- orders that would have matched immediately will instead cancel, and you may end up with 83 | # unexpected delta. Be careful. 84 | POST_ONLY = False 85 | 86 | ######################################################################################################################## 87 | # Misc Behavior, Technicals 88 | ######################################################################################################################## 89 | 90 | # If true, don't set up any orders, just say what we would do 91 | # DRY_RUN = True 92 | DRY_RUN = False 93 | 94 | # How often to re-check and replace orders. 95 | # Generally, it's safe to make this short because we're fetching from websockets. But if too many 96 | # order amend/replaces are done, you may hit a ratelimit. If so, email BitMEX if you feel you need a higher limit. 97 | LOOP_INTERVAL = 5 98 | 99 | # Wait times between orders / errors 100 | API_REST_INTERVAL = 1 101 | API_ERROR_INTERVAL = 10 102 | TIMEOUT = 7 103 | 104 | # If we're doing a dry run, use these numbers for BTC balances 105 | DRY_BTC = 50 106 | 107 | # Available levels: logging.(DEBUG|INFO|WARN|ERROR) 108 | LOG_LEVEL = logging.INFO 109 | 110 | # To uniquely identify orders placed by this bot, the bot sends a ClOrdID (Client order ID) that is attached 111 | # to each order so its source can be identified. This keeps the market maker from cancelling orders that are 112 | # manually placed, or orders placed by another bot. 113 | # 114 | # If you are running multiple bots on the same symbol, give them unique ORDERID_PREFIXes - otherwise they will 115 | # cancel each others' orders. 116 | # Max length is 13 characters. 117 | ORDERID_PREFIX = "mm_bitmex_" 118 | 119 | # If any of these files (and this file) changes, reload the bot. 120 | WATCHED_FILES = [join('market_maker', 'market_maker.py'), join('market_maker', 'bitmex.py'), 'settings.py'] 121 | 122 | 123 | ######################################################################################################################## 124 | # BitMEX Portfolio 125 | ######################################################################################################################## 126 | 127 | # Specify the contracts that you hold. These will be used in portfolio calculations. 128 | CONTRACTS = ['XBTUSD'] 129 | -------------------------------------------------------------------------------- /auth/APIKeyAuth.py: -------------------------------------------------------------------------------- 1 | from requests.auth import AuthBase 2 | import time 3 | import hashlib 4 | import hmac 5 | from future.builtins import bytes 6 | from future.standard_library import hooks 7 | with hooks(): # Python 2/3 compat 8 | from urllib.parse import urlparse 9 | 10 | 11 | class APIKeyAuth(AuthBase): 12 | 13 | """Attaches API Key Authentication to the given Request object.""" 14 | 15 | def __init__(self, apiKey, apiSecret): 16 | """Init with Key & Secret.""" 17 | self.apiKey = apiKey 18 | self.apiSecret = apiSecret 19 | 20 | def __call__(self, r): 21 | """Called when forming a request - generates api key headers.""" 22 | # modify and return the request 23 | nonce = generate_expires() 24 | r.headers['api-expires'] = str(nonce) 25 | r.headers['api-key'] = self.apiKey 26 | r.headers['api-signature'] = generate_signature(self.apiSecret, r.method, r.url, nonce, r.body or '') 27 | 28 | return r 29 | 30 | 31 | def generate_expires(): 32 | return int(time.time() + 3600) 33 | 34 | 35 | # Generates an API signature. 36 | # A signature is HMAC_SHA256(secret, verb + path + nonce + data), hex encoded. 37 | # Verb must be uppercased, url is relative, nonce must be an increasing 64-bit integer 38 | # and the data, if present, must be JSON without whitespace between keys. 39 | # 40 | # For example, in psuedocode (and in real code below): 41 | # 42 | # verb=POST 43 | # url=/api/v1/order 44 | # nonce=1416993995705 45 | # data={"symbol":"XBTZ14","quantity":1,"price":395.01} 46 | # signature = HEX(HMAC_SHA256(secret, 'POST/api/v1/order1416993995705{"symbol":"XBTZ14","quantity":1,"price":395.01}')) 47 | def generate_signature(secret, verb, url, nonce, data): 48 | """Generate a request signature compatible with BitMEX.""" 49 | # Parse the url so we can remove the base and extract just the path. 50 | parsedURL = urlparse(url) 51 | path = parsedURL.path 52 | if parsedURL.query: 53 | path = path + '?' + parsedURL.query 54 | 55 | if isinstance(data, (bytes, bytearray)): 56 | data = data.decode('utf8') 57 | 58 | # print "Computing HMAC: %s" % verb + path + str(nonce) + data 59 | message = verb + path + str(nonce) + data 60 | 61 | signature = hmac.new(bytes(secret, 'utf8'), bytes(message, 'utf8'), digestmod=hashlib.sha256).hexdigest() 62 | return signature 63 | -------------------------------------------------------------------------------- /auth/APIKeyAuthWithExpires.py: -------------------------------------------------------------------------------- 1 | from requests.auth import AuthBase 2 | import time 3 | from market_maker.auth.APIKeyAuth import generate_signature 4 | 5 | 6 | class APIKeyAuthWithExpires(AuthBase): 7 | 8 | """Attaches API Key Authentication to the given Request object. This implementation uses `expires`.""" 9 | 10 | def __init__(self, apiKey, apiSecret): 11 | """Init with Key & Secret.""" 12 | self.apiKey = apiKey 13 | self.apiSecret = apiSecret 14 | 15 | def __call__(self, r): 16 | """ 17 | Called when forming a request - generates api key headers. This call uses `expires` instead of nonce. 18 | 19 | This way it will not collide with other processes using the same API Key if requests arrive out of order. 20 | For more details, see https://www.bitmex.com/app/apiKeys 21 | """ 22 | # modify and return the request 23 | expires = int(round(time.time()) + 5) # 5s grace period in case of clock skew 24 | r.headers['api-expires'] = str(expires) 25 | r.headers['api-key'] = self.apiKey 26 | r.headers['api-signature'] = generate_signature(self.apiSecret, r.method, r.url, expires, r.body or '') 27 | 28 | return r 29 | -------------------------------------------------------------------------------- /auth/AccessTokenAuth.py: -------------------------------------------------------------------------------- 1 | from requests.auth import AuthBase 2 | 3 | 4 | class AccessTokenAuth(AuthBase): 5 | 6 | """Attaches Access Token Authentication to the given Request object.""" 7 | 8 | def __init__(self, accessToken): 9 | """Init with Token.""" 10 | self.token = accessToken 11 | 12 | def __call__(self, r): 13 | """Called when forming a request - generates access token header.""" 14 | if (self.token): 15 | r.headers['access-token'] = self.token 16 | return r 17 | -------------------------------------------------------------------------------- /auth/__init__.py: -------------------------------------------------------------------------------- 1 | from market_maker.auth.AccessTokenAuth import * 2 | from market_maker.auth.APIKeyAuth import * 3 | from market_maker.auth.APIKeyAuthWithExpires import * 4 | -------------------------------------------------------------------------------- /bitmex.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(self.apiKey, self.apiSecret) 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 | 83 | Returns 84 | ------- 85 | A list of dicts: 86 | {u'amount': 60, 87 | u'date': 1306775375, 88 | u'price': 8.7401099999999996, 89 | u'tid': u'93842'}, 90 | 91 | """ 92 | return self.ws.recent_trades() 93 | 94 | def get_last_trade(self, symbol, count, columns='price,side,size', filter=None): 95 | """返回最近交易, 倒叙排列""" 96 | if(filter != None): 97 | query = { 98 | 'symbol': symbol, 99 | 'count': count, 100 | 'columns': columns, 101 | 'reverse' : True, 102 | 'filter' : filter 103 | } 104 | else: 105 | query = { 106 | 'symbol': symbol, 107 | 'count': count, 108 | 'columns': columns, 109 | 'reverse' : True 110 | } 111 | return self._curl_bitmex(path='trade', query=query, verb='GET') 112 | 113 | # 114 | # Authentication required methods 115 | # 116 | def authentication_required(fn): 117 | """Annotation for methods that require auth.要求进行身份验证""" 118 | def wrapped(self, *args, **kwargs): 119 | if not (self.apiKey): 120 | msg = "You must be authenticated to use this method" 121 | raise errors.AuthenticationError(msg) 122 | else: 123 | return fn(self, *args, **kwargs) 124 | return wrapped 125 | 126 | @authentication_required 127 | def funds(self): 128 | """Get your current balance.你账户的余额和保证金要求的更新""" 129 | return self.ws.funds() 130 | 131 | @authentication_required 132 | def position(self, symbol): 133 | """Get your open position.你仓位的更新""" 134 | return self.ws.position(symbol) 135 | 136 | @authentication_required 137 | def isolate_margin(self, symbol, leverage, rethrow_errors=False): 138 | """Set the leverage on an isolated margin position""" 139 | path = "position/leverage" 140 | postdict = { 141 | 'symbol': symbol, 142 | 'leverage': leverage 143 | } 144 | return self._curl_bitmex(path=path, postdict=postdict, verb="POST", rethrow_errors=rethrow_errors) 145 | 146 | @authentication_required 147 | def delta(self): 148 | return self.position(self.symbol)['homeNotional'] 149 | 150 | @authentication_required 151 | def buy(self, quantity, price): 152 | """Place a buy order. 153 | 154 | Returns order object. ID: orderID 155 | """ 156 | return self.place_order(quantity, price) 157 | 158 | @authentication_required 159 | def sell(self, quantity, price): 160 | """Place a sell order. 161 | 162 | Returns order object. ID: orderID 163 | """ 164 | return self.place_order(-quantity, price) 165 | 166 | @authentication_required 167 | def place_order(self, quantity, price): 168 | """Place an order.""" 169 | if price < 0: 170 | raise Exception("Price must be positive.") 171 | 172 | endpoint = "order" 173 | # Generate a unique clOrdID with our prefix so we can identify it. 174 | clOrdID = self.orderIDPrefix + base64.b64encode(uuid.uuid4().bytes).decode('utf8').rstrip('=\n') 175 | postdict = { 176 | 'symbol': self.symbol, 177 | 'orderQty': quantity, 178 | 'price': price, 179 | 'clOrdID': clOrdID 180 | } 181 | return self._curl_bitmex(path=endpoint, postdict=postdict, verb="POST") 182 | 183 | @authentication_required 184 | def buy_stop(self, quantity, price): 185 | """Place a buy stop order. 186 | 187 | Returns order object. ID: orderID 188 | """ 189 | return self.stop_order(quantity, price, 'Buy') 190 | 191 | @authentication_required 192 | def sell_stop(self, quantity, price): 193 | """Place a sell stop order. 194 | 195 | Returns order object. ID: orderID 196 | """ 197 | return self.stop_order(-quantity, price, 'Sell') 198 | 199 | @authentication_required 200 | def stop_order(self, quantity, price, side): 201 | """Stop Market order.创建设置止损单""" 202 | if price < 0: 203 | raise Exception("Price must be positive.") 204 | 205 | endpoint = "order" 206 | # Generate a unique clOrdID with our prefix so we can identify it. 207 | clOrdID = self.orderIDPrefix + base64.b64encode(uuid.uuid4().bytes).decode('utf8').rstrip('=\n') 208 | postdict = { 209 | 'symbol': self.symbol, 210 | 'orderQty': abs(quantity), 211 | 'stopPx': price, 212 | 'side' : side, 213 | 'clOrdID': clOrdID, 214 | 'execInst': 'LastPrice' 215 | } 216 | return self._curl_bitmex(path=endpoint, postdict=postdict, verb="POST") 217 | 218 | @authentication_required 219 | def amend_bulk_orders(self, orders): 220 | """Amend multiple orders.修改多个订单""" 221 | # Note rethrow; if this fails, we want to catch it and re-tick 222 | return self._curl_bitmex(path='order/bulk', postdict={'orders': orders}, verb='PUT', rethrow_errors=True) 223 | 224 | @authentication_required 225 | def create_bulk_orders(self, orders): 226 | """Create multiple orders.创建多个订单""" 227 | for order in orders: 228 | order['clOrdID'] = self.orderIDPrefix + base64.b64encode(uuid.uuid4().bytes).decode('utf8').rstrip('=\n') 229 | order['symbol'] = self.symbol 230 | if self.postOnly: 231 | order['execInst'] = 'ParticipateDoNotInitiate' 232 | return self._curl_bitmex(path='order/bulk', postdict={'orders': orders}, verb='POST') 233 | 234 | @authentication_required 235 | def open_orders(self): 236 | """Get open orders.更新你自己的委托订单""" 237 | return self.ws.open_orders(self.orderIDPrefix) 238 | 239 | @authentication_required 240 | def http_open_orders(self): 241 | """Get open orders via HTTP. Used on close to ensure we catch them all.""" 242 | path = "order" 243 | orders = self._curl_bitmex( 244 | path=path, 245 | query={ 246 | 'filter': json.dumps({'ordStatus.isTerminated': False, 'symbol': self.symbol}), 247 | 'count': 500 248 | }, 249 | verb="GET" 250 | ) 251 | # Only return orders that start with our clOrdID prefix. 252 | return [o for o in orders if str(o['clOrdID']).startswith(self.orderIDPrefix)] 253 | 254 | @authentication_required 255 | def cancel(self, orderID): 256 | """Cancel an existing order.取消一个存在的订单""" 257 | path = "order" 258 | postdict = { 259 | 'orderID': orderID, 260 | } 261 | return self._curl_bitmex(path=path, postdict=postdict, verb="DELETE") 262 | 263 | @authentication_required 264 | def cancel_all_orders(self): 265 | """Cancels all of your orders.取消所有存在的订单""" 266 | path = "order/all" 267 | return self._curl_bitmex(path=path, verb="DELETE") 268 | 269 | @authentication_required 270 | def withdraw(self, amount, fee, address): 271 | path = "user/requestWithdrawal" 272 | postdict = { 273 | 'amount': amount, 274 | 'fee': fee, 275 | 'currency': 'XBt', 276 | 'address': address 277 | } 278 | return self._curl_bitmex(path=path, postdict=postdict, verb="POST", max_retries=0) 279 | 280 | def _curl_bitmex(self, path, query=None, postdict=None, timeout=None, verb=None, rethrow_errors=False, 281 | max_retries=None): 282 | """Send a request to BitMEX Servers.""" 283 | # Handle URL 284 | url = self.base_url + path 285 | 286 | if timeout is None: 287 | timeout = self.timeout 288 | 289 | # Default to POST if data is attached, GET otherwise 290 | if not verb: 291 | verb = 'POST' if postdict else 'GET' 292 | 293 | # By default don't retry POST or PUT. Retrying GET/DELETE is okay because they are idempotent. 294 | # In the future we could allow retrying PUT, so long as 'leavesQty' is not used (not idempotent), 295 | # or you could change the clOrdID (set {"clOrdID": "new", "origClOrdID": "old"}) so that an amend 296 | # can't erroneously be applied twice. 297 | if max_retries is None: 298 | max_retries = 200 if verb in ['POST', 'PUT'] else 600 299 | 300 | # Auth: API Key/Secret 301 | auth = APIKeyAuthWithExpires(self.apiKey, self.apiSecret) 302 | 303 | def exit_or_throw(e): 304 | if rethrow_errors: 305 | raise e 306 | else: 307 | exit(1) 308 | 309 | def retry(): 310 | self.retries += 1 311 | if self.retries > max_retries: 312 | raise Exception("Max retries on %s (%s) hit, raising." % (path, json.dumps(postdict or ''))) 313 | return self._curl_bitmex(path, query, postdict, timeout, verb, rethrow_errors, max_retries) 314 | 315 | # Make the request 316 | response = None 317 | try: 318 | self.logger.info("sending req to %s: %s" % (url, json.dumps(postdict or query or ''))) 319 | req = requests.Request(verb, url, json=postdict, auth=auth, params=query) 320 | prepped = self.session.prepare_request(req) 321 | response = self.session.send(prepped, timeout=timeout) 322 | # Make non-200s throw 323 | response.raise_for_status() 324 | 325 | except requests.exceptions.HTTPError as e: 326 | if response is None: 327 | raise e 328 | 329 | # 401 - Auth error. This is fatal. 330 | if response.status_code == 401: 331 | self.logger.error("API Key or Secret incorrect, please check and restart.") 332 | self.logger.error("Error: " + response.text) 333 | if postdict: 334 | self.logger.error(postdict) 335 | # Always exit, even if rethrow_errors, because this is fatal 336 | exit(1) 337 | 338 | # 404, can be thrown if order canceled or does not exist. 339 | elif response.status_code == 404: 340 | if verb == 'DELETE': 341 | self.logger.error("Order not found: %s" % postdict['orderID']) 342 | return 343 | self.logger.error("Unable to contact the BitMEX API (404). " + 344 | "Request: %s \n %s" % (url, json.dumps(postdict))) 345 | exit_or_throw(e) 346 | 347 | # 429, ratelimit; cancel orders & wait until X-RateLimit-Reset 348 | elif response.status_code == 429: 349 | self.logger.error("Ratelimited on current request. Sleeping, then trying again. Try fewer " + 350 | "order pairs or contact support@bitmex.com to raise your limits. " + 351 | "Request: %s \n %s" % (url, json.dumps(postdict))) 352 | 353 | # Figure out how long we need to wait. 354 | ratelimit_reset = response.headers['X-RateLimit-Reset'] 355 | to_sleep = int(ratelimit_reset) - int(time.time()) 356 | reset_str = datetime.datetime.fromtimestamp(int(ratelimit_reset)).strftime('%X') 357 | 358 | # We're ratelimited, and we may be waiting for a long time. Cancel orders. 359 | self.logger.warning("Canceling all known orders in the meantime.") 360 | self.cancel([o['orderID'] for o in self.open_orders()]) 361 | 362 | self.logger.error("Your ratelimit will reset at %s. Sleeping for %d seconds." % (reset_str, to_sleep)) 363 | time.sleep(to_sleep) 364 | 365 | # Retry the request. 366 | return retry() 367 | 368 | # 503 - BitMEX temporary downtime, likely due to a deploy. Try again 369 | elif response.status_code == 503: 370 | self.logger.warning("Unable to contact the BitMEX API (503), retrying. " + 371 | "Request: %s \n %s" % (url, json.dumps(postdict))) 372 | time.sleep(3) 373 | return retry() 374 | 375 | elif response.status_code == 400: 376 | error = response.json()['error'] 377 | message = error['message'].lower() if error else '' 378 | 379 | # Duplicate clOrdID: that's fine, probably a deploy, go get the order(s) and return it 380 | if 'duplicate clordid' in message: 381 | orders = postdict['orders'] if 'orders' in postdict else postdict 382 | 383 | IDs = json.dumps({'clOrdID': [order['clOrdID'] for order in orders]}) 384 | orderResults = self._curl_bitmex('/order', query={'filter': IDs}, verb='GET') 385 | 386 | for i, order in enumerate(orderResults): 387 | if ( 388 | order['orderQty'] != abs(postdict['orderQty']) or 389 | order['side'] != ('Buy' if postdict['orderQty'] > 0 else 'Sell') or 390 | order['price'] != postdict['price'] or 391 | order['stopPx'] != postdict['stopPx'] or 392 | order['symbol'] != postdict['symbol']): 393 | raise Exception('Attempted to recover from duplicate clOrdID, but order returned from API ' + 394 | 'did not match POST.\nPOST data: %s\nReturned order: %s' % ( 395 | json.dumps(orders[i]), json.dumps(order))) 396 | # All good 397 | return orderResults 398 | 399 | elif 'insufficient available balance' in message: 400 | self.logger.error('Account out of funds. The message: %s' % error['message']) 401 | exit_or_throw(Exception('Insufficient Funds')) 402 | 403 | 404 | # If we haven't returned or re-raised yet, we get here. 405 | self.logger.error("Unhandled Error: %s: %s" % (e, response.text)) 406 | self.logger.error("Endpoint was: %s %s: %s" % (verb, path, json.dumps(postdict))) 407 | exit_or_throw(e) 408 | 409 | except requests.exceptions.Timeout as e: 410 | # Timeout, re-run this request 411 | self.logger.warning("Timed out on request: %s (%s), retrying..." % (path, json.dumps(postdict or ''))) 412 | return retry() 413 | 414 | except requests.exceptions.ConnectionError as e: 415 | self.logger.warning("Unable to contact the BitMEX API (%s). Please check the URL. Retrying. " + 416 | "Request: %s %s \n %s" % (e, url, json.dumps(postdict))) 417 | time.sleep(1) 418 | return retry() 419 | 420 | # Reset retry counter on success 421 | self.retries = 0 422 | 423 | return response.json() 424 | -------------------------------------------------------------------------------- /custom_strategy_V1.0.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import sys 3 | from os.path import getmtime 4 | import logging 5 | import requests 6 | from time import sleep 7 | import datetime 8 | import schedule 9 | import re 10 | 11 | from market_maker.market_maker import OrderManager, XBt_to_XBT 12 | from market_maker.settings import settings 13 | from market_maker.utils import log, constants, errors, math 14 | from telegram_msg import tg_send_message, tg_send_important_message 15 | 16 | # Used for reloading the bot - saves modified times of key files 17 | import os 18 | watched_files_mtimes = [(f, getmtime(f)) for f in settings.WATCHED_FILES] 19 | 20 | 21 | # 22 | # Helpers 23 | # 24 | logger = logging.getLogger('root') 25 | 26 | class CustomOrderManager(OrderManager): 27 | 28 | def reset(self): 29 | self.exchange.cancel_all_orders() 30 | self.sanity_check() 31 | self.print_status() 32 | self.position_grade = 0 33 | self.last_running_qty = 0 34 | self.cycleclock = 30 // settings.LOOP_INTERVAL 35 | #仓位等级由0-6级, 按持仓量分级, 每大于order size增加1级, 最高6级 36 | #持仓方向通过self.running_qty来判断, 大于0为多仓, 小于0为空仓 37 | schedule.every().day.at("00:00").do(self.write_mybalance) #每天00:00执行一次 38 | schedule.every(5).seconds.do(self.set_MarkPriceList) #每5秒执行一次 39 | self.MarkPriceList = [] 40 | marketPrice = self.exchange.get_portfolio()['XBTUSD']['markPrice'] 41 | for x in range(120): 42 | self.MarkPriceList.append(marketPrice) 43 | # Create orders and converge. 44 | with open(r'/root/mybalance.txt', 'r') as f: 45 | lines = f.readlines() 46 | m1 = re.match(r'(\d{4}-\d{2}-\d{2})\s(\d{2}\:\d{2}\:\d{2})\s+([0-9\.]+)', lines[-2]) 47 | self.yesterday_balance = float(m1.group(3)) 48 | m2 = re.match(r'(\d{4}-\d{2}-\d{2})\s(\d{2}\:\d{2}\:\d{2})\s+([0-9\.]+)', lines[-3]) 49 | self.before_yesterday_balance = float(m2.group(3)) 50 | self.place_orders() 51 | 52 | def write_mybalance(self): 53 | now = datetime.datetime.now() 54 | mybalance = '%.6f' % XBt_to_XBT(self.start_XBt) 55 | with open(r'/root/mybalance.txt', 'a') as f: 56 | f.write(now.strftime('%Y-%m-%d %H:%M:%S') + ' ' + mybalance + '\n') 57 | message = 'BitMEX今日交易统计\n' + \ 58 | '时间:' + now.strftime('%Y-%m-%d %H:%M:%S') + '\n' + \ 59 | '保证金余额:' + mybalance + '\n' + \ 60 | '合约数量:' + str(self.running_qty) + '\n' + \ 61 | '开仓价格:' + str(self.exchange.get_position()['avgCostPrice']) + '\n' + \ 62 | '风险等级:' + str(self.position_grade) + '\n' + \ 63 | '最新价格:' + str(self.get_ticker()['last']) + '\n' + \ 64 | '指数价格:' + str(self.exchange.get_portfolio()['XBTUSD']['markPrice']) + '\n' + \ 65 | '今日盈利:' + '%.6f' % (float(mybalance) - self.yesterday_balance) + '\n' + \ 66 | '作日盈利:' + '%.6f' % (self.yesterday_balance - self.before_yesterday_balance) 67 | tg_send_important_message(message) 68 | self.before_yesterday_balance = self.yesterday_balance 69 | self.yesterday_balance = float(mybalance) 70 | 71 | def set_MarkPriceList(self): 72 | self.MarkPriceList.pop() 73 | self.MarkPriceList.insert(0, self.exchange.get_portfolio()['XBTUSD']['markPrice']) 74 | 75 | def get_wave_coefficient(self): 76 | """求波动系数, 当前市场波动系数, 超过一定值取消挂单""" 77 | return (max(self.MarkPriceList) - min(self.MarkPriceList)) 78 | 79 | def get_position_grade(self): 80 | """获取仓位等级""" 81 | 82 | self.position_grade = abs(self.running_qty) // settings.ORDER_START_SIZE 83 | if abs(self.running_qty) == settings.ORDER_START_SIZE: 84 | self.position_grade = 0 85 | elif self.position_grade > 6: 86 | self.position_grade = 6 87 | return self.position_grade 88 | 89 | def get_price_offset2(self, index): 90 | """根据index依次设置每一个价格,这里为差价依次增大,为上一差价2倍""" 91 | # Maintain existing spreads for max profit 92 | if settings.MAINTAIN_SPREADS: 93 | start_position = self.start_position_buy if index < 0 else self.start_position_sell 94 | # First positions (index 1, -1) should start right at start_position, others should branch from there 95 | index = index + 1 if index < 0 else index - 1 96 | else: 97 | # Offset mode: ticker comes from a reference exchange and we define an offset. 98 | start_position = self.start_position_buy if index < 0 else self.start_position_sell 99 | 100 | # If we're attempting to sell, but our sell price is actually lower than the buy, 101 | # move over to the sell side. 102 | if index > 0 and start_position < self.start_position_buy: 103 | start_position = self.start_position_sell 104 | # Same for buys. 105 | if index < 0 and start_position > self.start_position_sell: 106 | start_position = self.start_position_buy 107 | if (self.running_qty != 0): 108 | avgCostPrice = self.exchange.get_position()['avgCostPrice'] 109 | if (avgCostPrice % 1 == 0.5): 110 | start_position = avgCostPrice 111 | else: 112 | start_position = avgCostPrice - 0.25 if index < 0 else avgCostPrice + 0.25 113 | if index > 0: 114 | return math.toNearest(start_position + (2 ** index - 1) * settings.OFFSET_SPREAD, self.instrument['tickSize']) 115 | if index < 0: 116 | return math.toNearest(start_position - (2 ** abs(index) - 1) * settings.OFFSET_SPREAD, self.instrument['tickSize']) 117 | if index == 0: 118 | return math.toNearest(start_position, self.instrument['tickSize']) 119 | 120 | def get_price_offset3(self, index): 121 | """按仓位等级来设置价格""" 122 | L = [1, 3, 5, 7, 9, 11, 13, 15, 17] 123 | if abs(index) > 10: 124 | logger.error("ORDER_PAIRS cannot over 10") 125 | self.exit() 126 | avgCostPrice = self.exchange.get_position()['avgCostPrice'] 127 | if (avgCostPrice % 0.5 == 0): 128 | start_position = avgCostPrice 129 | else: 130 | start_position = avgCostPrice - 0.25 if index < 0 else avgCostPrice + 0.25 131 | if (index > 0 and start_position < self.start_position_sell): 132 | start_position = self.start_position_sell 133 | if (index < 0 and start_position > self.start_position_buy): 134 | start_position = self.start_position_buy 135 | if settings.MAINTAIN_SPREADS: 136 | # First positions (index 1, -1) should start right at start_position, others should branch from there 137 | index = index + 1 if index < 0 else index - 1 138 | print('start_position: %s ' % start_position) 139 | if index > 0: 140 | return math.toNearest(start_position + L[index - 1] * 0.5, self.instrument['tickSize']) 141 | if index < 0: 142 | return math.toNearest(start_position - L[abs(index) - 1] * 0.5, self.instrument['tickSize']) 143 | if index == 0: 144 | return math.toNearest(start_position, self.instrument['tickSize']) 145 | 146 | def place_orders(self): 147 | """Create order items for use in convergence.""" 148 | buy_orders = [] 149 | sell_orders = [] 150 | order_status = 0 151 | """order_status参数说明 152 | 0: running_qty为0, 维持原样 153 | 1: self.running_qty > 0, 买卖都变化, 买单按照offset2, 卖单按照offset3 154 | 2: 买单维持不变, 卖单按照offset3 155 | 3: self.running_qty < 0, 买卖都变化, 买单按照offset3, 卖单按照offset2 156 | 4: 卖单维持不变, 买单按照offset3 157 | """ 158 | # Create orders from the outside in. This is intentional - let's say the inner order gets taken; 159 | # then we match orders from the outside in, ensuring the fewest number of orders are amended and only 160 | # a new order is created in the inside. If we did it inside-out, all orders would be amended 161 | # down and a new order would be created at the outside. 162 | position_grade = self.get_position_grade() 163 | print ('position_grade: %s ' % position_grade) 164 | print ('running_qty: %s ' % self.running_qty) 165 | schedule.run_pending() 166 | if (abs(self.last_running_qty) > abs(self.running_qty) and self.running_qty > settings.ORDER_START_SIZE): 167 | if (self.cycleclock == 30 // settings.LOOP_INTERVAL): 168 | self.send_tg_message() 169 | self.cycleclock = self.cycleclock - 1 170 | print('Countdown: %s' % self.cycleclock) 171 | if (self.cycleclock == 0): 172 | self.cycleclock = 30 // settings.LOOP_INTERVAL 173 | else: 174 | return 175 | wave_coefficient = self.get_wave_coefficient() 176 | if(self.running_qty == 0 and wave_coefficient < 8): 177 | for i in reversed(range(1, settings.ORDER_PAIRS + 1)): 178 | if not self.long_position_limit_exceeded(): 179 | buy_orders.append(self.prepare_order(-i, order_status)) 180 | if not self.short_position_limit_exceeded(): 181 | sell_orders.append(self.prepare_order(i, order_status)) 182 | elif(self.running_qty == 0): 183 | if (len(self.exchange.get_orders()) != 0): 184 | self.exchange.cancel_all_orders() 185 | self.send_tg_message() 186 | print('wave_coefficient is over 5, Suspend trading!') 187 | return 188 | elif(self.running_qty > 0): 189 | if (self.running_qty == self.last_running_qty): #持仓不变 190 | return 191 | elif (self.running_qty > self.last_running_qty and self.last_running_qty >= 0): #多仓增加,买单不变,卖单变化offset3 192 | order_status = 2 193 | for i in reversed(range(1, (self.running_qty - 1) // settings.ORDER_START_SIZE + 2)): 194 | if not self.short_position_limit_exceeded(): 195 | sell_orders.append(self.prepare_order(i, order_status)) 196 | elif (self.running_qty < self.last_running_qty and self.last_running_qty >= 0): #多仓减少,卖单不变,买单变化offset2 197 | order_status = 4 198 | for i in reversed(range(self.running_qty // settings.ORDER_START_SIZE + 1, settings.ORDER_PAIRS + 1)): 199 | if not self.long_position_limit_exceeded(): 200 | buy_orders.append(self.prepare_order(-i, order_status)) 201 | elif (self.last_running_qty < 0): #空转多,买卖单都变化,买offset2卖offset3 202 | order_status = 1 203 | for i in reversed(range(self.running_qty // settings.ORDER_START_SIZE + 1, settings.ORDER_PAIRS + 1)): 204 | if not self.long_position_limit_exceeded(): 205 | buy_orders.append(self.prepare_order(-i, order_status)) 206 | for i in reversed(range(1, (self.running_qty - 1) // settings.ORDER_START_SIZE + 2)): 207 | if not self.short_position_limit_exceeded(): 208 | sell_orders.append(self.prepare_order(i, order_status)) 209 | else: 210 | logger.error('running_qty bug. running_qty: %s last_running_qty: %s' % (self.running_qty, self.last_running_qty)) 211 | self.exit() 212 | else: 213 | if (self.running_qty == self.last_running_qty): #持仓不变 214 | return 215 | elif (abs(self.running_qty) > abs(self.last_running_qty) and self.last_running_qty <= 0): #空仓增加,买单变化offset3,卖单不变 216 | order_status = 4 217 | for i in reversed(range(1, (abs(self.running_qty) - 1) // settings.ORDER_START_SIZE + 2)): 218 | if not self.long_position_limit_exceeded(): 219 | buy_orders.append(self.prepare_order(-i, order_status)) 220 | elif (abs(self.running_qty) < abs(self.last_running_qty) and self.last_running_qty <= 0): #空仓减少,卖单变化offset2,买单不变 221 | order_status = 2 222 | for i in reversed(range(abs(self.running_qty) // settings.ORDER_START_SIZE + 1, settings.ORDER_PAIRS + 1)): 223 | if not self.short_position_limit_exceeded(): 224 | sell_orders.append(self.prepare_order(i, order_status)) 225 | elif (self.last_running_qty > 0): #多转空,买卖单都变化,买offset3卖offset2 226 | order_status = 3 227 | for i in reversed(range(1, (abs(self.running_qty) - 1) // settings.ORDER_START_SIZE + 2)): 228 | if not self.long_position_limit_exceeded(): 229 | buy_orders.append(self.prepare_order(-i, order_status)) 230 | for i in reversed(range(abs(self.running_qty) // settings.ORDER_START_SIZE + 1, settings.ORDER_PAIRS + 1)): 231 | if not self.short_position_limit_exceeded(): 232 | sell_orders.append(self.prepare_order(i, order_status)) 233 | else: 234 | logger.error('running_qty bug. running_qty: %s last_running_qty: %s' % (self.running_qty, self.last_running_qty)) 235 | self.exit() 236 | self.last_running_qty = self.running_qty 237 | print(buy_orders) 238 | print(sell_orders) 239 | return self.converge_orders(buy_orders, sell_orders, order_status) 240 | 241 | def prepare_order(self, index, order_status): 242 | """Create an order object.""" 243 | 244 | if settings.RANDOM_ORDER_SIZE is True: 245 | quantity = random.randint(settings.MIN_ORDER_SIZE, settings.MAX_ORDER_SIZE) 246 | else: 247 | quantity = settings.ORDER_START_SIZE + ((abs(index) - 1) * settings.ORDER_STEP_SIZE) 248 | if(index == 1 or index == -1): 249 | if ((self.running_qty > 0 and order_status == 4) or (self.running_qty < 0 and order_status == 2)): #多仓部分减少或空仓部分减少 250 | quantity = quantity - (abs(self.running_qty) % settings.ORDER_START_SIZE) 251 | else: 252 | quantity = quantity + (abs(self.running_qty) % settings.ORDER_START_SIZE) #仓位化整 253 | if((order_status == 0) or (order_status == 1 and index < 0) or (order_status == 3 and index > 0) or (order_status == 2 and self.running_qty < 0) or (order_status == 4 and self.running_qty > 0)): 254 | price = self.get_price_offset2(index) 255 | elif((order_status == 1 and index > 0) or (order_status == 3 and index < 0) or (order_status == 2 and self.running_qty > 0) or (order_status == 4 and self.running_qty < 0)): 256 | price = self.get_price_offset3(index) 257 | else: 258 | logger.error('Choose offset Error. order_status:%s index:%s self.running_qty:%s' % (order_status, index, self.running_qty)) 259 | self.exit() 260 | return {'price': price, 'orderQty': quantity, 'side': "Buy" if index < 0 else "Sell"} 261 | 262 | def converge_orders(self, buy_orders, sell_orders, order_status): 263 | """Converge the orders we currently have in the book with what we want to be in the book. 264 | This involves amending any open orders and creating new ones if any have filled completely. 265 | We start from the closest orders outward.""" 266 | 267 | tickLog = self.exchange.get_instrument()['tickLog'] 268 | to_amend = [] 269 | to_create = [] 270 | to_cancel = [] 271 | buys_matched = 0 272 | sells_matched = 0 273 | existing_orders = self.exchange.get_orders() 274 | 275 | # Check all existing orders and match them up with what we want to place. 276 | # If there's an open one, we might be able to amend it to fit what we want. 277 | for order in existing_orders: 278 | try: 279 | if (order['side'] == 'Buy' and (order_status == 0 or order_status == 4 or order_status == 3 or order_status == 1)): 280 | desired_order = buy_orders[buys_matched] 281 | buys_matched += 1 282 | elif (order['side'] == 'Sell' and (order_status == 0 or order_status == 2 or order_status == 1 or order_status == 3)): 283 | desired_order = sell_orders[sells_matched] 284 | sells_matched += 1 285 | else: 286 | continue 287 | 288 | # Found an existing order. Do we need to amend it? 289 | if desired_order['orderQty'] != order['leavesQty'] or ( 290 | # If price has changed, and the change is more than our RELIST_INTERVAL, amend. 291 | desired_order['price'] != order['price'] and 292 | abs((desired_order['price'] / order['price']) - 1) > settings.RELIST_INTERVAL): 293 | to_amend.append({'orderID': order['orderID'], 'orderQty': order['cumQty'] + desired_order['orderQty'], 294 | 'price': desired_order['price'], 'side': order['side']}) 295 | except IndexError: 296 | # Will throw if there isn't a desired order to match. In that case, cancel it. 297 | if ((order_status == 2 and self.running_qty > 0) or (order_status == 1 and self.running_qty > 0) or (order_status == 4 and self.running_qty < 0) or (order_status == 3 and self.running_qty < 0)): 298 | to_cancel.append(order) 299 | 300 | if (order_status == 0 or order_status == 4 or order_status == 3 or order_status == 1): 301 | while buys_matched < len(buy_orders): 302 | to_create.append(buy_orders[buys_matched]) 303 | buys_matched += 1 304 | if (order_status == 0 or order_status == 2 or order_status == 1 or order_status == 3): 305 | while sells_matched < len(sell_orders): 306 | to_create.append(sell_orders[sells_matched]) 307 | sells_matched += 1 308 | 309 | if len(to_amend) > 0: 310 | for amended_order in reversed(to_amend): 311 | reference_order = [o for o in existing_orders if o['orderID'] == amended_order['orderID']][0] 312 | logger.info("Amending %4s: %d @ %.*f to %d @ %.*f (%+.*f)" % ( 313 | amended_order['side'], 314 | reference_order['leavesQty'], tickLog, reference_order['price'], 315 | (amended_order['orderQty'] - reference_order['cumQty']), tickLog, amended_order['price'], 316 | tickLog, (amended_order['price'] - reference_order['price']) 317 | )) 318 | # This can fail if an order has closed in the time we were processing. 319 | # The API will send us `invalid ordStatus`, which means that the order's status (Filled/Canceled) 320 | # made it not amendable. 321 | # If that happens, we need to catch it and re-tick. 322 | try: 323 | self.exchange.amend_bulk_orders(to_amend) 324 | except requests.exceptions.HTTPError as e: 325 | errorObj = e.response.json() 326 | if errorObj['error']['message'] == 'Invalid ordStatus': 327 | logger.warn("Amending failed. Waiting for order data to converge and retrying.") 328 | sleep(0.5) 329 | return self.place_orders() 330 | else: 331 | logger.error("Unknown error on amend: %s. Exiting" % errorObj) 332 | sys.exit(1) 333 | 334 | if len(to_create) > 0: 335 | logger.info("Creating %d orders:" % (len(to_create))) 336 | for order in reversed(to_create): 337 | logger.info("%4s %d @ %.*f" % (order['side'], order['orderQty'], tickLog, order['price'])) 338 | self.exchange.create_bulk_orders(to_create) 339 | 340 | # Could happen if we exceed a delta limit 341 | if len(to_cancel) > 0: 342 | logger.info("Canceling %d orders:" % (len(to_cancel))) 343 | for order in reversed(to_cancel): 344 | logger.info("%4s %d @ %.*f" % (order['side'], order['leavesQty'], tickLog, order['price'])) 345 | self.exchange.cancel_bulk_orders(to_cancel) 346 | 347 | if ((len(to_amend) > 0) or (len(to_create) > 0) or (len(to_cancel) > 0)): 348 | self.send_tg_message() 349 | 350 | def send_tg_message(self): 351 | now = datetime.datetime.now() 352 | mybalance = '%.6f' % XBt_to_XBT(self.start_XBt) 353 | message = 'BitMEX交易状态\n' + \ 354 | '时间:' + now.astimezone(datetime.timezone(datetime.timedelta(hours=8))).strftime('%Y-%m-%d %H:%M:%S') + '\n' + \ 355 | '保证金余额:' + mybalance + '\n' + \ 356 | '合约数量:' + str(self.running_qty) + '\n' + \ 357 | '开仓价格:' + str(self.exchange.get_position()['avgCostPrice']) + '\n' + \ 358 | '风险等级:' + str(self.position_grade) + '\n' + \ 359 | '最新价格:' + str(self.get_ticker()['last']) + '\n' + \ 360 | '指数价格:' + str(self.exchange.get_portfolio()['XBTUSD']['markPrice']) + '\n' + \ 361 | '今日盈利:' + '%.6f' % (float(mybalance) - self.yesterday_balance) + '\n' + \ 362 | '作日盈利:' + '%.6f' % (self.yesterday_balance - self.before_yesterday_balance) 363 | tg_send_message(message) 364 | if self.position_grade > 4: 365 | tg_send_important_message(message) 366 | 367 | def exit(self): 368 | logger.info("Shutting down. All open orders will be cancelled.") 369 | now = datetime.datetime.now() 370 | message = 'BitMEX交易机器人异常退出\n' + \ 371 | '时间:' + now.astimezone(datetime.timezone(datetime.timedelta(hours=8))).strftime('%Y-%m-%d %H:%M:%S') + '\n' + \ 372 | '合约数量:' + str(self.running_qty) + '\n' + \ 373 | '开仓价格:' + str(self.exchange.get_position()['avgCostPrice']) + '\n' + \ 374 | '风险等级:' + str(self.position_grade) + '\n' + \ 375 | '最新价格:' + str(self.get_ticker()['last']) + '\n' + \ 376 | '指数价格:' + str(self.exchange.get_portfolio()['XBTUSD']['markPrice']) 377 | tg_send_important_message(message) 378 | try: 379 | self.exchange.cancel_all_orders() 380 | self.exchange.bitmex.exit() 381 | except errors.AuthenticationError as e: 382 | logger.info("Was not authenticated; could not cancel orders.") 383 | except Exception as e: 384 | logger.info("Unable to cancel orders: %s" % e) 385 | 386 | sys.exit() 387 | 388 | 389 | def run() -> None: 390 | order_manager = CustomOrderManager() 391 | 392 | # Try/except just keeps ctrl-c from printing an ugly stacktrace 393 | try: 394 | order_manager.run_loop() 395 | except (KeyboardInterrupt, SystemExit): 396 | sys.exit() 397 | -------------------------------------------------------------------------------- /custom_strategy_V2.1.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import sys 3 | from os.path import getmtime 4 | import logging 5 | import requests 6 | from time import sleep 7 | import datetime 8 | import schedule 9 | import re 10 | 11 | from market_maker.market_maker import OrderManager, XBt_to_XBT 12 | from market_maker.settings import settings 13 | from market_maker.utils import log, constants, errors, math 14 | from telegram_msg import tg_send_message, tg_send_important_message 15 | 16 | # Used for reloading the bot - saves modified times of key files 17 | import os 18 | watched_files_mtimes = [(f, getmtime(f)) for f in settings.WATCHED_FILES] 19 | 20 | 21 | # 22 | # Helpers 23 | # 24 | logger = logging.getLogger('root') 25 | 26 | class CustomOrderManager(OrderManager): 27 | 28 | def reset(self): 29 | self.exchange.cancel_all_orders() 30 | self.sanity_check() 31 | self.print_status() 32 | self.position_grade = 0 33 | self.last_running_qty = 0 34 | self.reset = True #设置初始化标记, 买卖单都变化 35 | self.cycleclock = 30 // settings.LOOP_INTERVAL 36 | #仓位等级由0-6级, 按持仓量分级, 每大于order size增加1级, 最高6级 37 | #持仓方向通过self.running_qty来判断, 大于0为多仓, 小于0为空仓 38 | schedule.every().day.at("00:00").do(self.write_mybalance) #每天00:00执行一次 39 | schedule.every(5).seconds.do(self.set_MarkPriceList) #每5秒执行一次 40 | self.MarkPriceList = [] 41 | marketPrice = self.exchange.get_portfolio()['XBTUSD']['markPrice'] 42 | for x in range(120): 43 | self.MarkPriceList.append(marketPrice) 44 | # Create orders and converge. 45 | with open(r'/root/mybalance.txt', 'r') as f: 46 | lines = f.readlines() 47 | m1 = re.match(r'(\d{4}-\d{2}-\d{2})\s(\d{2}\:\d{2}\:\d{2})\s+([0-9\.]+)', lines[-1]) 48 | self.yesterday_balance = float(m1.group(3)) 49 | m2 = re.match(r'(\d{4}-\d{2}-\d{2})\s(\d{2}\:\d{2}\:\d{2})\s+([0-9\.]+)', lines[-2]) 50 | self.before_yesterday_balance = float(m2.group(3)) 51 | self.place_orders() 52 | 53 | def write_mybalance(self): 54 | now = datetime.datetime.now() 55 | mybalance = '%.6f' % XBt_to_XBT(self.start_XBt) 56 | with open(r'/root/mybalance.txt', 'a') as f: 57 | f.write(now.strftime('%Y-%m-%d %H:%M:%S') + ' ' + mybalance + '\n') 58 | message = 'BitMEX今日交易统计\n' + \ 59 | '时间:' + now.strftime('%Y-%m-%d %H:%M:%S') + '\n' + \ 60 | '保证金余额:' + mybalance + '\n' + \ 61 | '合约数量:' + str(self.running_qty) + '\n' + \ 62 | '开仓价格:' + str(self.exchange.get_position()['avgCostPrice']) + '\n' + \ 63 | '风险等级:' + str(self.position_grade) + '\n' + \ 64 | '最新价格:' + str(self.get_ticker()['last']) + '\n' + \ 65 | '指数价格:' + str(self.exchange.get_portfolio()['XBTUSD']['markPrice']) + '\n' + \ 66 | '今日盈利:' + '%.6f' % (float(mybalance) - self.yesterday_balance) + '\n' + \ 67 | '作日盈利:' + '%.6f' % (self.yesterday_balance - self.before_yesterday_balance) 68 | tg_send_important_message(message) 69 | self.before_yesterday_balance = self.yesterday_balance 70 | self.yesterday_balance = float(mybalance) 71 | 72 | def set_MarkPriceList(self): 73 | self.MarkPriceList.pop() 74 | self.MarkPriceList.insert(0, self.exchange.get_portfolio()['XBTUSD']['markPrice']) 75 | 76 | def get_wave_coefficient(self): 77 | """求波动系数, 当前市场波动系数, 超过一定值取消挂单""" 78 | return (max(self.MarkPriceList) - min(self.MarkPriceList)) 79 | 80 | def get_position_grade(self): 81 | """获取仓位等级""" 82 | 83 | self.position_grade = abs(self.running_qty) // settings.ORDER_START_SIZE 84 | if abs(self.running_qty) == settings.ORDER_START_SIZE: 85 | self.position_grade = 0 86 | elif self.position_grade > 6: 87 | self.position_grade = 6 88 | return self.position_grade 89 | 90 | def get_price_offset2(self, index): 91 | """根据index依次设置每一个价格,这里为差价依次增大,分别为0.5, 1, 2, 3, 5, 7, 11, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 105, 110, 115, 120, 125, 130, 135, 140, 145, 150, 155""" 92 | L = [0.5, 1, 2, 3, 5, 7, 11, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 105, 110, 115, 120, 125, 130, 135, 140, 145, 150, 155] 93 | if abs(index) > 37: 94 | logger.error("ORDER_PAIRS cannot over 10") 95 | self.exit() 96 | # Maintain existing spreads for max profit 97 | if settings.MAINTAIN_SPREADS: 98 | start_position = self.start_position_buy if index < 0 else self.start_position_sell 99 | # First positions (index 1, -1) should start right at start_position, others should branch from there 100 | index = index + 1 if index < 0 else index - 1 101 | else: 102 | # Offset mode: ticker comes from a reference exchange and we define an offset. 103 | start_position = self.start_position_buy if index < 0 else self.start_position_sell 104 | 105 | # If we're attempting to sell, but our sell price is actually lower than the buy, 106 | # move over to the sell side. 107 | if index > 0 and start_position < self.start_position_buy: 108 | start_position = self.start_position_sell 109 | # Same for buys. 110 | if index < 0 and start_position > self.start_position_sell: 111 | start_position = self.start_position_buy 112 | if (self.running_qty != 0): 113 | avgCostPrice = self.exchange.get_position()['avgCostPrice'] 114 | if (avgCostPrice % 1 == 0.5): 115 | start_position = avgCostPrice 116 | else: 117 | start_position = avgCostPrice - 0.25 if index < 0 else avgCostPrice + 0.25 118 | if index > 0: 119 | return math.toNearest(start_position + L[index - 1], self.instrument['tickSize']) 120 | if index < 0: 121 | return math.toNearest(start_position - L[abs(index) - 1], self.instrument['tickSize']) 122 | if index == 0: 123 | return math.toNearest(start_position, self.instrument['tickSize']) 124 | 125 | def get_price_offset3(self, index): 126 | """按仓位等级来设置价格, 每0.5设置一个价格""" 127 | avgCostPrice = self.exchange.get_position()['avgCostPrice'] 128 | if (avgCostPrice % 0.5 == 0): 129 | start_position = avgCostPrice 130 | else: 131 | start_position = avgCostPrice - 0.25 if index < 0 else avgCostPrice + 0.25 132 | if (index > 0 and start_position < self.start_position_sell): 133 | start_position = self.start_position_sell + settings.INTERVAL2 134 | elif (index < 0 and start_position > self.start_position_buy): 135 | start_position = self.start_position_buy - settings.INTERVAL2 136 | elif index > 0: 137 | start_position = start_position + settings.INTERVAL2 138 | elif index < 0: 139 | start_position = start_position - settings.INTERVAL2 140 | if settings.MAINTAIN_SPREADS: 141 | # First positions (index 1, -1) should start right at start_position, others should branch from there 142 | index = index + 1 if index < 0 else index - 1 143 | print('start_position: %s ' % start_position) 144 | if index > 0: 145 | return math.toNearest(start_position + index * 0.5, self.instrument['tickSize']) 146 | if index < 0: 147 | return math.toNearest(start_position - abs(index) * 0.5, self.instrument['tickSize']) 148 | if index == 0: 149 | return math.toNearest(start_position, self.instrument['tickSize']) 150 | 151 | def place_orders(self): 152 | """Create order items for use in convergence.""" 153 | buy_orders = [] 154 | sell_orders = [] 155 | order_status = 0 156 | """order_status参数说明 157 | 0: running_qty为0, 维持原样 158 | 1: self.running_qty > 0, 买卖都变化, 买单按照offset2, 卖单按照offset3 159 | 2: 买单维持不变, 卖单按照offset3 160 | 3: self.running_qty < 0, 买卖都变化, 买单按照offset3, 卖单按照offset2 161 | 4: 卖单维持不变, 买单按照offset3 162 | """ 163 | # Create orders from the outside in. This is intentional - let's say the inner order gets taken; 164 | # then we match orders from the outside in, ensuring the fewest number of orders are amended and only 165 | # a new order is created in the inside. If we did it inside-out, all orders would be amended 166 | # down and a new order would be created at the outside. 167 | position_grade = self.get_position_grade() 168 | print ('position_grade: %s ' % position_grade) 169 | print ('running_qty: %s ' % self.running_qty) 170 | schedule.run_pending() 171 | if (abs(self.last_running_qty) > abs(self.running_qty) and self.running_qty > settings.ORDER_START_SIZE): 172 | if (self.cycleclock == 30 // settings.LOOP_INTERVAL): 173 | self.send_tg_message() 174 | self.cycleclock = self.cycleclock - 1 175 | print('Countdown: %s' % self.cycleclock) 176 | if (self.cycleclock == 0): 177 | self.cycleclock = 30 // settings.LOOP_INTERVAL 178 | else: 179 | return 180 | wave_coefficient = self.get_wave_coefficient() 181 | if(self.running_qty == 0 and wave_coefficient < 8 and (self.last_running_qty != 0 or self.reset == True)): 182 | sleep(5) 183 | for i in reversed(range(1, 4 * (settings.ORDER_PAIRS - 1) + 1 + 1)): 184 | if not self.long_position_limit_exceeded(): 185 | buy_orders.append(self.prepare_order(-i, order_status)) 186 | if not self.short_position_limit_exceeded(): 187 | sell_orders.append(self.prepare_order(i, order_status)) 188 | elif(self.running_qty == 0 and self.last_running_qty != 0): 189 | if (len(self.exchange.get_orders()) != 0): 190 | self.exchange.cancel_all_orders() 191 | self.send_tg_message() 192 | print('wave_coefficient is over 8, Suspend trading!') 193 | return 194 | elif(self.running_qty == 0 and self.last_running_qty == 0): 195 | logger.info("Order has created.") 196 | return 197 | elif(self.running_qty > 0): 198 | cycles_sell = self.running_qty // (2 * settings.ORDER_START_SIZE) + 2 if self.running_qty <= 2 * settings.ORDER_START_SIZE else (self.running_qty - 2 * settings.ORDER_START_SIZE - 1) // (settings.ORDER_START_SIZE // 2) + 4 199 | cycles_buy = 1 if self.running_qty < settings.ORDER_START_SIZE else (self.running_qty - settings.ORDER_START_SIZE) // (settings.ORDER_START_SIZE // 4) + 2 200 | if (self.running_qty == self.last_running_qty): #持仓不变 201 | return 202 | elif (self.running_qty > self.last_running_qty and self.last_running_qty >= 0 and self.reset == False): #多仓增加,买单不变,卖单变化offset3 203 | order_status = 2 204 | for i in reversed(range(1, cycles_sell)): 205 | if not self.short_position_limit_exceeded(): 206 | sell_orders.append(self.prepare_order(i, order_status)) 207 | elif (self.running_qty < self.last_running_qty and self.last_running_qty >= 0 and self.reset == False): #多仓减少,卖单不变,买单变化offset2 208 | order_status = 4 209 | for i in reversed(range(cycles_buy, 4 * (settings.ORDER_PAIRS - 1) + 1 + 1)): 210 | if not self.long_position_limit_exceeded(): 211 | buy_orders.append(self.prepare_order(-i, order_status)) 212 | elif (self.last_running_qty < 0 or (self.last_running_qty == 0 and self.reset == True)): #空转多(或系统重开有仓位时),买卖单都变化,买offset2卖offset3 213 | order_status = 1 214 | for i in reversed(range(cycles_buy, 4 * (settings.ORDER_PAIRS - 1) + 1 + 1)): 215 | if not self.long_position_limit_exceeded(): 216 | buy_orders.append(self.prepare_order(-i, order_status)) 217 | for i in reversed(range(1, cycles_sell)): 218 | if not self.short_position_limit_exceeded(): 219 | sell_orders.append(self.prepare_order(i, order_status)) 220 | else: 221 | logger.error('running_qty bug. running_qty: %s last_running_qty: %s' % (self.running_qty, self.last_running_qty)) 222 | self.exit() 223 | else: 224 | cycles_buy = abs(self.running_qty) // (2 * settings.ORDER_START_SIZE) + 2 if abs(self.running_qty) <= 2 * settings.ORDER_START_SIZE else (abs(self.running_qty) - 2 * settings.ORDER_START_SIZE - 1) // (settings.ORDER_START_SIZE // 2) + 4 225 | cycles_sell = 1 if abs(self.running_qty) < settings.ORDER_START_SIZE else (abs(self.running_qty) - settings.ORDER_START_SIZE) // (settings.ORDER_START_SIZE // 4) + 2 226 | if (self.running_qty == self.last_running_qty): #持仓不变 227 | return 228 | elif (abs(self.running_qty) > abs(self.last_running_qty) and self.last_running_qty <= 0 and self.reset == False): #空仓增加,买单变化offset3,卖单不变 229 | order_status = 4 230 | for i in reversed(range(1, cycles_buy)): 231 | if not self.long_position_limit_exceeded(): 232 | buy_orders.append(self.prepare_order(-i, order_status)) 233 | elif (abs(self.running_qty) < abs(self.last_running_qty) and self.last_running_qty <= 0 and self.reset == False): #空仓减少,卖单变化offset2,买单不变 234 | order_status = 2 235 | for i in reversed(range(cycles_sell, 4 * (settings.ORDER_PAIRS - 1) + 1 + 1)): 236 | if not self.short_position_limit_exceeded(): 237 | sell_orders.append(self.prepare_order(i, order_status)) 238 | elif (self.last_running_qty > 0 or (self.last_running_qty == 0 and self.reset == True)): #多转空(或系统重开有仓位时),买卖单都变化,买offset3卖offset2 239 | order_status = 3 240 | for i in reversed(range(1, cycles_buy)): 241 | if not self.long_position_limit_exceeded(): 242 | buy_orders.append(self.prepare_order(-i, order_status)) 243 | for i in reversed(range(cycles_sell, 4 * (settings.ORDER_PAIRS - 1) + 1 + 1)): 244 | if not self.short_position_limit_exceeded(): 245 | sell_orders.append(self.prepare_order(i, order_status)) 246 | else: 247 | logger.error('running_qty bug. running_qty: %s last_running_qty: %s' % (self.running_qty, self.last_running_qty)) 248 | self.exit() 249 | self.last_running_qty = self.running_qty 250 | self.reset = False 251 | print(buy_orders) 252 | print(sell_orders) 253 | return self.converge_orders(buy_orders, sell_orders, order_status) 254 | 255 | def prepare_order(self, index, order_status): 256 | """Create an order object.""" 257 | 258 | if(index == 1 or index == -1): 259 | if (((self.running_qty > 0 and order_status == 4) or (self.running_qty < 0 and order_status == 2))) and (abs(self.running_qty) % settings.ORDER_START_SIZE) != 0: #多仓部分减少或空仓部分减少 260 | quantity = settings.ORDER_START_SIZE + (abs(self.running_qty) % settings.ORDER_START_SIZE) if settings.ORDER_START_SIZE < abs(self.running_qty) < 2 * settings.ORDER_START_SIZE else abs(self.running_qty) % settings.ORDER_START_SIZE 261 | elif((0 < self.running_qty < 2 * settings.ORDER_START_SIZE and (order_status == 2 or order_status == 1)) or (-2 * settings.ORDER_START_SIZE < self.running_qty < 0 and (order_status == 4 or order_status == 3))): 262 | quantity = settings.ORDER_START_SIZE + (abs(self.running_qty) % settings.ORDER_START_SIZE) #仓位化整 263 | elif((self.running_qty > 2 * settings.ORDER_START_SIZE and (order_status == 2 or order_status == 1)) or (self.running_qty < -2 * settings.ORDER_START_SIZE and (order_status == 4 or order_status == 3))) and (abs(self.running_qty) % (settings.ORDER_START_SIZE // 2)) != 0: 264 | quantity = settings.ORDER_START_SIZE - (settings.ORDER_START_SIZE // 2 - abs(self.running_qty) % (settings.ORDER_START_SIZE // 2)) 265 | else: 266 | quantity = settings.ORDER_START_SIZE 267 | elif((self.running_qty >= 2 * settings.ORDER_START_SIZE and index == 2) or (self.running_qty <= -2 * settings.ORDER_START_SIZE and index == -2)): 268 | quantity = settings.ORDER_START_SIZE 269 | elif((self.running_qty > 2 * settings.ORDER_START_SIZE and index > 2) or (self.running_qty < -2 * settings.ORDER_START_SIZE and index < -2)): 270 | quantity = settings.ORDER_START_SIZE / 2 271 | elif((self.running_qty <= 0 and index >= 2) or (self.running_qty >= 0 and index <= -2)): 272 | quantity = settings.ORDER_START_SIZE / 4 273 | else: 274 | logger.error('Choose quantity Error. index: %s running_qty: %s' % (index, self.running_qty)) 275 | self.exit() 276 | if((order_status == 0) or (order_status == 1 and index < 0) or (order_status == 3 and index > 0) or (order_status == 2 and self.running_qty < 0) or (order_status == 4 and self.running_qty > 0)): 277 | price = self.get_price_offset2(index) 278 | elif((order_status == 1 and index > 0) or (order_status == 3 and index < 0) or (order_status == 2 and self.running_qty > 0) or (order_status == 4 and self.running_qty < 0)): 279 | price = self.get_price_offset3(index) 280 | else: 281 | logger.error('Choose offset Error. order_status:%s index:%s self.running_qty:%s' % (order_status, index, self.running_qty)) 282 | self.exit() 283 | return {'price': price, 'orderQty': quantity, 'side': "Buy" if index < 0 else "Sell"} 284 | 285 | def converge_orders(self, buy_orders, sell_orders, order_status): 286 | """Converge the orders we currently have in the book with what we want to be in the book. 287 | This involves amending any open orders and creating new ones if any have filled completely. 288 | We start from the closest orders outward.""" 289 | 290 | tickLog = self.exchange.get_instrument()['tickLog'] 291 | to_amend = [] 292 | to_create = [] 293 | to_cancel = [] 294 | buys_matched = 0 295 | sells_matched = 0 296 | existing_orders = self.exchange.get_orders() 297 | 298 | # Check all existing orders and match them up with what we want to place. 299 | # If there's an open one, we might be able to amend it to fit what we want. 300 | for order in existing_orders: 301 | try: 302 | if (order['side'] == 'Buy' and (order_status == 0 or order_status == 4 or order_status == 3 or order_status == 1)): 303 | desired_order = buy_orders[buys_matched] 304 | buys_matched += 1 305 | elif (order['side'] == 'Sell' and (order_status == 0 or order_status == 2 or order_status == 1 or order_status == 3)): 306 | desired_order = sell_orders[sells_matched] 307 | sells_matched += 1 308 | else: 309 | continue 310 | 311 | # Found an existing order. Do we need to amend it? 312 | if desired_order['orderQty'] != order['leavesQty'] or ( 313 | # If price has changed, and the change is more than our RELIST_INTERVAL, amend. 314 | desired_order['price'] != order['price'] and 315 | abs((desired_order['price'] / order['price']) - 1) > settings.RELIST_INTERVAL): 316 | to_amend.append({'orderID': order['orderID'], 'orderQty': order['cumQty'] + desired_order['orderQty'], 317 | 'price': desired_order['price'], 'side': order['side']}) 318 | except IndexError: 319 | # Will throw if there isn't a desired order to match. In that case, cancel it. 320 | if ((order_status == 2 and self.running_qty > 0) or (order_status == 1 and self.running_qty > 0) or (order_status == 4 and self.running_qty < 0) or (order_status == 3 and self.running_qty < 0)): 321 | to_cancel.append(order) 322 | 323 | if (order_status == 0 or order_status == 4 or order_status == 3 or order_status == 1): 324 | while buys_matched < len(buy_orders): 325 | to_create.append(buy_orders[buys_matched]) 326 | buys_matched += 1 327 | if (order_status == 0 or order_status == 2 or order_status == 1 or order_status == 3): 328 | while sells_matched < len(sell_orders): 329 | to_create.append(sell_orders[sells_matched]) 330 | sells_matched += 1 331 | 332 | if len(to_amend) > 0: 333 | for amended_order in reversed(to_amend): 334 | reference_order = [o for o in existing_orders if o['orderID'] == amended_order['orderID']][0] 335 | logger.info("Amending %4s: %d @ %.*f to %d @ %.*f (%+.*f)" % ( 336 | amended_order['side'], 337 | reference_order['leavesQty'], tickLog, reference_order['price'], 338 | (amended_order['orderQty'] - reference_order['cumQty']), tickLog, amended_order['price'], 339 | tickLog, (amended_order['price'] - reference_order['price']) 340 | )) 341 | # This can fail if an order has closed in the time we were processing. 342 | # The API will send us `invalid ordStatus`, which means that the order's status (Filled/Canceled) 343 | # made it not amendable. 344 | # If that happens, we need to catch it and re-tick. 345 | try: 346 | self.exchange.amend_bulk_orders(to_amend) 347 | except requests.exceptions.HTTPError as e: 348 | errorObj = e.response.json() 349 | if errorObj['error']['message'] == 'Invalid ordStatus': 350 | logger.warn("Amending failed. Waiting for order data to converge and retrying.") 351 | sleep(0.5) 352 | return self.place_orders() 353 | else: 354 | logger.error("Unknown error on amend: %s. Exiting" % errorObj) 355 | sys.exit(1) 356 | 357 | if len(to_create) > 0: 358 | logger.info("Creating %d orders:" % (len(to_create))) 359 | for order in reversed(to_create): 360 | logger.info("%4s %d @ %.*f" % (order['side'], order['orderQty'], tickLog, order['price'])) 361 | self.exchange.create_bulk_orders(to_create) 362 | 363 | # Could happen if we exceed a delta limit 364 | if len(to_cancel) > 0: 365 | logger.info("Canceling %d orders:" % (len(to_cancel))) 366 | for order in reversed(to_cancel): 367 | logger.info("%4s %d @ %.*f" % (order['side'], order['leavesQty'], tickLog, order['price'])) 368 | self.exchange.cancel_bulk_orders(to_cancel) 369 | 370 | if ((len(to_amend) > 0) or (len(to_create) > 0) or (len(to_cancel) > 0)): 371 | self.send_tg_message() 372 | 373 | def send_tg_message(self): 374 | now = datetime.datetime.now() 375 | mybalance = '%.6f' % XBt_to_XBT(self.start_XBt) 376 | message = 'BitMEX交易状态\n' + \ 377 | '时间:' + now.astimezone(datetime.timezone(datetime.timedelta(hours=8))).strftime('%Y-%m-%d %H:%M:%S') + '\n' + \ 378 | '保证金余额:' + mybalance + '\n' + \ 379 | '合约数量:' + str(self.running_qty) + '\n' + \ 380 | '开仓价格:' + str(self.exchange.get_position()['avgCostPrice']) + '\n' + \ 381 | '风险等级:' + str(self.position_grade) + '\n' + \ 382 | '最新价格:' + str(self.get_ticker()['last']) + '\n' + \ 383 | '指数价格:' + str(self.exchange.get_portfolio()['XBTUSD']['markPrice']) + '\n' + \ 384 | '今日盈利:' + '%.6f' % (float(mybalance) - self.yesterday_balance) + '\n' + \ 385 | '作日盈利:' + '%.6f' % (self.yesterday_balance - self.before_yesterday_balance) 386 | tg_send_message(message) 387 | if self.position_grade > 3: 388 | tg_send_important_message(message) 389 | 390 | def exit(self): 391 | logger.info("Shutting down. All open orders will be cancelled.") 392 | now = datetime.datetime.now() 393 | message = 'BitMEX交易机器人异常退出\n' + \ 394 | '时间:' + now.astimezone(datetime.timezone(datetime.timedelta(hours=8))).strftime('%Y-%m-%d %H:%M:%S') + '\n' + \ 395 | '合约数量:' + str(self.running_qty) + '\n' + \ 396 | '开仓价格:' + str(self.exchange.get_position()['avgCostPrice']) + '\n' + \ 397 | '风险等级:' + str(self.position_grade) + '\n' + \ 398 | '最新价格:' + str(self.get_ticker()['last']) + '\n' + \ 399 | '指数价格:' + str(self.exchange.get_portfolio()['XBTUSD']['markPrice']) 400 | tg_send_important_message(message) 401 | try: 402 | self.exchange.cancel_all_orders() 403 | self.exchange.bitmex.exit() 404 | except errors.AuthenticationError as e: 405 | logger.info("Was not authenticated; could not cancel orders.") 406 | except Exception as e: 407 | logger.info("Unable to cancel orders: %s" % e) 408 | 409 | sys.exit() 410 | 411 | 412 | def run() -> None: 413 | order_manager = CustomOrderManager() 414 | 415 | # Try/except just keeps ctrl-c from printing an ugly stacktrace 416 | try: 417 | order_manager.run_loop() 418 | except (KeyboardInterrupt, SystemExit): 419 | sys.exit() 420 | -------------------------------------------------------------------------------- /custom_strategy_V3.0.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import sys 3 | from os.path import getmtime 4 | import logging 5 | import requests 6 | from time import sleep 7 | import datetime 8 | import schedule 9 | import re 10 | 11 | from market_maker.market_maker import OrderManager, XBt_to_XBT 12 | from market_maker.settings import settings 13 | from market_maker.utils import log, constants, errors, math 14 | from telegram_msg import tg_send_message, tg_send_important_message 15 | 16 | # Used for reloading the bot - saves modified times of key files 17 | import os 18 | watched_files_mtimes = [(f, getmtime(f)) for f in settings.WATCHED_FILES] 19 | 20 | 21 | # 22 | # Helpers 23 | # 24 | logger = logging.getLogger('root') 25 | 26 | class CustomOrderManager(OrderManager): 27 | 28 | def reset(self): 29 | self.exchange.cancel_all_orders() 30 | self.sanity_check() 31 | self.print_status() 32 | self.position_grade = 0 33 | self.last_running_qty = 0 34 | self.reset = True #设置初始化标记, 买卖单都变化 35 | self.restart = False #设置再循环标记, 只有True时才可以重新建仓, 否则等待 36 | self.pin_buy_orders = [] 37 | self.pin_sell_orders = [] 38 | self.last10price_flag = False 39 | self.last10price_countdown = 60 40 | #计算插针建仓倒数, 超过60秒撤销挂单 41 | self.cycleclock = 30 // settings.LOOP_INTERVAL 42 | #仓位等级由0-6级, 按持仓量分级, 每大于order size增加1级, 最高6级 43 | #持仓方向通过self.running_qty来判断, 大于0为多仓, 小于0为空仓 44 | schedule.every().day.at("00:00").do(self.write_mybalance) #每天00:00执行一次 45 | schedule.every(5).seconds.do(self.set_MarkPriceList) #每5秒执行一次 46 | schedule.every().second.do(self.set_Last10PriceList) #每1秒执行一次 47 | self.MarkPriceList = [] 48 | marketPrice = self.exchange.get_portfolio()['XBTUSD']['markPrice'] 49 | self.LastPriceList10second = [] 50 | lastPrice = self.get_ticker()['last'] 51 | for x in range(120): 52 | self.MarkPriceList.append(marketPrice) 53 | for x in range(10): 54 | self.LastPriceList10second.append(lastPrice) 55 | # Create orders and converge. 56 | with open(r'/root/mybalance.txt', 'r') as f: 57 | lines = f.readlines() 58 | m1 = re.match(r'(\d{4}-\d{2}-\d{2})\s(\d{2}\:\d{2}\:\d{2})\s+([0-9\.]+)', lines[-1]) 59 | self.yesterday_balance = float(m1.group(3)) 60 | m2 = re.match(r'(\d{4}-\d{2}-\d{2})\s(\d{2}\:\d{2}\:\d{2})\s+([0-9\.]+)', lines[-2]) 61 | self.before_yesterday_balance = float(m2.group(3)) 62 | self.place_orders() 63 | 64 | def write_mybalance(self): 65 | now = datetime.datetime.now() 66 | mybalance = '%.6f' % XBt_to_XBT(self.start_XBt) 67 | with open(r'/root/mybalance.txt', 'a') as f: 68 | f.write(now.strftime('%Y-%m-%d %H:%M:%S') + ' ' + mybalance + '\n') 69 | message = 'BitMEX今日交易统计\n' + \ 70 | '时间:' + now.strftime('%Y-%m-%d %H:%M:%S') + '\n' + \ 71 | '保证金余额:' + mybalance + '\n' + \ 72 | '合约数量:' + str(self.running_qty) + '\n' + \ 73 | '开仓价格:' + str(self.exchange.get_position()['avgCostPrice']) + '\n' + \ 74 | '风险等级:' + str(self.position_grade) + '\n' + \ 75 | '最新价格:' + str(self.get_ticker()['last']) + '\n' + \ 76 | '指数价格:' + str(self.exchange.get_portfolio()['XBTUSD']['markPrice']) + '\n' + \ 77 | '今日盈利:' + '%.6f' % (float(mybalance) - self.yesterday_balance) + '\n' + \ 78 | '作日盈利:' + '%.6f' % (self.yesterday_balance - self.before_yesterday_balance) 79 | tg_send_important_message(message) 80 | self.before_yesterday_balance = self.yesterday_balance 81 | self.yesterday_balance = float(mybalance) 82 | 83 | def set_MarkPriceList(self): 84 | self.MarkPriceList.pop() 85 | self.MarkPriceList.insert(0, self.exchange.get_portfolio()['XBTUSD']['markPrice']) 86 | 87 | def set_Last10PriceList(self): 88 | if (self.last10price_flag == True): 89 | self.last10price_countdown = self.last10price_countdown - 1 90 | self.LastPriceList10second.pop() 91 | self.LastPriceList10second.insert(0, self.get_ticker()['last']) 92 | 93 | def get_wave_coefficient(self): 94 | """求波动系数, 当前市场波动系数, 超过一定值取消挂单""" 95 | return (max(self.MarkPriceList) - min(self.MarkPriceList)) 96 | 97 | def get_wave_coefficient_last10price(self): 98 | """求10秒内最新价波动系数, 正数为上涨, 负数为下跌, 超过一定值插针挂单""" 99 | if ((sum(self.LastPriceList10second[0:5]) - sum(self.LastPriceList10second[5:10])) > 30 ): 100 | return (max(self.LastPriceList10second) - min(self.LastPriceList10second)) 101 | elif ((sum(self.LastPriceList10second[0:5]) - sum(self.LastPriceList10second[5:10])) < 30 ): 102 | return (min(self.LastPriceList10second) - max(self.LastPriceList10second)) 103 | else: 104 | return 0 105 | 106 | def get_position_grade(self): 107 | """获取仓位等级""" 108 | 109 | self.position_grade = abs(self.running_qty) // settings.ORDER_START_SIZE 110 | if abs(self.running_qty) == settings.ORDER_START_SIZE: 111 | self.position_grade = 0 112 | elif self.position_grade > 6: 113 | self.position_grade = 6 114 | return self.position_grade 115 | 116 | def get_price_offset2(self, index): 117 | """根据index依次设置每一个价格,这里为差价依次增大,分别为0.5, 1, 2, 3, 5, 7, 11, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 105, 110, 115, 120, 125, 130, 135, 140, 145, 150, 155""" 118 | L = [0.5, 1, 2, 3, 5, 7, 11, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 105, 110, 115, 120, 125, 130, 135, 140, 145, 150, 155] 119 | if abs(index) > 37: 120 | logger.error("ORDER_PAIRS cannot over 10") 121 | self.exit() 122 | # Maintain existing spreads for max profit 123 | if settings.MAINTAIN_SPREADS: 124 | start_position = self.start_position_buy if index < 0 else self.start_position_sell 125 | # First positions (index 1, -1) should start right at start_position, others should branch from there 126 | index = index + 1 if index < 0 else index - 1 127 | else: 128 | # Offset mode: ticker comes from a reference exchange and we define an offset. 129 | start_position = self.start_position_buy if index < 0 else self.start_position_sell 130 | 131 | # If we're attempting to sell, but our sell price is actually lower than the buy, 132 | # move over to the sell side. 133 | if index > 0 and start_position < self.start_position_buy: 134 | start_position = self.start_position_sell 135 | # Same for buys. 136 | if index < 0 and start_position > self.start_position_sell: 137 | start_position = self.start_position_buy 138 | if (self.running_qty != 0): 139 | avgCostPrice = self.exchange.get_position()['avgCostPrice'] 140 | if (avgCostPrice % 1 == 0.5): 141 | start_position = avgCostPrice 142 | else: 143 | start_position = avgCostPrice - 0.25 if index < 0 else avgCostPrice + 0.25 144 | if index > 0: 145 | return math.toNearest(start_position + L[index - 1], self.instrument['tickSize']) 146 | if index < 0: 147 | return math.toNearest(start_position - L[abs(index) - 1], self.instrument['tickSize']) 148 | if index == 0: 149 | return math.toNearest(start_position, self.instrument['tickSize']) 150 | 151 | def get_price_offset3(self, index): 152 | """按仓位等级来设置价格, 每0.5设置一个价格""" 153 | avgCostPrice = self.exchange.get_position()['avgCostPrice'] 154 | if (avgCostPrice % 0.5 == 0): 155 | start_position = avgCostPrice 156 | else: 157 | start_position = avgCostPrice - 0.25 if index < 0 else avgCostPrice + 0.25 158 | if (index > 0 and start_position < self.start_position_sell): 159 | start_position = self.start_position_sell + settings.INTERVAL2 160 | elif (index < 0 and start_position > self.start_position_buy): 161 | start_position = self.start_position_buy - settings.INTERVAL2 162 | elif index > 0: 163 | start_position = start_position + settings.INTERVAL2 164 | elif index < 0: 165 | start_position = start_position - settings.INTERVAL2 166 | if settings.MAINTAIN_SPREADS: 167 | # First positions (index 1, -1) should start right at start_position, others should branch from there 168 | index = index + 1 if index < 0 else index - 1 169 | print('start_position: %s ' % start_position) 170 | if index > 0: 171 | return math.toNearest(start_position + index * 0.5, self.instrument['tickSize']) 172 | if index < 0: 173 | return math.toNearest(start_position - abs(index) * 0.5, self.instrument['tickSize']) 174 | if index == 0: 175 | return math.toNearest(start_position, self.instrument['tickSize']) 176 | 177 | def place_orders(self): 178 | """Create order items for use in convergence.""" 179 | buy_orders = [] 180 | sell_orders = [] 181 | order_status = 0 182 | """order_status参数说明 183 | 0: running_qty为0, 维持原样 184 | 1: self.running_qty > 0, 买卖都变化, 买单按照offset2, 卖单按照offset3 185 | 2: 买单维持不变, 卖单按照offset3 186 | 3: self.running_qty < 0, 买卖都变化, 买单按照offset3, 卖单按照offset2 187 | 4: 卖单维持不变, 买单按照offset3 188 | 5: 追加指定订单 189 | 6: 取消指定订单 190 | """ 191 | # Create orders from the outside in. This is intentional - let's say the inner order gets taken; 192 | # then we match orders from the outside in, ensuring the fewest number of orders are amended and only 193 | # a new order is created in the inside. If we did it inside-out, all orders would be amended 194 | # down and a new order would be created at the outside. 195 | position_grade = self.get_position_grade() 196 | print ('position_grade: %s ' % position_grade) 197 | print ('running_qty: %s ' % self.running_qty) 198 | schedule.run_pending() 199 | if (abs(self.last_running_qty) > abs(self.running_qty) and self.running_qty > settings.ORDER_START_SIZE): 200 | if (self.cycleclock == 30 // settings.LOOP_INTERVAL): 201 | self.send_tg_message() 202 | self.cycleclock = self.cycleclock - 1 203 | print('Countdown: %s' % self.cycleclock) 204 | if (self.cycleclock == 0): 205 | self.cycleclock = 30 // settings.LOOP_INTERVAL 206 | else: 207 | return 208 | wave_coefficient = self.get_wave_coefficient() 209 | if(self.running_qty == 0 and wave_coefficient < 8 and (self.last_running_qty != 0 or self.reset == True)): 210 | if(self.restart == False): 211 | sleep(10) 212 | self.restart = True 213 | return 214 | self.restart = False 215 | for i in reversed(range(1, 4 * (settings.ORDER_PAIRS - 1) + 3 + 1)): 216 | if not self.long_position_limit_exceeded(): 217 | buy_orders.append(self.prepare_order(-i, order_status)) 218 | if not self.short_position_limit_exceeded(): 219 | sell_orders.append(self.prepare_order(i, order_status)) 220 | elif(self.running_qty == 0 and self.last_running_qty != 0): 221 | if (len(self.exchange.get_orders()) != 0): 222 | self.exchange.cancel_all_orders() 223 | self.send_tg_message() 224 | print('wave_coefficient is over 8, Suspend trading!') 225 | return 226 | elif(self.running_qty == 0 and self.last_running_qty == 0): 227 | if (self.place_order_pin(buy_orders, sell_orders, order_status) == False): 228 | logger.info("Order has created.") 229 | return 230 | elif(self.running_qty > 0): 231 | cycles_sell = self.running_qty // (2 * settings.ORDER_START_SIZE) + 2 if self.running_qty <= 2 * settings.ORDER_START_SIZE else (self.running_qty - 2 * settings.ORDER_START_SIZE - 1) // (settings.ORDER_START_SIZE // 2) + 4 232 | cycles_buy = 1 if self.running_qty < settings.ORDER_START_SIZE else (self.running_qty - settings.ORDER_START_SIZE // 2) // (settings.ORDER_START_SIZE // 4) + 2 233 | if (self.running_qty == self.last_running_qty): #持仓不变 234 | if (self.place_order_pin(buy_orders, sell_orders, order_status) == False): 235 | return 236 | elif (self.running_qty > self.last_running_qty and self.last_running_qty >= 0 and self.reset == False): #多仓增加,买单不变,卖单变化offset3 237 | order_status = 2 238 | for i in reversed(range(1, cycles_sell)): 239 | if not self.short_position_limit_exceeded(): 240 | sell_orders.append(self.prepare_order(i, order_status)) 241 | elif (self.running_qty < self.last_running_qty and self.last_running_qty >= 0 and self.reset == False): #多仓减少,卖单不变,买单变化offset2 242 | order_status = 4 243 | for i in reversed(range(cycles_buy, 4 * (settings.ORDER_PAIRS - 1) + 3 + 1)): 244 | if not self.long_position_limit_exceeded(): 245 | buy_orders.append(self.prepare_order(-i, order_status)) 246 | elif (self.last_running_qty < 0 or (self.last_running_qty == 0 and self.reset == True)): #空转多(或系统重开有仓位时),买卖单都变化,买offset2卖offset3 247 | order_status = 1 248 | for i in reversed(range(cycles_buy, 4 * (settings.ORDER_PAIRS - 1) + 3 + 1)): 249 | if not self.long_position_limit_exceeded(): 250 | buy_orders.append(self.prepare_order(-i, order_status)) 251 | for i in reversed(range(1, cycles_sell)): 252 | if not self.short_position_limit_exceeded(): 253 | sell_orders.append(self.prepare_order(i, order_status)) 254 | else: 255 | logger.error('running_qty bug. running_qty: %s last_running_qty: %s' % (self.running_qty, self.last_running_qty)) 256 | self.exit() 257 | else: 258 | cycles_buy = abs(self.running_qty) // (2 * settings.ORDER_START_SIZE) + 2 if abs(self.running_qty) <= 2 * settings.ORDER_START_SIZE else (abs(self.running_qty) - 2 * settings.ORDER_START_SIZE - 1) // (settings.ORDER_START_SIZE // 2) + 4 259 | cycles_sell = 1 if abs(self.running_qty) < settings.ORDER_START_SIZE else (abs(self.running_qty) - settings.ORDER_START_SIZE // 2) // (settings.ORDER_START_SIZE // 4) + 2 260 | if (self.running_qty == self.last_running_qty): #持仓不变 261 | if (self.place_order_pin(buy_orders, sell_orders, order_status) == False): 262 | return 263 | elif (abs(self.running_qty) > abs(self.last_running_qty) and self.last_running_qty <= 0 and self.reset == False): #空仓增加,买单变化offset3,卖单不变 264 | order_status = 4 265 | for i in reversed(range(1, cycles_buy)): 266 | if not self.long_position_limit_exceeded(): 267 | buy_orders.append(self.prepare_order(-i, order_status)) 268 | elif (abs(self.running_qty) < abs(self.last_running_qty) and self.last_running_qty <= 0 and self.reset == False): #空仓减少,卖单变化offset2,买单不变 269 | order_status = 2 270 | for i in reversed(range(cycles_sell, 4 * (settings.ORDER_PAIRS - 1) + 3 + 1)): 271 | if not self.short_position_limit_exceeded(): 272 | sell_orders.append(self.prepare_order(i, order_status)) 273 | elif (self.last_running_qty > 0 or (self.last_running_qty == 0 and self.reset == True)): #多转空(或系统重开有仓位时),买卖单都变化,买offset3卖offset2 274 | order_status = 3 275 | for i in reversed(range(1, cycles_buy)): 276 | if not self.long_position_limit_exceeded(): 277 | buy_orders.append(self.prepare_order(-i, order_status)) 278 | for i in reversed(range(cycles_sell, 4 * (settings.ORDER_PAIRS - 1) + 3 + 1)): 279 | if not self.short_position_limit_exceeded(): 280 | sell_orders.append(self.prepare_order(i, order_status)) 281 | else: 282 | logger.error('running_qty bug. running_qty: %s last_running_qty: %s' % (self.running_qty, self.last_running_qty)) 283 | self.exit() 284 | 285 | self.last_running_qty = self.running_qty 286 | self.reset = False 287 | print(buy_orders) 288 | print(sell_orders) 289 | return self.converge_orders(buy_orders, sell_orders, order_status) 290 | 291 | def place_order_pin(self, buy_orders, sell_orders, order_status): 292 | ret = False 293 | wave_coefficient_last10price = self.get_wave_coefficient_last10price() 294 | if (wave_coefficient_last10price <= -20 and self.last10price_flag == False): 295 | self.last10price_flag = True 296 | order_status = 5 297 | buy_orders.append({'price': min(self.LastPriceList10second) - 5, 'orderQty': settings.ORDER_PIN_SIZE, 'side': "Buy"}) 298 | buy_orders.append({'price': min(self.LastPriceList10second) - 10, 'orderQty': settings.ORDER_PIN_SIZE, 'side': "Buy"}) 299 | self.pin_buy_orders = buy_orders 300 | ret = True 301 | elif (wave_coefficient_last10price >= 20 and self.last10price_flag == False): 302 | self.last10price_flag = True 303 | order_status = 5 304 | sell_orders.append({'price': max(self.LastPriceList10second) + 5, 'orderQty': settings.ORDER_PIN_SIZE, 'side': "Sell"}) 305 | sell_orders.append({'price': max(self.LastPriceList10second) + 10, 'orderQty': settings.ORDER_PIN_SIZE, 'side': "Sell"}) 306 | self.pin_sell_orders = sell_orders 307 | ret = True 308 | if (self.last10price_countdown <= 0): 309 | self.last10price_flag = False 310 | self.last10price_countdown = 60 311 | order_status = 6 312 | buy_orders = self.pin_buy_orders 313 | sell_orders = self.pin_sell_orders 314 | self.pin_buy_orders = [] 315 | self.pin_sell_orders = [] 316 | ret = True 317 | return ret 318 | 319 | def clear_position(self, buy_orders, sell_orders): 320 | """清空所有仓位""" 321 | if (self.running_qty > 0): 322 | sell_orders.append({'price': self.start_position_buy - 1, 'orderQty': self.running_qty, 'side': "Sell"}) 323 | elif (self.running_qty < 0): 324 | buy_orders.append({'price': self.start_position_sell + 1, 'orderQty': abs(self.running_qty), 'side': "buy"}) 325 | 326 | def prepare_order(self, index, order_status): 327 | """Create an order object.""" 328 | 329 | if(index == 1 or index == -1): 330 | if (((self.running_qty > 0 and order_status == 4) or (self.running_qty < 0 and order_status == 2))) and (abs(self.running_qty) % settings.ORDER_START_SIZE) != 0: #多仓部分减少或空仓部分减少 331 | quantity = settings.ORDER_START_SIZE + (abs(self.running_qty) % settings.ORDER_START_SIZE) if settings.ORDER_START_SIZE < abs(self.running_qty) < 2 * settings.ORDER_START_SIZE else abs(self.running_qty) % settings.ORDER_START_SIZE 332 | elif((0 < self.running_qty < 2 * settings.ORDER_START_SIZE and (order_status == 2 or order_status == 1)) or (-2 * settings.ORDER_START_SIZE < self.running_qty < 0 and (order_status == 4 or order_status == 3))): 333 | quantity = abs(self.running_qty) #仓位化整 334 | elif((self.running_qty > 2 * settings.ORDER_START_SIZE and (order_status == 2 or order_status == 1)) or (self.running_qty < -2 * settings.ORDER_START_SIZE and (order_status == 4 or order_status == 3))) and (abs(self.running_qty) % (settings.ORDER_START_SIZE // 2)) != 0: 335 | quantity = settings.ORDER_START_SIZE - (settings.ORDER_START_SIZE // 2 - abs(self.running_qty) % (settings.ORDER_START_SIZE // 2)) 336 | elif(self.running_qty == 0): 337 | quantity = settings.ORDER_START_SIZE / 2 338 | else: 339 | quantity = settings.ORDER_START_SIZE 340 | elif((self.running_qty >= 2 * settings.ORDER_START_SIZE and index == 2) or (self.running_qty <= -2 * settings.ORDER_START_SIZE and index == -2)): 341 | quantity = settings.ORDER_START_SIZE 342 | elif((self.running_qty > 2 * settings.ORDER_START_SIZE and index > 2) or (self.running_qty < -2 * settings.ORDER_START_SIZE and index < -2)): 343 | quantity = settings.ORDER_START_SIZE / 2 344 | elif((self.running_qty <= 0 and index >= 2) or (self.running_qty >= 0 and index <= -2)): 345 | quantity = settings.ORDER_START_SIZE / 4 346 | else: 347 | logger.error('Choose quantity Error. index: %s running_qty: %s' % (index, self.running_qty)) 348 | self.exit() 349 | if((order_status == 0) or (order_status == 1 and index < 0) or (order_status == 3 and index > 0) or (order_status == 2 and self.running_qty < 0) or (order_status == 4 and self.running_qty > 0)): 350 | price = self.get_price_offset2(index) 351 | elif((order_status == 1 and index > 0) or (order_status == 3 and index < 0) or (order_status == 2 and self.running_qty > 0) or (order_status == 4 and self.running_qty < 0)): 352 | price = self.get_price_offset3(index) 353 | else: 354 | logger.error('Choose offset Error. order_status:%s index:%s self.running_qty:%s' % (order_status, index, self.running_qty)) 355 | self.exit() 356 | return {'price': price, 'orderQty': quantity, 'side': "Buy" if index < 0 else "Sell"} 357 | 358 | def converge_orders(self, buy_orders, sell_orders, order_status): 359 | """Converge the orders we currently have in the book with what we want to be in the book. 360 | This involves amending any open orders and creating new ones if any have filled completely. 361 | We start from the closest orders outward.""" 362 | 363 | tickLog = self.exchange.get_instrument()['tickLog'] 364 | to_amend = [] 365 | to_create = [] 366 | to_cancel = [] 367 | buys_matched = 0 368 | sells_matched = 0 369 | existing_orders = self.exchange.get_orders() 370 | 371 | # Check all existing orders and match them up with what we want to place. 372 | # If there's an open one, we might be able to amend it to fit what we want. 373 | for order in existing_orders: 374 | try: 375 | if (order['side'] == 'Buy' and (order_status == 0 or order_status == 4 or order_status == 3 or order_status == 1)): 376 | desired_order = buy_orders[buys_matched] 377 | buys_matched += 1 378 | elif (order['side'] == 'Sell' and (order_status == 0 or order_status == 2 or order_status == 1 or order_status == 3)): 379 | desired_order = sell_orders[sells_matched] 380 | sells_matched += 1 381 | elif (order['price'] == buy_orders[buys_matched]['price'] and order_status == 6): 382 | to_cancel.append(order) 383 | buys_matched += 1 384 | continue 385 | elif (order['price'] == sell_orders[sells_matched]['price'] and order_status == 6): 386 | to_cancel.append(order) 387 | sells_matched += 1 388 | continue 389 | else: 390 | continue 391 | 392 | # Found an existing order. Do we need to amend it? 393 | if desired_order['orderQty'] != order['leavesQty'] or ( 394 | # If price has changed, and the change is more than our RELIST_INTERVAL, amend. 395 | desired_order['price'] != order['price'] and 396 | abs((desired_order['price'] / order['price']) - 1) > settings.RELIST_INTERVAL): 397 | to_amend.append({'orderID': order['orderID'], 'orderQty': order['cumQty'] + desired_order['orderQty'], 398 | 'price': desired_order['price'], 'side': order['side']}) 399 | except IndexError: 400 | # Will throw if there isn't a desired order to match. In that case, cancel it. 401 | if ((order_status == 2 and order['side'] == 'Sell') or (order_status == 1 and self.running_qty > 0) or (order_status == 4 and order['side'] == 'Buy') or (order_status == 3 and self.running_qty < 0)): 402 | to_cancel.append(order) 403 | 404 | if (order_status == 0 or order_status == 4 or order_status == 3 or order_status == 1 or order_status == 5): 405 | while buys_matched < len(buy_orders): 406 | to_create.append(buy_orders[buys_matched]) 407 | buys_matched += 1 408 | if (order_status == 0 or order_status == 2 or order_status == 1 or order_status == 3 or order_status == 5): 409 | while sells_matched < len(sell_orders): 410 | to_create.append(sell_orders[sells_matched]) 411 | sells_matched += 1 412 | 413 | if len(to_amend) > 0: 414 | for amended_order in reversed(to_amend): 415 | reference_order = [o for o in existing_orders if o['orderID'] == amended_order['orderID']][0] 416 | logger.info("Amending %4s: %d @ %.*f to %d @ %.*f (%+.*f)" % ( 417 | amended_order['side'], 418 | reference_order['leavesQty'], tickLog, reference_order['price'], 419 | (amended_order['orderQty'] - reference_order['cumQty']), tickLog, amended_order['price'], 420 | tickLog, (amended_order['price'] - reference_order['price']) 421 | )) 422 | # This can fail if an order has closed in the time we were processing. 423 | # The API will send us `invalid ordStatus`, which means that the order's status (Filled/Canceled) 424 | # made it not amendable. 425 | # If that happens, we need to catch it and re-tick. 426 | try: 427 | self.exchange.amend_bulk_orders(to_amend) 428 | except requests.exceptions.HTTPError as e: 429 | errorObj = e.response.json() 430 | if errorObj['error']['message'] == 'Invalid ordStatus': 431 | logger.warn("Amending failed. Waiting for order data to converge and retrying.") 432 | sleep(0.5) 433 | return self.place_orders() 434 | else: 435 | logger.error("Unknown error on amend: %s. Exiting" % errorObj) 436 | sys.exit(1) 437 | 438 | if len(to_create) > 0: 439 | logger.info("Creating %d orders:" % (len(to_create))) 440 | for order in reversed(to_create): 441 | logger.info("%4s %d @ %.*f" % (order['side'], order['orderQty'], tickLog, order['price'])) 442 | self.exchange.create_bulk_orders(to_create) 443 | 444 | # Could happen if we exceed a delta limit 445 | if len(to_cancel) > 0: 446 | logger.info("Canceling %d orders:" % (len(to_cancel))) 447 | for order in reversed(to_cancel): 448 | logger.info("%4s %d @ %.*f" % (order['side'], order['leavesQty'], tickLog, order['price'])) 449 | self.exchange.cancel_bulk_orders(to_cancel) 450 | 451 | if ((len(to_amend) > 0) or (len(to_create) > 0) or (len(to_cancel) > 0)): 452 | self.send_tg_message() 453 | 454 | def send_tg_message(self): 455 | now = datetime.datetime.now() 456 | mybalance = '%.6f' % XBt_to_XBT(self.start_XBt) 457 | message = 'BitMEX交易状态\n' + \ 458 | '时间:' + now.astimezone(datetime.timezone(datetime.timedelta(hours=8))).strftime('%Y-%m-%d %H:%M:%S') + '\n' + \ 459 | '保证金余额:' + mybalance + '\n' + \ 460 | '合约数量:' + str(self.running_qty) + '\n' + \ 461 | '开仓价格:' + str(self.exchange.get_position()['avgCostPrice']) + '\n' + \ 462 | '风险等级:' + str(self.position_grade) + '\n' + \ 463 | '最新价格:' + str(self.get_ticker()['last']) + '\n' + \ 464 | '指数价格:' + str(self.exchange.get_portfolio()['XBTUSD']['markPrice']) + '\n' + \ 465 | '今日盈利:' + '%.6f' % (float(mybalance) - self.yesterday_balance) + '\n' + \ 466 | '作日盈利:' + '%.6f' % (self.yesterday_balance - self.before_yesterday_balance) 467 | tg_send_message(message) 468 | if self.position_grade > 3: 469 | tg_send_important_message(message) 470 | 471 | def exit(self): 472 | logger.info("Shutting down. All open orders will be cancelled.") 473 | now = datetime.datetime.now() 474 | message = 'BitMEX交易机器人异常退出\n' + \ 475 | '时间:' + now.astimezone(datetime.timezone(datetime.timedelta(hours=8))).strftime('%Y-%m-%d %H:%M:%S') + '\n' + \ 476 | '合约数量:' + str(self.running_qty) + '\n' + \ 477 | '开仓价格:' + str(self.exchange.get_position()['avgCostPrice']) + '\n' + \ 478 | '风险等级:' + str(self.position_grade) + '\n' + \ 479 | '最新价格:' + str(self.get_ticker()['last']) + '\n' + \ 480 | '指数价格:' + str(self.exchange.get_portfolio()['XBTUSD']['markPrice']) 481 | tg_send_important_message(message) 482 | try: 483 | self.exchange.cancel_all_orders() 484 | self.exchange.bitmex.exit() 485 | except errors.AuthenticationError as e: 486 | logger.info("Was not authenticated; could not cancel orders.") 487 | except Exception as e: 488 | logger.info("Unable to cancel orders: %s" % e) 489 | 490 | sys.exit() 491 | 492 | 493 | def run() -> None: 494 | order_manager = CustomOrderManager() 495 | 496 | # Try/except just keeps ctrl-c from printing an ugly stacktrace 497 | try: 498 | order_manager.run_loop() 499 | except (KeyboardInterrupt, SystemExit): 500 | sys.exit() 501 | -------------------------------------------------------------------------------- /custom_strategy_V3.1.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import sys 3 | from os.path import getmtime 4 | import logging 5 | import requests 6 | from time import sleep 7 | import datetime 8 | import schedule 9 | import re 10 | import numpy as np 11 | import atexit 12 | import signal 13 | 14 | from market_maker.market_maker import OrderManager, XBt_to_XBT, ExchangeInterface 15 | from market_maker.utils import log, constants, errors, math 16 | from telegram_msg import tg_send_message, tg_send_railgun_message, tg_send_important_message, tg_get_updates, tg_get_railgun_updates, tg_get_important_updates 17 | 18 | # Used for reloading the bot - saves modified times of key files 19 | import os 20 | 21 | LOOP_INTERVAL = 1 22 | STOP_SIZE = 70 23 | START_SIZE_MAGNIFICATION = 1000 24 | STOP_PRICE = 7000 25 | #BASE_URL = "https://testnet.bitmex.com/api/v1/" 26 | #API_KEY = "9NRBliZDL4IaNK8ocye2MUtv" 27 | #API_SECRET = "T-RMbdjYP24sAUvxpbzYrDCGX3JLwXl9PPB7caSXJt1gia7p" 28 | BASE_URL = "https://www.bitmex.com/api/v1/" 29 | #railgun 30 | #API_KEY = "xQk9myeZDheUomfcnAIt2sHd" 31 | #API_SECRET = "aj-RGh53UjxJWay1NXNPX8y0zdWdABEyX4MeuRaCdBHuQKHm" 32 | #index 33 | ACCOUNT_NAME = "index" 34 | API_KEY = "vZxXmRdnSx8_mHt1jTWECfBi" 35 | API_SECRET = "dBvP3q6-ar7byQbMan_AalfKWNjVsXGdnp5mhNcWSYe75kuE" 36 | 37 | if(ACCOUNT_NAME == "index"): 38 | tg_send_message_alias = tg_send_message 39 | tg_get__updates_alias = tg_get_updates 40 | 41 | # 42 | # Helpers 43 | # 44 | logger = logging.getLogger('root') 45 | 46 | class CustomOrderManager(OrderManager): 47 | def __init__(self): 48 | self.exchange = ExchangeInterface(base_url=BASE_URL, 49 | apiKey=API_KEY, apiSecret=API_SECRET, 50 | orderIDPrefix="mm_bitmex_", postOnly=False, 51 | timeout=7) 52 | # Once exchange is created, register exit handler that will always cancel orders 53 | # on any error. 54 | atexit.register(self.exit) 55 | signal.signal(signal.SIGTERM, self.exit) 56 | 57 | logger.info("Using symbol %s." % self.exchange.symbol) 58 | 59 | self.start_time = datetime.datetime.now() 60 | self.instrument = self.exchange.get_instrument() 61 | self.starting_qty = self.exchange.get_delta() 62 | self.running_qty = self.starting_qty 63 | self.reset() 64 | 65 | def reset(self): 66 | self.exchange.cancel_all_orders() 67 | self.sanity_check() 68 | self.print_status() 69 | self.position_grade = 0 70 | self.last_running_qty = 0 71 | self.reset = True #设置初始化标记, 买卖单都变化 72 | self.stop_order_price = None #止损触发价格 73 | self.stop_market_maker_flag = False #暂停所有交易, 取消平仓及止损以外所有挂单 74 | self.cancel_all_orders_flag = False #取消所有挂单, 并暂停交易 75 | self.clear_position_flag = False #清空所有仓位, 并暂停交易 76 | self.countdown = False #延迟挂单计数器, 仓位成交后必须等待60秒后挂新单 77 | self.delay_order_check = False #控制是否延迟挂单 78 | self.restart_flag = False #防止挂单后延迟生效而产生的重新挂单 79 | self.buy_only_flag = False #仅挂买单, 由telegram控制 80 | self.sell_only_flag = False #仅挂卖单, 由telegram控制 81 | self.last_buy_orders = [] 82 | self.last_sell_orders = [] 83 | self.MA15_list_difference = [] 84 | 85 | #持仓方向通过self.running_qty来判断, 大于0为多仓, 小于0为空仓 86 | schedule.every().day.at("00:00").do(self.write_mybalance) #每天00:00执行一次 87 | schedule.every(5).seconds.do(self.check_tg_message) #每5秒执行一次检查来自telegram的消息 88 | schedule.every(5).seconds.do(self.check_double_order) #每5秒执行一次检测是否有重复挂单,发现立即删除 89 | schedule.every(5).seconds.do(self.set_BXBT_list_30min) #每5秒执行一次记录最新价, 程序初始化通过BXBT指数来计算MA, 之后改用最新价 90 | schedule.every().minute.do(self.get_MA15_defference) #记录每分钟价格与MA15的差值 91 | 92 | self.BXBT_list_30min = [] 93 | trade_list = self.exchange.bitmex.get_last_trade('.BXBT', 200) 94 | for trade in trade_list[0:30]: 95 | print('time: %s price: %s' % (trade['timestamp'], trade['price'])) 96 | for i in range(0, 12): 97 | self.BXBT_list_30min.append(trade['price']) 98 | for i in range(0, 180): 99 | self.MA15_list_difference.append(trade_list[i]['price'] - (trade_list[i]['price'] + trade_list[i+1]['price'] + trade_list[i+2]['price'] + trade_list[i+3]['price'] + trade_list[i+4]['price'] + trade_list[i+5]['price'] + trade_list[i+6]['price'] + trade_list[i+7]['price'] + trade_list[i+8]['price'] + trade_list[i+9]['price'] + trade_list[i+10]['price'] + trade_list[i+11]['price'] + trade_list[i+12]['price'] + trade_list[i+13]['price'] + trade_list[i+14]['price'])/15) 100 | 101 | # Create orders and converge. 102 | with open(r'/root/mybalance.txt', 'r') as f: 103 | lines = f.readlines() 104 | m1 = re.match(r'(\d{4}-\d{2}-\d{2})\s(\d{2}\:\d{2}\:\d{2})\s+([0-9\.]+)', lines[-1]) 105 | self.yesterday_balance = float(m1.group(3)) 106 | m2 = re.match(r'(\d{4}-\d{2}-\d{2})\s(\d{2}\:\d{2}\:\d{2})\s+([0-9\.]+)', lines[-2]) 107 | self.before_yesterday_balance = float(m2.group(3)) 108 | self.ORDER_START_SIZE = self.start_XBt // 1000000 * START_SIZE_MAGNIFICATION #新算法, 每次初始交易重新设定ORDER_START_SIZE 109 | print('ORDER_START_SIZE: %s' % self.ORDER_START_SIZE) 110 | self.place_orders() 111 | 112 | def write_mybalance(self): 113 | now = datetime.datetime.now() 114 | mybalance = '%.6f' % XBt_to_XBT(self.start_XBt) 115 | with open(r'/root/mybalance.txt', 'a') as f: 116 | f.write(now.strftime('%Y-%m-%d %H:%M:%S') + ' ' + mybalance + '\n') 117 | message = 'BitMEX今日交易统计' + ACCOUNT_NAME + '\n' + \ 118 | '时间:' + now.strftime('%Y-%m-%d %H:%M:%S') + '\n' + \ 119 | '保证金余额:' + mybalance + '\n' + \ 120 | '合约数量:' + str(self.running_qty) + '\n' + \ 121 | '开仓价格:' + str(self.exchange.get_position()['avgCostPrice']) + '\n' + \ 122 | '风险等级:' + str(self.position_grade) + '\n' + \ 123 | '最新价格:' + str(self.get_ticker()['last']) + '\n' + \ 124 | '指数价格:' + str(self.exchange.get_portfolio()['XBTUSD']['markPrice']) + '\n' + \ 125 | '今日盈利:' + '%.6f' % (float(mybalance) - self.yesterday_balance) + '\n' + \ 126 | '作日盈利:' + '%.6f' % (self.yesterday_balance - self.before_yesterday_balance) 127 | tg_send_important_message(message) 128 | self.before_yesterday_balance = self.yesterday_balance 129 | self.yesterday_balance = float(mybalance) 130 | 131 | def get_MA15_defference(self): 132 | if(self.BXBT_list_30min[0] > self.get_BXBT_MA15()): 133 | temp_price = self.BXBT_list_30min[0] 134 | for n in range(11): 135 | if(temp_price < self.BXBT_list_30min[n+1]): 136 | temp_price = self.BXBT_list_30min[n+1] 137 | elif(self.BXBT_list_30min[0] < self.get_BXBT_MA15()): 138 | temp_price = self.BXBT_list_30min[0] 139 | for n in range(11): 140 | if(temp_price > self.BXBT_list_30min[n+1]): 141 | temp_price = self.BXBT_list_30min[n+1] 142 | price_defference = temp_price - self.get_BXBT_MA15() 143 | self.MA15_list_difference.pop() 144 | self.MA15_list_difference.insert(0, price_defference) 145 | 146 | def get_5th_max_MA15_defference(self, getmessage = 5): 147 | """取得排名第五大的最大值""" 148 | max_MA15_defference_list = [] 149 | for i in range(0,18): 150 | max_MA15_defference_list.append(max(self.MA15_list_difference[i*10:i*10+10])) 151 | max1 = max2 = max3 = max4 = max5 = 0 152 | for i in range(0,18): 153 | if(max_MA15_defference_list[i] > max1): 154 | max5 = max4 155 | max4 = max3 156 | max3 = max2 157 | max2 = max1 158 | max1 = max_MA15_defference_list[i] 159 | elif(max_MA15_defference_list[i] > max2): 160 | max5 = max4 161 | max4 = max3 162 | max3 = max2 163 | max2 = max_MA15_defference_list[i] 164 | elif(max_MA15_defference_list[i] > max3): 165 | max5 = max4 166 | max4 = max3 167 | max3 = max_MA15_defference_list[i] 168 | elif(max_MA15_defference_list[i] > max4): 169 | max5 = max4 170 | max4 = max_MA15_defference_list[i] 171 | elif(max_MA15_defference_list[i] > max5): 172 | max5 = max_MA15_defference_list[i] 173 | print('max1 = %s max2 = %s max3 = %s max4 = %s max5 = %s' % (max1, max2, max3, max4, max5)) 174 | if(getmessage == 0): 175 | return ('max1 = %s\n max2 = %s\n max3 = %s\n max4 = %s\n max5 = %s\n' % (max1, max2, max3, max4, max5)) 176 | elif(getmessage == 1): 177 | return max1 178 | elif(getmessage == 2): 179 | return max2 180 | elif(getmessage == 3): 181 | return max3 182 | elif(getmessage == 4): 183 | return max4 184 | elif(getmessage == 5): 185 | return max5 186 | else: 187 | return max5 188 | 189 | def get_5th_min_MA15_defference(self, getmessage = 5): 190 | """取得排名第五小的最小值""" 191 | max_MA15_defference_list = [] 192 | for i in range(0,18): 193 | max_MA15_defference_list.append(min(self.MA15_list_difference[i*10:i*10 +10])) 194 | min1 = min2 = min3 = min4 = min5 = 0 195 | for i in range(0,18): 196 | if(max_MA15_defference_list[i] < min1): 197 | min5 = min4 198 | min4 = min3 199 | min3 = min2 200 | min2 = min1 201 | min1 = max_MA15_defference_list[i] 202 | elif(max_MA15_defference_list[i] < min2): 203 | min5 = min4 204 | min4 = min3 205 | min3 = min2 206 | min2 = max_MA15_defference_list[i] 207 | elif(max_MA15_defference_list[i] < min3): 208 | min5 = min4 209 | min4 = min3 210 | min3 = max_MA15_defference_list[i] 211 | elif(max_MA15_defference_list[i] < min4): 212 | min5 = min4 213 | min4 = max_MA15_defference_list[i] 214 | elif(max_MA15_defference_list[i] < min5): 215 | min5 = max_MA15_defference_list[i] 216 | print('min1 = %s min2 = %s min3 = %s min4 = %s min5 = %s' % (min1, min2, min3, min4, min5)) 217 | if(getmessage == 0): 218 | return ('min1 = %s\n min2 = %s\n min3 = %s\n min4 = %s\n min5 = %s' % (min1, min2, min3, min4, min5)) 219 | elif(getmessage == 1): 220 | return min1 221 | elif(getmessage == 2): 222 | return min2 223 | elif(getmessage == 3): 224 | return min3 225 | elif(getmessage == 4): 226 | return min4 227 | elif(getmessage == 5): 228 | return min5 229 | else: 230 | return min5 231 | 232 | def get_avg_MA15_defference_postive(self): 233 | """获取所有正数偏差值的平均值的1/2""" 234 | new_list = [i for i in self.MA15_list_difference if i > 1] 235 | return (np.mean(new_list) / 2) 236 | 237 | def get_avg_MA15_defference_negative(self): 238 | """获取所有负数偏差值的平均值""" 239 | new_list = [i for i in self.MA15_list_difference if i < -1] 240 | return abs(np.mean(new_list) / 2) 241 | 242 | def set_BXBT_list_30min(self): 243 | self.BXBT_list_30min.pop() 244 | self.BXBT_list_30min.insert(0, self.get_ticker()['last']) 245 | 246 | def get_BXBT_MA30(self): 247 | return np.mean(self.BXBT_list_30min) 248 | 249 | def get_BXBT_MA7(self): 250 | return np.mean(self.BXBT_list_30min[0:84]) 251 | 252 | def get_BXBT_MA10(self): 253 | return np.mean(self.BXBT_list_30min[0:120]) 254 | 255 | def get_BXBT_MA15(self): 256 | return np.mean(self.BXBT_list_30min[0:180]) 257 | 258 | def check_last_price_upordown(self): 259 | ret = 0 260 | if(self.running_qty < 0): 261 | if self.BXBT_list_30min[0] >= self.BXBT_list_30min[1]: 262 | ret = ret + 1 263 | if self.BXBT_list_30min[1] >= self.BXBT_list_30min[2]: 264 | ret = ret + 1 265 | if self.BXBT_list_30min[2] >= self.BXBT_list_30min[3]: 266 | ret = ret + 1 267 | if self.BXBT_list_30min[3] >= self.BXBT_list_30min[4]: 268 | ret = ret + 1 269 | if self.BXBT_list_30min[4] >= self.BXBT_list_30min[5]: 270 | ret = ret + 1 271 | elif(self.running_qty > 0): 272 | if self.BXBT_list_30min[0] <= self.BXBT_list_30min[1]: 273 | ret = ret + 1 274 | if self.BXBT_list_30min[1] <= self.BXBT_list_30min[2]: 275 | ret = ret + 1 276 | if self.BXBT_list_30min[2] <= self.BXBT_list_30min[3]: 277 | ret = ret + 1 278 | if self.BXBT_list_30min[3] <= self.BXBT_list_30min[4]: 279 | ret = ret + 1 280 | if self.BXBT_list_30min[4] <= self.BXBT_list_30min[5]: 281 | ret = ret + 1 282 | else: 283 | if(self.last_running_qty < 0): 284 | if self.BXBT_list_30min[0] <= self.BXBT_list_30min[1]: 285 | ret = ret + 1 286 | if self.BXBT_list_30min[1] <= self.BXBT_list_30min[2]: 287 | ret = ret + 1 288 | if self.BXBT_list_30min[2] <= self.BXBT_list_30min[3]: 289 | ret = ret + 1 290 | if self.BXBT_list_30min[3] <= self.BXBT_list_30min[4]: 291 | ret = ret + 1 292 | if self.BXBT_list_30min[4] <= self.BXBT_list_30min[5]: 293 | ret = ret + 1 294 | elif(self.last_running_qty > 0): 295 | if self.BXBT_list_30min[0] >= self.BXBT_list_30min[1]: 296 | ret = ret + 1 297 | if self.BXBT_list_30min[1] >= self.BXBT_list_30min[2]: 298 | ret = ret + 1 299 | if self.BXBT_list_30min[2] >= self.BXBT_list_30min[3]: 300 | ret = ret + 1 301 | if self.BXBT_list_30min[3] >= self.BXBT_list_30min[4]: 302 | ret = ret + 1 303 | if self.BXBT_list_30min[4] >= self.BXBT_list_30min[5]: 304 | ret = ret + 1 305 | else: 306 | return False 307 | if ret >= 4: 308 | return True 309 | else: 310 | return False 311 | 312 | def check_tg_message(self): 313 | """检查是否有来自telegram的消息,并处理""" 314 | tg_message = tg_get__updates_alias() 315 | if (tg_message == None): 316 | return 317 | elif (tg_message == '/new'): 318 | self.send_tg_message() 319 | elif (tg_message == '/order'): 320 | self.send_tg_order_message() 321 | elif (tg_message == '/get_maxmin'): 322 | tg_send_message_alias(self.get_5th_max_MA15_defference(getmessage = 0) + self.get_5th_min_MA15_defference(getmessage = 0)) 323 | elif (tg_message == '/bxbt_ma7'): 324 | BXBT_MA7 = self.get_BXBT_MA7() 325 | tg_send_message_alias('BXBT_MA7 is %.2f now' % BXBT_MA7) 326 | elif (tg_message == '/bxbt_ma10'): 327 | BXBT_MA10 = self.get_BXBT_MA10() 328 | tg_send_message_alias('BXBT_MA10 is %.2f now' % BXBT_MA10) 329 | elif (tg_message == '/bxbt_ma15'): 330 | BXBT_MA15 = self.get_BXBT_MA15() 331 | tg_send_message_alias('BXBT_MA15 is %.2f now' % BXBT_MA15) 332 | elif (tg_message == '/check_important'): 333 | ret = self.check_tg_important_message() 334 | if (ret != None): 335 | tg_send_message_alias(ret) 336 | else: 337 | tg_send_message_alias('未执行命令') 338 | else: 339 | return 340 | 341 | def check_tg_important_message(self): 342 | tg_important_message = tg_get_important_updates() 343 | if (tg_important_message == None): 344 | return None 345 | elif (tg_important_message == '/stop_market_maker3'): 346 | self.stop_market_maker_flag = True 347 | return '执行stop_market_maker3' 348 | elif (tg_important_message == '/start_market_maker3'): 349 | self.stop_market_maker_flag = False 350 | self.cancel_all_orders_flag = False 351 | self.clear_position_flag = False 352 | return '执行start_market_maker3' 353 | elif (tg_important_message == '/cancel_all_orders3'): 354 | self.cancel_all_orders_flag = True 355 | self.stop_market_maker_flag = True 356 | self.clear_position_flag = False 357 | return '执行cancel_all_orders3' 358 | elif (tg_important_message == '/clear_position3'): 359 | self.clear_position_flag = True 360 | self.stop_market_maker_flag = True 361 | self.cancel_all_orders_flag = False 362 | return '执行clear_position3' 363 | elif (tg_important_message == '/buy_only3'): 364 | self.buy_only_flag = True 365 | self.sell_only_flag = False 366 | return '执行buy_only3' 367 | elif (tg_important_message == '/sell_only3'): 368 | self.buy_only_flag = False 369 | self.sell_only_flag = True 370 | return '执行sell_only3' 371 | elif (tg_important_message == '/cancel_buysell_only3'): 372 | self.buy_only_flag = False 373 | self.sell_only_flag = False 374 | return '执行cancel_buysell_only3' 375 | else: 376 | return None 377 | 378 | def get_position_grade(self): 379 | """获取仓位等级""" 380 | self.position_grade = abs(self.running_qty) // (self.ORDER_START_SIZE//4) 381 | if self.position_grade > 6: 382 | self.position_grade = 6 383 | return self.position_grade 384 | 385 | def get_price_offset2(self, index): 386 | """根据index依次设置每一个价格,这里为差价依次增大""" 387 | #L = [2, 5, 9, 15, 24, 40, 70, 100] 388 | L = [10, 20, 105] 389 | if abs(index) > 3: 390 | logger.error("index cannot over 3") 391 | self.exit() 392 | 393 | BXBT_MA15 = self.get_BXBT_MA15() 394 | if index > 0: 395 | L[0] = self.get_5th_max_MA15_defference() 396 | L[1] = 2 * L[0] 397 | if(BXBT_MA15 + L[index - 1] < self.start_position_sell): 398 | return math.toNearest(self.start_position_sell, self.instrument['tickSize']) 399 | else: 400 | return math.toNearest(BXBT_MA15 + L[index - 1], self.instrument['tickSize']) 401 | elif index < 0: 402 | L[0] = abs(self.get_5th_min_MA15_defference()) 403 | L[1] = 2 * L[0] 404 | if(BXBT_MA15 - L[abs(index) - 1] > self.start_position_buy): 405 | return math.toNearest(self.start_position_buy, self.instrument['tickSize']) 406 | else: 407 | return math.toNearest(BXBT_MA15 - L[abs(index) - 1], self.instrument['tickSize']) 408 | else: 409 | logger.error("offset2_index(%s) cannot 0" % index) 410 | self.exit() 411 | 412 | def get_price_offset3(self, index): 413 | avgCostPrice = self.exchange.get_position()['avgCostPrice'] 414 | if(avgCostPrice == None): 415 | return None 416 | BXBT_MA15 = self.get_BXBT_MA15() 417 | if index > 0: 418 | if((BXBT_MA15 + (self.get_avg_MA15_defference_postive() if self.buy_only_flag == False else self.get_5th_max_MA15_defference())) < avgCostPrice+1): 419 | return math.toNearest(avgCostPrice + index, self.instrument['tickSize']) 420 | else: 421 | return math.toNearest(BXBT_MA15 + (self.get_avg_MA15_defference_postive() if self.buy_only_flag == False else self.get_5th_max_MA15_defference()), self.instrument['tickSize']) 422 | elif index < 0: 423 | if((BXBT_MA15 - (self.get_avg_MA15_defference_negative() if self.sell_only_flag == False else self.get_5th_min_MA15_defference())) > avgCostPrice-1): 424 | return math.toNearest(avgCostPrice - abs(index), self.instrument['tickSize']) 425 | else: 426 | return math.toNearest(BXBT_MA15 - (self.get_avg_MA15_defference_negative() if self.sell_only_flag == False else self.get_5th_min_MA15_defference()), self.instrument['tickSize']) 427 | else: 428 | logger.error("offset3_index(%s) cannot 0" % index) 429 | self.exit() 430 | 431 | 432 | def place_orders(self): 433 | """Create order items for use in convergence.""" 434 | buy_orders = [] 435 | sell_orders = [] 436 | buy_stop_order = {} 437 | sell_stop_order = {} 438 | order_status = 0 439 | """order_status参数说明 440 | 0: running_qty为0, 维持原样 441 | 1: self.running_qty > 0, 买卖都变化, 买单按照offset2, 卖单按照offset3 442 | 2: 买单维持不变, 卖单按照offset3 443 | 3: self.running_qty < 0, 买卖都变化, 买单按照offset3, 卖单按照offset2 444 | 4: 卖单维持不变, 买单按照offset3 445 | 5: 追加指定订单 446 | 6: 取消指定订单 447 | 7: self.running_qty > 0, 买单按照offset2, 卖单不变 448 | 8: self.running_qty < 0, 买单不变, 卖单按照offset2 449 | """ 450 | # Create orders from the outside in. This is intentional - let's say the inner order gets taken; 451 | # then we match orders from the outside in, ensuring the fewest number of orders are amended and only 452 | # a new order is created in the inside. If we did it inside-out, all orders would be amended 453 | # down and a new order would be created at the outside. 454 | position_grade = self.get_position_grade() 455 | avgCostPrice = self.exchange.get_position()['avgCostPrice'] 456 | print ('position_grade: %s ' % position_grade) 457 | print ('running_qty: %s ' % self.running_qty) 458 | print ('ORDER_START_SIZE: %s ' % self.ORDER_START_SIZE) 459 | schedule.run_pending() 460 | 461 | if(self.countdown == True): #设置倒数计时, 60秒后delay_order_check设为True, 可以重新挂非清仓方向的价格 462 | self.cycleclock = self.cycleclock - 1 463 | if(self.cycleclock <= 0): 464 | if(self.check_last_price_upordown() == True): 465 | self.cycleclock = 5 466 | else: 467 | self.countdown = False 468 | self.delay_order_check = True 469 | 470 | if(self.get_ticker()['last'] > STOP_PRICE and self.buy_only_flag == False): 471 | self.buy_only_flag = True 472 | if(self.running_qty < 0): 473 | self.clear_position(buy_orders, sell_orders) 474 | return self.converge_orders(buy_orders, sell_orders, order_status) 475 | 476 | if(self.get_5th_max_MA15_defference(getmessage = 1) > 100): 477 | self.stop_market_maker_flag = True 478 | self.cancel_all_orders_flag = True 479 | self.buy_only_flag = False 480 | self.sell_only_flag = False 481 | tg_important_message('上涨差值超过100,暂停交易') 482 | 483 | if(self.stop_market_maker_flag == True and self.cancel_all_orders_flag == True): 484 | if (len(self.exchange.get_orders()) != 0): 485 | self.exchange.cancel_all_orders() 486 | logger.info("Cancel all orders") 487 | elif(self.stop_market_maker_flag == True and self.clear_position_flag == True): 488 | if(self.running_qty != 0): 489 | self.clear_position(buy_orders, sell_orders) 490 | else: 491 | if (len(self.exchange.get_orders()) != 0): 492 | self.exchange.cancel_all_orders() 493 | logger.info("Market_maker has stopped. No orders, no positions now") 494 | elif(self.stop_market_maker_flag == True): 495 | if(self.running_qty > 0): 496 | if avgCostPrice != None: 497 | sell_stop_order = self.prepare_stop_order(math.toNearest(avgCostPrice - STOP_SIZE, self.instrument['tickSize']), "Sell", abs(self.running_qty)) 498 | order_status = 4 499 | elif(self.running_qty < 0): 500 | if avgCostPrice != None: 501 | buy_stop_order = self.prepare_stop_order(math.toNearest(avgCostPrice + STOP_SIZE, self.instrument['tickSize']), "Buy", abs(self.running_qty)) 502 | order_status = 2 503 | elif(self.running_qty == 0 and self.last_running_qty == 0): 504 | if (len(self.exchange.get_orders()) != 0): 505 | self.exchange.cancel_all_orders() 506 | logger.info("Market_maker has stopped. No orders, no positions now") 507 | 508 | elif(self.running_qty == 0 and self.restart_flag == False): 509 | if(self.check_last_price_upordown() == True): 510 | self.restart_flag = True 511 | self.countdown_restart = 5 512 | return 513 | self.ORDER_START_SIZE = self.start_XBt // 1000000 * START_SIZE_MAGNIFICATION #新算法, 每次初始交易重新设定ORDER_START_SIZE 514 | order_status = 0 515 | if not(self.sell_only_flag == True): 516 | buy_orders.append(self.prepare_order(-1, order_status)) 517 | if not(self.buy_only_flag == True): 518 | sell_orders.append(self.prepare_order(1, order_status)) 519 | self.countdown = False 520 | self.restart_flag = True 521 | self.countdown_restart = 30 522 | 523 | elif(self.running_qty == 0 and self.restart_flag == True): 524 | self.countdown_restart = self.countdown_restart - 1 525 | if(self.countdown_restart <= 0): 526 | self.restart_flag = False 527 | return 528 | 529 | elif(self.running_qty != 0 and self.running_qty != self.last_running_qty): #仓位变动后开始倒计时60秒, 60秒后delay_order_check为True, 可以重新挂非清仓方向的价格 530 | if(self.running_qty > 0): 531 | order_status = 2 532 | sell_orders.append(self.prepare_order(1, order_status)) 533 | elif(self.running_qty < 0): 534 | order_status = 4 535 | buy_orders.append(self.prepare_order(-1, order_status)) 536 | self.cycleclock = 60 537 | self.countdown = True 538 | self.restart_flag = False 539 | self.delay_order_check = False 540 | 541 | elif(self.running_qty != 0 and self.running_qty == self.last_running_qty and self.delay_order_check == True): #可以重新挂非清仓方向的价格 542 | i = abs(self.running_qty) // (self.ORDER_START_SIZE//4) + 1 543 | if(self.running_qty > 0): 544 | order_status = 7 545 | if(i <= 3): 546 | buy_orders.append(self.prepare_order(-i, order_status)) 547 | if(self.running_qty < 0): 548 | order_status = 8 549 | if(i <= 3): 550 | sell_orders.append(self.prepare_order(i, order_status)) 551 | self.cycleclock = 30 552 | self.countdown = True 553 | self.delay_order_check = False 554 | 555 | else: 556 | if(self.running_qty > 0): 557 | order_status = 2 558 | sell_orders.append(self.prepare_order(1, order_status)) 559 | elif(self.running_qty < 0): 560 | order_status = 4 561 | buy_orders.append(self.prepare_order(-1, order_status)) 562 | 563 | if(self.last_running_qty != self.running_qty): 564 | self.send_tg_message() 565 | self.last_running_qty = self.running_qty 566 | self.reset = False 567 | buy_orders = list(filter(None.__ne__, buy_orders)) #去除None 568 | sell_orders = list(filter(None.__ne__, sell_orders)) #去除None 569 | print('BXBT_MA15: %s' % self.get_BXBT_MA15()) 570 | print(buy_orders) 571 | print(sell_orders) 572 | if((self.last_buy_orders == buy_orders and self.last_sell_orders == sell_orders) or (buy_orders == [] and sell_orders == [])): 573 | print('order no change, return') 574 | return 575 | else: 576 | self.last_buy_orders = buy_orders 577 | self.last_sell_orders = sell_orders 578 | self.converge_stop_order(buy_stop_order, sell_stop_order) 579 | return self.converge_orders(buy_orders, sell_orders, order_status) 580 | 581 | 582 | def clear_position(self, buy_orders, sell_orders): 583 | """清空所有仓位""" 584 | if (self.running_qty > 0): 585 | sell_orders.append({'price': self.start_position_buy - 1, 'orderQty': self.running_qty, 'side': "Sell"}) 586 | elif (self.running_qty < 0): 587 | buy_orders.append({'price': self.start_position_sell + 1, 'orderQty': abs(self.running_qty), 'side': "Buy"}) 588 | 589 | def prepare_order(self, index, order_status): 590 | """Create an order object.""" 591 | if(self.running_qty > 0 and index > 0): 592 | quantity = self.running_qty 593 | price = self.get_price_offset3(index) 594 | elif(self.running_qty < 0 and index < 0): 595 | quantity = abs(self.running_qty) 596 | price = self.get_price_offset3(index) 597 | else: 598 | quantity = self.ORDER_START_SIZE // 4 599 | price = self.get_price_offset2(index) 600 | if (price == None): 601 | return None 602 | else: 603 | return {'price': price, 'orderQty': quantity, 'side': "Buy" if index < 0 else "Sell"} 604 | 605 | def prepare_stop_order(self, price, side, orderqty): 606 | if((price < self.get_ticker()['last']) and (side == 'Buy')): 607 | price = self.get_ticker()['last'] + 0.5 608 | elif((price > self.get_ticker()['last']) and (side == 'Sell')): 609 | price = self.get_ticker()['last'] - 0.5 610 | self.stop_order_price = price 611 | return {'stopPx': price, 'orderQty': orderqty, 'side': side} 612 | 613 | def check_double_order(self): 614 | """检测是否有重复挂单, 发现价格一样的重复挂单删除""" 615 | to_cancel = [] 616 | def get_price(order): 617 | if(order['ordType'] == 'Stop'): 618 | return float(order['stopPx']) 619 | else: 620 | return float(order['price']) 621 | existing_orders = sorted(self.exchange.get_orders(), key=get_price, reverse=True) #对订单进行排序 622 | if(len(existing_orders) == 0): 623 | return 624 | order_target = {'price' : 0, 'ordType' : '', 'side' : '', 'stopPx' : 0} 625 | for order in existing_orders: 626 | if (order['ordType'] == 'Limit' and order_target['price'] == order['price'] and order_target['ordType'] == order['ordType'] and order_target['side'] == order['side']): 627 | to_cancel.append(order) 628 | elif(order['ordType'] == 'Stop' and order_target['stopPx'] == order['stopPx'] and order_target['ordType'] == order['ordType'] and order_target['side'] == order['side']): 629 | to_cancel.append(order) 630 | order_target = order 631 | if len(to_cancel) > 0: 632 | logger.info("Canceling stop %d orders:" % (len(to_cancel))) 633 | self.exchange.cancel_bulk_orders(to_cancel) 634 | 635 | def converge_stop_order(self, buy_stop_order, sell_stop_order): 636 | tickLog = self.exchange.get_instrument()['tickLog'] 637 | to_amend = [] 638 | to_create = [] 639 | to_cancel = [] 640 | buys_matched = 0 641 | sells_matched = 0 642 | existing_orders = self.exchange.get_orders() 643 | for order in existing_orders: 644 | if order['ordType'] != 'Stop': 645 | continue 646 | try: 647 | if(order['side'] == 'Buy'): 648 | if(len(buy_stop_order) == 0): 649 | to_cancel.append(order) 650 | continue 651 | else: 652 | desired_order = buy_stop_order 653 | buys_matched += 1 654 | elif (order['side'] == 'Sell'): 655 | if(len(sell_stop_order) == 0): 656 | to_cancel.append(order) 657 | continue 658 | else: 659 | desired_order = sell_stop_order 660 | sells_matched += 1 661 | else: 662 | continue 663 | if desired_order['orderQty'] != order['leavesQty'] or (desired_order['stopPx'] != order['stopPx']): 664 | to_amend.append({'orderID': order['orderID'], 'orderQty': order['cumQty'] + desired_order['orderQty'], 'stopPx': desired_order['stopPx'], 'side': order['side']}) 665 | except IndexError: 666 | # Will throw if there isn't a desired order to match. In that case, cancel it. 667 | to_cancel.append(order) 668 | if(len(buy_stop_order) > 0 and buys_matched < 1): 669 | self.exchange.bitmex.buy_stop(buy_stop_order['orderQty'], buy_stop_order['stopPx']) 670 | if(len(sell_stop_order) > 0 and sells_matched < 1): 671 | self.exchange.bitmex.sell_stop(sell_stop_order['orderQty'], sell_stop_order['stopPx']) 672 | 673 | if len(to_amend) > 0: 674 | for amended_order in reversed(to_amend): 675 | reference_order = [o for o in existing_orders if o['orderID'] == amended_order['orderID']][0] 676 | logger.info("Amending stop %4s: %d @ %.*f to %d @ %.*f (%+.*f)" % ( 677 | amended_order['side'], 678 | reference_order['leavesQty'], tickLog, reference_order['stopPx'], 679 | (amended_order['orderQty'] - reference_order['cumQty']), tickLog, amended_order['stopPx'], 680 | tickLog, (amended_order['stopPx'] - reference_order['stopPx']) 681 | )) 682 | # This can fail if an order has closed in the time we were processing. 683 | # The API will send us `invalid ordStatus`, which means that the order's status (Filled/Canceled) 684 | # made it not amendable. 685 | # If that happens, we need to catch it and re-tick. 686 | try: 687 | self.exchange.amend_bulk_orders(to_amend) 688 | except requests.exceptions.HTTPError as e: 689 | errorObj = e.response.json() 690 | if errorObj['error']['message'] == 'Invalid ordStatus': 691 | logger.warn("Amending failed. Waiting for order data to converge and retrying.") 692 | sleep(0.5) 693 | return self.place_orders() 694 | else: 695 | logger.error("Unknown error on amend: %s. Exiting" % errorObj) 696 | sys.exit(1) 697 | 698 | # Could happen if we exceed a delta limit 699 | if len(to_cancel) > 0: 700 | logger.info("Canceling stop %d orders:" % (len(to_cancel))) 701 | for order in reversed(to_cancel): 702 | logger.info("%4s %d @ %.*f" % (order['side'], order['leavesQty'], tickLog, order['stopPx'])) 703 | self.exchange.cancel_bulk_orders(to_cancel) 704 | 705 | 706 | def converge_orders(self, buy_orders, sell_orders, order_status): 707 | """Converge the orders we currently have in the book with what we want to be in the book. 708 | This involves amending any open orders and creating new ones if any have filled completely. 709 | We start from the closest orders outward.""" 710 | 711 | tickLog = self.exchange.get_instrument()['tickLog'] 712 | to_amend = [] 713 | to_create = [] 714 | to_cancel = [] 715 | buys_matched = 0 716 | sells_matched = 0 717 | existing_orders = self.exchange.get_orders() 718 | 719 | # Check all existing orders and match them up with what we want to place. 720 | # If there's an open one, we might be able to amend it to fit what we want. 721 | for order in existing_orders: 722 | if order['ordType'] != 'Limit': 723 | continue 724 | try: 725 | if (order['side'] == 'Buy' and (order_status == 0 or order_status == 4 or order_status == 3 or order_status == 1 or order_status == 7)): 726 | desired_order = buy_orders[buys_matched] 727 | buys_matched += 1 728 | elif (order['side'] == 'Sell' and (order_status == 0 or order_status == 2 or order_status == 1 or order_status == 3 or order_status == 8)): 729 | desired_order = sell_orders[sells_matched] 730 | sells_matched += 1 731 | elif (order['price'] == buy_orders[buys_matched]['price'] and order_status == 6): 732 | to_cancel.append(order) 733 | buys_matched += 1 734 | continue 735 | elif (order['price'] == sell_orders[sells_matched]['price'] and order_status == 6): 736 | to_cancel.append(order) 737 | sells_matched += 1 738 | continue 739 | else: 740 | continue 741 | 742 | # Found an existing order. Do we need to amend it? 743 | if desired_order['orderQty'] != order['leavesQty'] or ( 744 | # If price has changed, and the change is more than our RELIST_INTERVAL, amend. 745 | desired_order['price'] != order['price'] and 746 | abs((desired_order['price'] / order['price']) - 1) > 0): 747 | to_amend.append({'orderID': order['orderID'], 'orderQty': order['cumQty'] + desired_order['orderQty'], 748 | 'price': desired_order['price'], 'side': order['side']}) 749 | # Found an stop existing order. Do we need to amend it? 750 | 751 | except IndexError: 752 | # Will throw if there isn't a desired order to match. In that case, cancel it. 753 | if ((order_status == 2 and order['side'] == 'Sell') or (order_status == 1 and self.running_qty > 0) or (order_status == 4 and order['side'] == 'Buy') or (order_status == 3 and self.running_qty < 0) or (order_status == 7 and order['side'] == 'Buy') or (order_status == 8 and order['side'] == 'Sell')): 754 | to_cancel.append(order) 755 | 756 | if (order_status == 0 or order_status == 4 or order_status == 3 or order_status == 1 or order_status == 5 or order_status == 7): 757 | while buys_matched < len(buy_orders): 758 | to_create.append(buy_orders[buys_matched]) 759 | buys_matched += 1 760 | if (order_status == 0 or order_status == 2 or order_status == 1 or order_status == 3 or order_status == 5 or order_status == 8): 761 | while sells_matched < len(sell_orders): 762 | to_create.append(sell_orders[sells_matched]) 763 | sells_matched += 1 764 | 765 | if len(to_amend) > 0: 766 | for amended_order in reversed(to_amend): 767 | reference_order = [o for o in existing_orders if o['orderID'] == amended_order['orderID']][0] 768 | logger.info("Amending %4s: %d @ %.*f to %d @ %.*f (%+.*f)" % ( 769 | amended_order['side'], 770 | reference_order['leavesQty'], tickLog, reference_order['price'], 771 | (amended_order['orderQty'] - reference_order['cumQty']), tickLog, amended_order['price'], 772 | tickLog, (amended_order['price'] - reference_order['price']) 773 | )) 774 | # This can fail if an order has closed in the time we were processing. 775 | # The API will send us `invalid ordStatus`, which means that the order's status (Filled/Canceled) 776 | # made it not amendable. 777 | # If that happens, we need to catch it and re-tick. 778 | try: 779 | self.exchange.amend_bulk_orders(to_amend) 780 | except requests.exceptions.HTTPError as e: 781 | errorObj = e.response.json() 782 | if errorObj['error']['message'] == 'Invalid ordStatus': 783 | logger.warn("Amending failed. Waiting for order data to converge and retrying.") 784 | sleep(0.5) 785 | return self.place_orders() 786 | else: 787 | logger.error("Unknown error on amend: %s. Exiting" % errorObj) 788 | sys.exit(1) 789 | 790 | if len(to_create) > 0: 791 | logger.info("Creating %d orders:" % (len(to_create))) 792 | for order in reversed(to_create): 793 | logger.info("%4s %d @ %.*f" % (order['side'], order['orderQty'], tickLog, order['price'])) 794 | self.exchange.create_bulk_orders(to_create) 795 | 796 | # Could happen if we exceed a delta limit 797 | if len(to_cancel) > 0: 798 | logger.info("Canceling %d orders:" % (len(to_cancel))) 799 | for order in reversed(to_cancel): 800 | logger.info("%4s %d @ %.*f" % (order['side'], order['leavesQty'], tickLog, order['price'])) 801 | self.exchange.cancel_bulk_orders(to_cancel) 802 | 803 | def send_tg_message(self): 804 | now = datetime.datetime.now() 805 | mybalance = '%.6f' % XBt_to_XBT(self.start_XBt) 806 | message = 'BitMEX交易状态' + ACCOUNT_NAME + '\n' + ('暂停交易\n' if self.stop_market_maker_flag == True else '') + \ 807 | '时间:' + now.astimezone(datetime.timezone(datetime.timedelta(hours=8))).strftime('%Y-%m-%d %H:%M:%S') + '\n' + \ 808 | '保证金余额:' + mybalance + '\n' + \ 809 | '合约数量:' + str(self.running_qty) + '\n' + \ 810 | '开仓价格:' + str(self.exchange.get_position()['avgCostPrice']) + '\n' + \ 811 | '风险等级:' + str(self.position_grade) + '\n' + \ 812 | '最新价格:' + str(self.get_ticker()['last']) + '\n' + \ 813 | '指数价格:' + str(self.exchange.get_portfolio()['XBTUSD']['markPrice']) + '\n' + \ 814 | '今日盈利:' + '%.6f' % (float(mybalance) - self.yesterday_balance) + '\n' + \ 815 | '作日盈利:' + '%.6f' % (self.yesterday_balance - self.before_yesterday_balance) 816 | tg_send_message_alias(message) 817 | if self.position_grade > 2: 818 | tg_send_important_message(message) 819 | 820 | def send_tg_order_message(self): 821 | def get_price(order): 822 | if(order['ordType'] == 'Stop'): 823 | return float(order['stopPx']) 824 | else: 825 | return float(order['price']) 826 | 827 | message = 'BitMEX委托状态' + ACCOUNT_NAME + '\n' 828 | existing_orders = sorted(self.exchange.get_orders(), key=get_price, reverse=True) 829 | for order in existing_orders: 830 | if (order['ordType'] == 'Stop'): 831 | message = message + '%s %d @ %s %s\n' % (order['side'], order['leavesQty'], order['stopPx'], order['ordType']) 832 | else: 833 | message = message + '%s %d @ %s %s\n' % (order['side'], order['leavesQty'], order['price'], order['ordType']) 834 | tg_send_message_alias(message) 835 | 836 | def run_loop(self): 837 | while True: 838 | sys.stdout.write("-----\n") 839 | sys.stdout.flush() 840 | 841 | self.check_file_change() 842 | sleep(LOOP_INTERVAL) 843 | 844 | # This will restart on very short downtime, but if it's longer, 845 | # the MM will crash entirely as it is unable to connect to the WS on boot. 846 | if not self.check_connection(): 847 | logger.error("Realtime data connection unexpectedly closed, restarting.") 848 | self.restart() 849 | 850 | self.sanity_check() # Ensures health of mm - several cut-out points here 851 | self.print_status() # Print skew, delta, etc 852 | self.place_orders() # Creates desired orders and converges to existing orders 853 | 854 | def exit(self): 855 | logger.info("Shutting down. All open orders will be cancelled.") 856 | now = datetime.datetime.now() 857 | message = 'BitMEX交易机器人3异常退出\n' + \ 858 | '时间:' + now.astimezone(datetime.timezone(datetime.timedelta(hours=8))).strftime('%Y-%m-%d %H:%M:%S') + '\n' + \ 859 | '合约数量:' + str(self.running_qty) + '\n' + \ 860 | '开仓价格:' + str(self.exchange.get_position()['avgCostPrice']) + '\n' + \ 861 | '风险等级:' + str(self.position_grade) + '\n' + \ 862 | '最新价格:' + str(self.get_ticker()['last']) + '\n' + \ 863 | '指数价格:' + str(self.exchange.get_portfolio()['XBTUSD']['markPrice']) 864 | tg_send_important_message(message) 865 | try: 866 | self.exchange.cancel_all_orders() 867 | self.exchange.bitmex.exit() 868 | except errors.AuthenticationError as e: 869 | logger.info("Was not authenticated; could not cancel orders.") 870 | except Exception as e: 871 | logger.info("Unable to cancel orders: %s" % e) 872 | 873 | sys.exit() 874 | 875 | 876 | def run() -> None: 877 | order_manager = CustomOrderManager() 878 | 879 | # Try/except just keeps ctrl-c from printing an ugly stacktrace 880 | try: 881 | order_manager.run_loop() 882 | except (KeyboardInterrupt, SystemExit): 883 | sys.exit() 884 | -------------------------------------------------------------------------------- /custom_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import sys 3 | import numpy as np 4 | 5 | from market_maker.market_maker import OrderManager 6 | 7 | class CustomOrderManager(OrderManager): 8 | 9 | def reset(self): 10 | self.sanity_check() 11 | self.print_status() 12 | self.place_orders() 13 | 14 | def place_orders(self) -> None: 15 | print('TestOrderManager Test') 16 | self.BXBT_list_30min = [] 17 | trade_list = self.exchange.bitmex.get_last_trade('.BXBT', 30) 18 | for trade in trade_list: 19 | print('time: %s price: %s' % (trade['timestamp'], trade['price'])) 20 | self.BXBT_list_30min.append(trade['price']) 21 | self.BXBT_MA30 = np.mean(self.BXBT_list_30min) 22 | print('BXBT_MA30: %s' % self.BXBT_MA30) 23 | self.exit() 24 | 25 | def exit(self): 26 | print("TestOrderManager Over, do nothing") 27 | sys.exit() 28 | 29 | def run() -> None: 30 | order_manager = CustomOrderManager() 31 | 32 | # Try/except just keeps ctrl-c from printing an ugly stacktrace 33 | try: 34 | order_manager.run_loop() 35 | except (KeyboardInterrupt, SystemExit): 36 | sys.exit() 37 | -------------------------------------------------------------------------------- /market_maker.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from time import sleep 3 | import sys 4 | from datetime import datetime 5 | from os.path import getmtime 6 | import random 7 | import requests 8 | import atexit 9 | import signal 10 | 11 | from market_maker import bitmex 12 | from market_maker.settings import settings 13 | from market_maker.utils import log, constants, errors, math 14 | 15 | # Used for reloading the bot - saves modified times of key files 16 | import os 17 | watched_files_mtimes = [(f, getmtime(f)) for f in settings.WATCHED_FILES] 18 | 19 | 20 | # 21 | # Helpers 22 | # 23 | logger = log.setup_custom_logger('root') 24 | 25 | 26 | class ExchangeInterface: 27 | def __init__(self, dry_run=False, base_url=settings.BASE_URL, apiKey=settings.API_KEY, apiSecret=settings.API_SECRET, orderIDPrefix=settings.ORDERID_PREFIX, postOnly=settings.POST_ONLY, timeout=settings.TIMEOUT): 28 | self.dry_run = dry_run 29 | if len(sys.argv) > 1: 30 | self.symbol = sys.argv[1] 31 | else: 32 | self.symbol = settings.SYMBOL 33 | self.bitmex = bitmex.BitMEX(base_url=base_url, symbol=self.symbol, 34 | apiKey=apiKey, apiSecret=apiSecret, 35 | orderIDPrefix=orderIDPrefix, postOnly=postOnly, 36 | timeout=timeout) 37 | 38 | def cancel_order(self, order): 39 | tickLog = self.get_instrument()['tickLog'] 40 | logger.info("Canceling: %s %d @ %.*f" % (order['side'], order['orderQty'], tickLog, order['price'])) 41 | while True: 42 | try: 43 | self.bitmex.cancel(order['orderID']) 44 | sleep(settings.API_REST_INTERVAL) 45 | except ValueError as e: 46 | logger.info(e) 47 | sleep(settings.API_ERROR_INTERVAL) 48 | else: 49 | break 50 | 51 | def cancel_all_orders(self): 52 | if self.dry_run: 53 | return 54 | 55 | logger.info("Resetting current position. Canceling all existing orders.") 56 | tickLog = self.get_instrument()['tickLog'] 57 | self.bitmex.cancel_all_orders() 58 | sleep(settings.API_REST_INTERVAL) 59 | 60 | def get_portfolio(self): 61 | """获得一些市场信息""" 62 | contracts = settings.CONTRACTS 63 | portfolio = {} 64 | for symbol in contracts: 65 | position = self.bitmex.position(symbol=symbol) 66 | instrument = self.bitmex.instrument(symbol=symbol) 67 | 68 | if instrument['isQuanto']: 69 | future_type = "Quanto" 70 | elif instrument['isInverse']: 71 | future_type = "Inverse" 72 | elif not instrument['isQuanto'] and not instrument['isInverse']: 73 | future_type = "Linear" 74 | else: 75 | raise NotImplementedError("Unknown future type; not quanto or inverse: %s" % instrument['symbol']) 76 | 77 | if instrument['underlyingToSettleMultiplier'] is None: 78 | multiplier = float(instrument['multiplier']) / float(instrument['quoteToSettleMultiplier']) 79 | else: 80 | multiplier = float(instrument['multiplier']) / float(instrument['underlyingToSettleMultiplier']) 81 | 82 | portfolio[symbol] = { 83 | "currentQty": float(position['currentQty']), #我的当前仓位数量 84 | "futureType": future_type, 85 | "multiplier": multiplier, 86 | "markPrice": float(instrument['markPrice']), #BitMEX 指数价格 87 | "spot": float(instrument['indicativeSettlePrice'])#标记价格 88 | } 89 | 90 | return portfolio 91 | 92 | def calc_delta(self): 93 | """Calculate currency delta for portfolio""" 94 | portfolio = self.get_portfolio() 95 | spot_delta = 0 96 | mark_delta = 0 97 | for symbol in portfolio: 98 | item = portfolio[symbol] 99 | if item['futureType'] == "Quanto": 100 | spot_delta += item['currentQty'] * item['multiplier'] * item['spot'] 101 | mark_delta += item['currentQty'] * item['multiplier'] * item['markPrice'] 102 | elif item['futureType'] == "Inverse": 103 | spot_delta += (item['multiplier'] / item['spot']) * item['currentQty'] 104 | mark_delta += (item['multiplier'] / item['markPrice']) * item['currentQty'] 105 | elif item['futureType'] == "Linear": 106 | spot_delta += item['multiplier'] * item['currentQty'] 107 | mark_delta += item['multiplier'] * item['currentQty'] 108 | basis_delta = mark_delta - spot_delta 109 | delta = { 110 | "spot": spot_delta,#开仓价值 111 | "mark_price": mark_delta,#目前仓位价值 112 | "basis": basis_delta#未实现盈亏 113 | } 114 | return delta 115 | 116 | def get_recent_trades(self): 117 | return self.bitmex.recent_trades() 118 | 119 | def get_delta(self, symbol=None): 120 | if symbol is None: 121 | symbol = self.symbol 122 | return self.get_position(symbol)['currentQty'] 123 | 124 | def get_instrument(self, symbol=None): 125 | if symbol is None: 126 | symbol = self.symbol 127 | return self.bitmex.instrument(symbol) 128 | 129 | def get_margin(self): 130 | """Get your current balance.你账户的余额和保证金要求的更新""" 131 | if self.dry_run: 132 | return {'marginBalance': float(settings.DRY_BTC), 'availableFunds': float(settings.DRY_BTC)} 133 | return self.bitmex.funds() 134 | 135 | def get_orders(self): 136 | if self.dry_run: 137 | return [] 138 | return self.bitmex.open_orders() 139 | 140 | def get_highest_buy(self): 141 | buys = [o for o in self.get_orders() if o['side'] == 'Buy'] 142 | if not len(buys): 143 | return {'price': -2**32} 144 | highest_buy = max(buys or [], key=lambda o: o['price']) 145 | return highest_buy if highest_buy else {'price': -2**32} 146 | 147 | def get_lowest_sell(self): 148 | sells = [o for o in self.get_orders() if o['side'] == 'Sell'] 149 | if not len(sells): 150 | return {'price': 2**32} 151 | lowest_sell = min(sells or [], key=lambda o: o['price']) 152 | return lowest_sell if lowest_sell else {'price': 2**32} # ought to be enough for anyone 153 | 154 | def get_position(self, symbol=None): 155 | """Get your open position.你仓位的更新""" 156 | if symbol is None: 157 | symbol = self.symbol 158 | return self.bitmex.position(symbol) 159 | 160 | def get_ticker(self, symbol=None): 161 | """返回从instrument那里取得的买一价和卖一价 162 | ticker = { 163 | "last": instrument['lastPrice'], 164 | "buy": bid, 165 | "sell": ask, 166 | "mid": (bid + ask) / 2 167 | } 168 | """ 169 | if symbol is None: 170 | symbol = self.symbol 171 | return self.bitmex.ticker_data(symbol) 172 | 173 | def is_open(self): 174 | """Check that websockets are still open.""" 175 | return not self.bitmex.ws.exited 176 | 177 | def check_market_open(self): 178 | instrument = self.get_instrument() 179 | if instrument["state"] != "Open" and instrument["state"] != "Closed": 180 | raise errors.MarketClosedError("The instrument %s is not open. State: %s" % 181 | (self.symbol, instrument["state"])) 182 | 183 | def check_if_orderbook_empty(self): 184 | """This function checks whether the order book is empty""" 185 | instrument = self.get_instrument() 186 | if instrument['midPrice'] is None: 187 | raise errors.MarketEmptyError("Orderbook is empty, cannot quote") 188 | 189 | def amend_bulk_orders(self, orders): 190 | """Amend multiple orders.修改多个订单""" 191 | if self.dry_run: 192 | return orders 193 | return self.bitmex.amend_bulk_orders(orders) 194 | 195 | def create_bulk_orders(self, orders): 196 | """Create multiple orders.创建多个订单""" 197 | if self.dry_run: 198 | return orders 199 | return self.bitmex.create_bulk_orders(orders) 200 | 201 | def cancel_bulk_orders(self, orders): 202 | """取消多个订单""" 203 | if self.dry_run: 204 | return orders 205 | return self.bitmex.cancel([order['orderID'] for order in orders]) 206 | 207 | 208 | class OrderManager: 209 | def __init__(self): 210 | self.exchange = ExchangeInterface(settings.DRY_RUN) 211 | # Once exchange is created, register exit handler that will always cancel orders 212 | # on any error. 213 | atexit.register(self.exit) 214 | signal.signal(signal.SIGTERM, self.exit) 215 | 216 | logger.info("Using symbol %s." % self.exchange.symbol) 217 | 218 | if settings.DRY_RUN: 219 | logger.info("Initializing dry run. Orders printed below represent what would be posted to BitMEX.") 220 | else: 221 | logger.info("Order Manager initializing, connecting to BitMEX. Live run: executing real trades.") 222 | 223 | self.start_time = datetime.now() 224 | self.instrument = self.exchange.get_instrument() 225 | self.starting_qty = self.exchange.get_delta() 226 | self.running_qty = self.starting_qty 227 | self.reset() 228 | 229 | def reset(self): 230 | self.exchange.cancel_all_orders() 231 | self.sanity_check() 232 | self.print_status() 233 | 234 | # Create orders and converge. 235 | self.place_orders() 236 | 237 | def print_status(self): 238 | """Print the current MM status.""" 239 | 240 | margin = self.exchange.get_margin() 241 | position = self.exchange.get_position() 242 | self.running_qty = self.exchange.get_delta() 243 | tickLog = self.exchange.get_instrument()['tickLog'] 244 | self.start_XBt = margin["marginBalance"] 245 | 246 | logger.info("Current XBT Balance: %.6f" % XBt_to_XBT(self.start_XBt))#目前可用资金 247 | logger.info("Current Contract Position: %d" % self.running_qty)#持仓量 248 | if settings.CHECK_POSITION_LIMITS: 249 | logger.info("Position limits: %d/%d" % (settings.MIN_POSITION, settings.MAX_POSITION))#目前允许的最大可持仓量 250 | if position['currentQty'] != 0: 251 | logger.info("Avg Cost Price: %.*f" % (tickLog, float(position['avgCostPrice'])))#开仓价格 252 | logger.info("Avg Entry Price: %.*f" % (tickLog, float(position['avgEntryPrice'])))#平均价格 253 | logger.info("Contracts Traded This Run: %d" % (self.running_qty - self.starting_qty))#持仓量 254 | logger.info("Total Contract Delta: %.4f XBT" % self.exchange.calc_delta()['spot'])#开仓价值 255 | 256 | def get_ticker(self): 257 | """获得买一价和卖一价,并根据此价格设置order的开始买价和卖价""" 258 | ticker = self.exchange.get_ticker() 259 | tickLog = self.exchange.get_instrument()['tickLog'] 260 | 261 | # Set up our buy & sell positions as the smallest possible unit above and below the current spread 262 | # and we'll work out from there. That way we always have the best price but we don't kill wide 263 | # and potentially profitable spreads. 264 | ##self.start_position_buy = ticker["buy"] + self.instrument['tickSize'] 265 | ##self.start_position_sell = ticker["sell"] - self.instrument['tickSize'] 266 | self.start_position_buy = ticker["buy"] 267 | self.start_position_sell = ticker["sell"] 268 | 269 | # If we're maintaining spreads and we already have orders in place, 270 | # make sure they're not ours. If they are, we need to adjust, otherwise we'll 271 | # just work the orders inward until they collide. 272 | if settings.MAINTAIN_SPREADS is False: 273 | if 0 < self.exchange.get_highest_buy()['price'] <= 100000: 274 | self.start_position_buy = elf.exchange.get_highest_buy()['price'] 275 | if 0 < self.exchange.get_lowest_sell()['price'] <= 100000: 276 | self.start_position_sell = self.exchange.get_lowest_sell()['price'] 277 | 278 | # Back off if our spread is too small. 279 | ##if self.start_position_buy * (1.00 + settings.MIN_SPREAD) > self.start_position_sell: 280 | ## self.start_position_buy *= (1.00 - (settings.MIN_SPREAD / 2)) 281 | ## self.start_position_sell *= (1.00 + (settings.MIN_SPREAD / 2)) 282 | 283 | # Midpoint, used for simpler order placement. 284 | self.start_position_mid = ticker["mid"] 285 | logger.info( 286 | "%s Ticker: Buy: %.*f, Sell: %.*f" % 287 | (self.instrument['symbol'], tickLog, ticker["buy"], tickLog, ticker["sell"]) 288 | ) 289 | logger.info('Start Positions: Buy: %.*f, Sell: %.*f, Mid: %.*f' % 290 | (tickLog, self.start_position_buy, tickLog, self.start_position_sell, 291 | tickLog, self.start_position_mid)) 292 | return ticker 293 | 294 | def get_price_offset(self, index): 295 | """Given an index (1, -1, 2, -2, etc.) return the price for that side of the book. 296 | Negative is a buy, positive is a sell.根据index依次设置每一个价格,这里为等差""" 297 | # Maintain existing spreads for max profit 298 | if settings.MAINTAIN_SPREADS: 299 | start_position = self.start_position_buy if index < 0 else self.start_position_sell 300 | # First positions (index 1, -1) should start right at start_position, others should branch from there 301 | index = index + 1 if index < 0 else index - 1 302 | else: 303 | # Offset mode: ticker comes from a reference exchange and we define an offset. 304 | start_position = self.start_position_buy if index < 0 else self.start_position_sell 305 | 306 | # If we're attempting to sell, but our sell price is actually lower than the buy, 307 | # move over to the sell side. 308 | if index > 0 and start_position < self.start_position_buy: 309 | start_position = self.start_position_sell 310 | # Same for buys. 311 | if index < 0 and start_position > self.start_position_sell: 312 | start_position = self.start_position_buy 313 | 314 | return math.toNearest(start_position * (1 + settings.INTERVAL) ** index, self.instrument['tickSize']) 315 | 316 | ### 317 | # Orders 318 | ### 319 | 320 | def place_orders(self): 321 | """Create order items for use in convergence.""" 322 | 323 | buy_orders = [] 324 | sell_orders = [] 325 | # Create orders from the outside in. This is intentional - let's say the inner order gets taken; 326 | # then we match orders from the outside in, ensuring the fewest number of orders are amended and only 327 | # a new order is created in the inside. If we did it inside-out, all orders would be amended 328 | # down and a new order would be created at the outside. 329 | for i in reversed(range(1, settings.ORDER_PAIRS + 1)): 330 | if not self.long_position_limit_exceeded(): 331 | buy_orders.append(self.prepare_order(-i)) 332 | if not self.short_position_limit_exceeded(): 333 | sell_orders.append(self.prepare_order(i)) 334 | 335 | return self.converge_orders(buy_orders, sell_orders) 336 | 337 | def prepare_order(self, index): 338 | """Create an order object.""" 339 | 340 | if settings.RANDOM_ORDER_SIZE is True: 341 | quantity = random.randint(settings.MIN_ORDER_SIZE, settings.MAX_ORDER_SIZE) 342 | else: 343 | quantity = settings.ORDER_START_SIZE + ((abs(index) - 1) * settings.ORDER_STEP_SIZE) 344 | 345 | price = self.get_price_offset(index) 346 | 347 | return {'price': price, 'orderQty': quantity, 'side': "Buy" if index < 0 else "Sell"} 348 | 349 | def converge_orders(self, buy_orders, sell_orders): 350 | """Converge the orders we currently have in the book with what we want to be in the book. 351 | This involves amending any open orders and creating new ones if any have filled completely. 352 | We start from the closest orders outward.""" 353 | 354 | tickLog = self.exchange.get_instrument()['tickLog'] 355 | to_amend = [] 356 | to_create = [] 357 | to_cancel = [] 358 | buys_matched = 0 359 | sells_matched = 0 360 | existing_orders = self.exchange.get_orders() 361 | 362 | # Check all existing orders and match them up with what we want to place. 363 | # If there's an open one, we might be able to amend it to fit what we want. 364 | for order in existing_orders: 365 | try: 366 | if order['side'] == 'Buy': 367 | desired_order = buy_orders[buys_matched] 368 | buys_matched += 1 369 | else: 370 | desired_order = sell_orders[sells_matched] 371 | sells_matched += 1 372 | 373 | # Found an existing order. Do we need to amend it? 374 | if desired_order['orderQty'] != order['leavesQty'] or ( 375 | # If price has changed, and the change is more than our RELIST_INTERVAL, amend. 376 | desired_order['price'] != order['price'] and 377 | abs((desired_order['price'] / order['price']) - 1) > settings.RELIST_INTERVAL): 378 | to_amend.append({'orderID': order['orderID'], 'orderQty': order['cumQty'] + desired_order['orderQty'], 379 | 'price': desired_order['price'], 'side': order['side']}) 380 | except IndexError: 381 | # Will throw if there isn't a desired order to match. In that case, cancel it. 382 | to_cancel.append(order) 383 | 384 | while buys_matched < len(buy_orders): 385 | to_create.append(buy_orders[buys_matched]) 386 | buys_matched += 1 387 | 388 | while sells_matched < len(sell_orders): 389 | to_create.append(sell_orders[sells_matched]) 390 | sells_matched += 1 391 | 392 | if len(to_amend) > 0: 393 | for amended_order in reversed(to_amend): 394 | reference_order = [o for o in existing_orders if o['orderID'] == amended_order['orderID']][0] 395 | logger.info("Amending %4s: %d @ %.*f to %d @ %.*f (%+.*f)" % ( 396 | amended_order['side'], 397 | reference_order['leavesQty'], tickLog, reference_order['price'], 398 | (amended_order['orderQty'] - reference_order['cumQty']), tickLog, amended_order['price'], 399 | tickLog, (amended_order['price'] - reference_order['price']) 400 | )) 401 | # This can fail if an order has closed in the time we were processing. 402 | # The API will send us `invalid ordStatus`, which means that the order's status (Filled/Canceled) 403 | # made it not amendable. 404 | # If that happens, we need to catch it and re-tick. 405 | try: 406 | self.exchange.amend_bulk_orders(to_amend) 407 | except requests.exceptions.HTTPError as e: 408 | errorObj = e.response.json() 409 | if errorObj['error']['message'] == 'Invalid ordStatus': 410 | logger.warn("Amending failed. Waiting for order data to converge and retrying.") 411 | sleep(0.5) 412 | return self.place_orders() 413 | else: 414 | logger.error("Unknown error on amend: %s. Exiting" % errorObj) 415 | sys.exit(1) 416 | 417 | if len(to_create) > 0: 418 | logger.info("Creating %d orders:" % (len(to_create))) 419 | for order in reversed(to_create): 420 | logger.info("%4s %d @ %.*f" % (order['side'], order['orderQty'], tickLog, order['price'])) 421 | self.exchange.create_bulk_orders(to_create) 422 | 423 | # Could happen if we exceed a delta limit 424 | if len(to_cancel) > 0: 425 | logger.info("Canceling %d orders:" % (len(to_cancel))) 426 | for order in reversed(to_cancel): 427 | logger.info("%4s %d @ %.*f" % (order['side'], order['leavesQty'], tickLog, order['price'])) 428 | self.exchange.cancel_bulk_orders(to_cancel) 429 | 430 | ### 431 | # Position Limits 432 | ### 433 | 434 | def short_position_limit_exceeded(self): 435 | """Returns True if the short position limit is exceeded""" 436 | if not settings.CHECK_POSITION_LIMITS: 437 | return False 438 | position = self.exchange.get_delta() 439 | return position <= settings.MIN_POSITION 440 | 441 | def long_position_limit_exceeded(self): 442 | """Returns True if the long position limit is exceeded""" 443 | if not settings.CHECK_POSITION_LIMITS: 444 | return False 445 | position = self.exchange.get_delta() 446 | return position >= settings.MAX_POSITION 447 | 448 | ### 449 | # Sanity 450 | ## 451 | 452 | def sanity_check(self): 453 | """Perform checks before placing orders.""" 454 | 455 | # Check if OB is empty - if so, can't quote. 456 | self.exchange.check_if_orderbook_empty() 457 | 458 | # Ensure market is still open. 459 | self.exchange.check_market_open() 460 | 461 | # Get ticker, which sets price offsets and prints some debugging info. 462 | ticker = self.get_ticker() 463 | 464 | # Sanity check: 465 | if self.get_price_offset(-1) >= ticker["sell"] or self.get_price_offset(1) <= ticker["buy"]: 466 | logger.error("Buy: %s, Sell: %s" % (self.start_position_buy, self.start_position_sell)) 467 | logger.error("First buy position: %s\nBitMEX Best Ask: %s\nFirst sell position: %s\nBitMEX Best Bid: %s" % 468 | (self.get_price_offset(-1), ticker["sell"], self.get_price_offset(1), ticker["buy"])) 469 | logger.error("Sanity check failed, exchange data is inconsistent") 470 | self.exit() 471 | 472 | # Messaging if the position limits are reached 473 | if self.long_position_limit_exceeded(): 474 | logger.info("Long delta limit exceeded") 475 | logger.info("Current Position: %.f, Maximum Position: %.f" % 476 | (self.exchange.get_delta(), settings.MAX_POSITION)) 477 | 478 | if self.short_position_limit_exceeded(): 479 | logger.info("Short delta limit exceeded") 480 | logger.info("Current Position: %.f, Minimum Position: %.f" % 481 | (self.exchange.get_delta(), settings.MIN_POSITION)) 482 | 483 | ### 484 | # Running 485 | ### 486 | 487 | def check_file_change(self): 488 | """Restart if any files we're watching have changed.""" 489 | for f, mtime in watched_files_mtimes: 490 | if getmtime(f) > mtime: 491 | self.restart() 492 | 493 | def check_connection(self): 494 | """Ensure the WS connections are still open.""" 495 | return self.exchange.is_open() 496 | 497 | def exit(self): 498 | logger.info("Shutting down. All open orders will be cancelled.") 499 | try: 500 | self.exchange.cancel_all_orders() 501 | self.exchange.bitmex.exit() 502 | except errors.AuthenticationError as e: 503 | logger.info("Was not authenticated; could not cancel orders.") 504 | except Exception as e: 505 | logger.info("Unable to cancel orders: %s" % e) 506 | 507 | sys.exit() 508 | 509 | def run_loop(self): 510 | while True: 511 | sys.stdout.write("-----\n") 512 | sys.stdout.flush() 513 | 514 | self.check_file_change() 515 | sleep(settings.LOOP_INTERVAL) 516 | 517 | # This will restart on very short downtime, but if it's longer, 518 | # the MM will crash entirely as it is unable to connect to the WS on boot. 519 | if not self.check_connection(): 520 | logger.error("Realtime data connection unexpectedly closed, restarting.") 521 | self.restart() 522 | 523 | self.sanity_check() # Ensures health of mm - several cut-out points here 524 | self.print_status() # Print skew, delta, etc 525 | self.place_orders() # Creates desired orders and converges to existing orders 526 | 527 | def restart(self): 528 | logger.info("Restarting the market maker...") 529 | os.execv(sys.executable, [sys.executable] + sys.argv) 530 | 531 | # 532 | # Helpers 533 | # 534 | 535 | 536 | def XBt_to_XBT(XBt): 537 | return float(XBt) / constants.XBt_TO_XBT 538 | 539 | 540 | def cost(instrument, quantity, price): 541 | mult = instrument["multiplier"] 542 | P = mult * price if mult >= 0 else mult / price 543 | return abs(quantity * P) 544 | 545 | 546 | def margin(instrument, quantity, price): 547 | return cost(instrument, quantity, price) * instrument["initMargin"] 548 | 549 | 550 | def run(): 551 | logger.info('BitMEX Market Maker Version: %s\n' % constants.VERSION) 552 | 553 | om = OrderManager() 554 | # Try/except just keeps ctrl-c from printing an ugly stacktrace 555 | try: 556 | om.run_loop() 557 | except (KeyboardInterrupt, SystemExit): 558 | sys.exit() 559 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import importlib 4 | import os 5 | import sys 6 | 7 | from market_maker.utils.dotdict import dotdict 8 | import market_maker._settings_base as baseSettings 9 | 10 | 11 | def import_path(fullpath): 12 | """ 13 | Import a file with full path specification. Allows one to 14 | import from anywhere, something __import__ does not do. 15 | """ 16 | path, filename = os.path.split(fullpath) 17 | filename, ext = os.path.splitext(filename) 18 | sys.path.insert(0, path) 19 | module = importlib.import_module(filename, path) 20 | importlib.reload(module) # Might be out of date 21 | del sys.path[0] 22 | return module 23 | 24 | 25 | userSettings = import_path(os.path.join('.', 'settings')) 26 | symbolSettings = None 27 | symbol = sys.argv[1] if len(sys.argv) > 1 else None 28 | if symbol: 29 | print("Importing symbol settings for %s..." % symbol) 30 | try: 31 | symbolSettings = import_path(os.path.join('..', 'settings-%s' % symbol)) 32 | except Exception as e: 33 | print("Unable to find settings-%s.py." % symbol) 34 | 35 | # Assemble settings. 36 | settings = {} 37 | settings.update(vars(baseSettings)) 38 | settings.update(vars(userSettings)) 39 | if symbolSettings: 40 | settings.update(vars(symbolSettings)) 41 | 42 | # Main export 43 | settings = dotdict(settings) 44 | -------------------------------------------------------------------------------- /tele_bot_msg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding:utf-8 -*- 3 | 4 | import telebot 5 | 6 | TOKEN = '765047482:AAEvSaWe7etm7vP6kxQu03WRY9zNycb7Pcc' 7 | NOTICE_TOKEN = '704593545:AAGEauPlgPergENS6915Ke8W980fJ-SNo8M' 8 | USER_ID = 480588693 9 | 10 | bot = telebot.TeleBot(TOKEN) 11 | bot2 = telebot.TeleBot(NOTICE_TOKEN) 12 | chat_id = USER_ID 13 | 14 | def check_user_id(message): 15 | if(message.chat.id == chat_id): 16 | return True 17 | else: 18 | return False 19 | 20 | @bot.message_handler(commands=['help']) 21 | def send_welcome(message): 22 | bot.send_message(reply_to_message_id=message.message_id, chat_id=message.chat.id, text='这是一个私人服务器通知专用机器人') 23 | 24 | @bot.message_handler(commands=['new']) 25 | def send_tg_message_now(message): 26 | if check_user_id(message): 27 | from custom_strategy import CustomOrderManager 28 | CustomOrderManager.send_tg_message() 29 | else: 30 | print('Unauthenticated ID') 31 | bot.send_message(reply_to_message_id=message.message_id, chat_id=message.chat.id, text='您没有操作权限') 32 | 33 | 34 | @bot2.message_handler(commands=['start_market_maker']) 35 | def start_market_maker(message): 36 | if check_user_id(message): 37 | bot2.send_message(reply_to_message_id=message.message_id, chat_id=message.chat.id, text='不支持从telegram启动,请远程登陆服务器执行marketmaker_custom XBTUSD') 38 | else: 39 | bot2.send_message(reply_to_message_id=message.message_id, chat_id=message.chat.id, text='您没有操作权限') 40 | 41 | @bot2.message_handler(commands=['stop_market_maker']) 42 | def stop_market_maker(message): 43 | if check_user_id(message): 44 | from custom_strategy import CustomOrderManager 45 | CustomOrderManager.exit() 46 | else: 47 | bot2.send_message(reply_to_message_id=message.message_id, chat_id=message.chat.id, text='您没有操作权限') 48 | 49 | def run_polling(): 50 | bot.polling() 51 | 52 | if __name__ == '__main__': 53 | bot.polling() -------------------------------------------------------------------------------- /telegram_msg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding:utf-8 -*- 3 | 4 | import telegram 5 | import json 6 | import time 7 | from datetime import datetime 8 | from telegram.error import (TelegramError, Unauthorized, BadRequest, TimedOut, ChatMigrated, NetworkError, RetryAfter) 9 | 10 | TOKEN = '765047482:AAEvSaWe7etm7vP6kxQu03WRY9zNycb7Pcc' 11 | NOTICE_TOKEN = '704593545:AAGEauPlgPergENS6915Ke8W980fJ-SNo8M' 12 | RAILGUN_TOKEN = '865990567:AAGu1LVZk017G-VqoQptZl6xBSJXSdDcXx8' 13 | USER_ID = 480588693 14 | 15 | bot = telegram.Bot(token=TOKEN) 16 | bot2 = telegram.Bot(token=NOTICE_TOKEN) 17 | bot3 = telegram.Bot(token=RAILGUN_TOKEN) 18 | chat_id = USER_ID 19 | last_message_id = 0 20 | 21 | def tg_send_message(content): 22 | """发送给telegram自己消息""" 23 | try: 24 | bot.send_message(chat_id=chat_id, text=content) 25 | except TimedOut as e: 26 | time.sleep(5) 27 | bot.send_message(chat_id=chat_id, text=content) 28 | except RetryAfter as e: 29 | time.sleep(5) 30 | bot.send_message(chat_id=chat_id, text=content) 31 | 32 | def tg_send_railgun_message(content): 33 | """发送给telegram自己消息""" 34 | try: 35 | bot3.send_message(chat_id=chat_id, text=content) 36 | except TimedOut as e: 37 | time.sleep(5) 38 | bot3.send_message(chat_id=chat_id, text=content) 39 | except RetryAfter as e: 40 | time.sleep(5) 41 | bot3.send_message(chat_id=chat_id, text=content) 42 | 43 | def tg_send_important_message(content): 44 | """发送给telegram自己消息""" 45 | try: 46 | bot2.send_message(chat_id=chat_id, text=content) 47 | except TimedOut as e: 48 | time.sleep(5) 49 | bot2.send_message(chat_id=chat_id, text=content) 50 | except RetryAfter as e: 51 | time.sleep(5) 52 | bot2.send_message(chat_id=chat_id, text=content) 53 | 54 | def tg_get_updates(): 55 | """接收bot消息""" 56 | try: 57 | tg_date = bot.get_updates(offset = -1) 58 | except TimedOut as e: 59 | time.sleep(5) 60 | tg_date = bot.get_updates(offset = -1) 61 | except RetryAfter as e: 62 | time.sleep(5) 63 | tg_date = bot.get_updates(offset = -1) 64 | if len(tg_date) == 0: 65 | return None 66 | temp_chat_id = tg_date[-1]['message']['chat']['id'] 67 | message = tg_date[-1]['message']['text'] 68 | message_date = tg_date[-1]['message']['date'].timestamp() 69 | message_id = tg_date[-1]['message']['message_id'] 70 | global last_message_id 71 | 72 | if(temp_chat_id != chat_id): 73 | return None 74 | if (abs(time.time() - message_date) < 6 and last_message_id != message_id): #5秒内的消息才会处理 75 | last_message_id = message_id 76 | return message 77 | else: 78 | return None 79 | 80 | def tg_get_railgun_updates(): 81 | """接收bot3消息""" 82 | try: 83 | tg_date = bot3.get_updates(offset = -1) 84 | except TimedOut as e: 85 | time.sleep(5) 86 | tg_date = bot3.get_updates(offset = -1) 87 | except RetryAfter as e: 88 | time.sleep(5) 89 | tg_date = bot3.get_updates(offset = -1) 90 | if len(tg_date) == 0: 91 | return None 92 | temp_chat_id = tg_date[-1]['message']['chat']['id'] 93 | message = tg_date[-1]['message']['text'] 94 | message_date = tg_date[-1]['message']['date'].timestamp() 95 | message_id = tg_date[-1]['message']['message_id'] 96 | global last_message_id 97 | 98 | if(temp_chat_id != chat_id): 99 | return None 100 | if (abs(time.time() - message_date) < 6 and last_message_id != message_id): #5秒内的消息才会处理 101 | last_message_id = message_id 102 | return message 103 | else: 104 | return None 105 | 106 | def tg_get_important_updates(): 107 | """接收bot2重要消息""" 108 | try: 109 | tg_date = bot2.get_updates(offset = -1) 110 | except TimedOut as e: 111 | time.sleep(5) 112 | tg_date = bot2.get_updates(offset = -1) 113 | except RetryAfter as e: 114 | time.sleep(5) 115 | tg_date = bot2.get_updates(offset = -1) 116 | if len(tg_date) == 0: 117 | return None 118 | temp_chat_id = tg_date[-1]['message']['chat']['id'] 119 | message = tg_date[-1]['message']['text'] 120 | message_date = tg_date[-1]['message']['date'].timestamp() 121 | message_id = tg_date[-1]['message']['message_id'] 122 | global last_message_id 123 | 124 | if(temp_chat_id != chat_id): 125 | return None 126 | if (abs(time.time() - message_date) < 20 and last_message_id != message_id): #20秒内的消息才会处理 127 | last_message_id = message_id 128 | return message 129 | else: 130 | return None -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judgelight/Bitmex_market_maker/1b6e09d2d67bd903a834ef671dcae57ad4d0fc2b/utils/__init__.py -------------------------------------------------------------------------------- /utils/constants.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | # Constants 3 | XBt_TO_XBT = 100000000 4 | VERSION = 'v1.1' 5 | try: 6 | VERSION = str(subprocess.check_output(["git", "describe", "--tags"], stderr=subprocess.DEVNULL).rstrip()) 7 | except Exception as e: 8 | # git not available, ignore 9 | pass 10 | -------------------------------------------------------------------------------- /utils/dotdict.py: -------------------------------------------------------------------------------- 1 | class dotdict(dict): 2 | """dot.notation access to dictionary attributes""" 3 | def __getattr__(self, attr): 4 | return self.get(attr) 5 | __setattr__ = dict.__setitem__ 6 | __delattr__ = dict.__delitem__ 7 | -------------------------------------------------------------------------------- /utils/errors.py: -------------------------------------------------------------------------------- 1 | class AuthenticationError(Exception): 2 | pass 3 | 4 | class MarketClosedError(Exception): 5 | pass 6 | 7 | class MarketEmptyError(Exception): 8 | pass 9 | -------------------------------------------------------------------------------- /utils/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from market_maker.settings import settings 3 | 4 | 5 | def setup_custom_logger(name, log_level=settings.LOG_LEVEL): 6 | formatter = logging.Formatter(fmt='%(asctime)s - %(levelname)s - %(module)s - %(message)s') 7 | 8 | handler = logging.StreamHandler() 9 | handler.setFormatter(formatter) 10 | 11 | logger = logging.getLogger(name) 12 | logger.setLevel(log_level) 13 | logger.addHandler(handler) 14 | return logger 15 | -------------------------------------------------------------------------------- /utils/math.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | def toNearest(num, tickSize): 4 | """Given a number, round it to the nearest tick. Very useful for sussing float error 5 | out of numbers: e.g. toNearest(401.46, 0.01) -> 401.46, whereas processing is 6 | normally with floats would give you 401.46000000000004. 7 | Use this after adding/subtracting/multiplying numbers.""" 8 | tickDec = Decimal(str(tickSize)) 9 | return float((Decimal(round(num / tickSize, 0)) * tickDec)) 10 | -------------------------------------------------------------------------------- /ws/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judgelight/Bitmex_market_maker/1b6e09d2d67bd903a834ef671dcae57ad4d0fc2b/ws/__init__.py -------------------------------------------------------------------------------- /ws/ws_thread.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import websocket 3 | import threading 4 | import traceback 5 | import ssl 6 | from time import sleep 7 | import json 8 | import decimal 9 | import logging 10 | from market_maker.settings import settings 11 | from market_maker.auth.APIKeyAuth import generate_expires, generate_signature 12 | from market_maker.utils.log import setup_custom_logger 13 | from market_maker.utils.math import toNearest 14 | from future.utils import iteritems 15 | from future.standard_library import hooks 16 | from telegram_msg import tg_send_message, tg_send_important_message 17 | with hooks(): # Python 2/3 compat 18 | from urllib.parse import urlparse, urlunparse 19 | 20 | 21 | # Connects to BitMEX websocket for streaming realtime data. 22 | # The Marketmaker still interacts with this as if it were a REST Endpoint, but now it can get 23 | # much more realtime data without heavily polling the API. 24 | # 25 | # The Websocket offers a bunch of data as raw properties right on the object. 26 | # On connect, it synchronously asks for a push of all this data then returns. 27 | # Right after, the MM can start using its data. It will be updated in realtime, so the MM can 28 | # poll as often as it wants. 29 | class BitMEXWebsocket(): 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, apiKey, apiSecret): 35 | self.apiKey = apiKey 36 | self.apiSecret = apiSecret 37 | self.logger = logging.getLogger('root') 38 | self.__reset() 39 | 40 | def __del__(self): 41 | self.exit() 42 | 43 | def connect(self, endpoint="", symbol="XBTN15", shouldAuth=True): 44 | '''Connect to the websocket and initialize data stores.''' 45 | 46 | self.logger.debug("Connecting WebSocket.") 47 | self.symbol = symbol 48 | self.shouldAuth = shouldAuth 49 | 50 | # We can subscribe right in the connection querystring, so let's build that. 51 | # Subscribe to all pertinent endpoints 52 | subscriptions = [sub + ':' + symbol for sub in ["quote", "trade"]] 53 | subscriptions += ["instrument"] # We want all of them 54 | if self.shouldAuth: 55 | subscriptions += [sub + ':' + symbol for sub in ["order", "execution"]] 56 | subscriptions += ["margin", "position"] 57 | 58 | # Get WS URL and connect. 59 | urlParts = list(urlparse(endpoint)) 60 | urlParts[0] = urlParts[0].replace('http', 'ws') 61 | urlParts[2] = "/realtime?subscribe=" + ",".join(subscriptions) 62 | wsURL = urlunparse(urlParts) 63 | self.logger.info("Connecting to %s" % wsURL) 64 | self.__connect(wsURL) 65 | self.logger.info('Connected to WS. Waiting for data images, this may take a moment...') 66 | 67 | # Connected. Wait for partials 68 | self.__wait_for_symbol(symbol) 69 | if self.shouldAuth: 70 | self.__wait_for_account() 71 | self.logger.info('Got all market data. Starting.') 72 | 73 | # 74 | # Data methods 75 | # 76 | def get_instrument(self, symbol): 77 | '''Get the raw instrument data for this symbol.''' 78 | instruments = self.data['instrument'] 79 | matchingInstruments = [i for i in instruments if i['symbol'] == symbol] 80 | if len(matchingInstruments) == 0: 81 | raise Exception("Unable to find instrument or index with symbol: " + symbol) 82 | instrument = matchingInstruments[0] 83 | # Turn the 'tickSize' into 'tickLog' for use in rounding 84 | # http://stackoverflow.com/a/6190291/832202 85 | instrument['tickLog'] = decimal.Decimal(str(instrument['tickSize'])).as_tuple().exponent * -1 86 | return instrument 87 | 88 | def get_ticker(self, symbol): 89 | '''Return a ticker object. Generated from instrument.''' 90 | 91 | instrument = self.get_instrument(symbol) 92 | 93 | # If this is an index, we have to get the data from the last trade. 94 | if instrument['symbol'][0] == '.': 95 | ticker = {} 96 | ticker['mid'] = ticker['buy'] = ticker['sell'] = ticker['last'] = instrument['markPrice'] 97 | # Normal instrument 98 | else: 99 | bid = instrument['bidPrice'] or instrument['lastPrice'] 100 | ask = instrument['askPrice'] or instrument['lastPrice'] 101 | ticker = { 102 | "last": instrument['lastPrice'], 103 | "buy": bid, 104 | "sell": ask, 105 | "mid": (bid + ask) / 2 106 | } 107 | 108 | # The instrument has a tickSize. Use it to round values. 109 | return {k: toNearest(float(v or 0), instrument['tickSize']) for k, v in iteritems(ticker)} 110 | 111 | def funds(self): 112 | return self.data['margin'][0] 113 | 114 | def market_depth(self, symbol): 115 | raise NotImplementedError('orderBook is not subscribed; use askPrice and bidPrice on instrument') 116 | # return self.data['orderBook25'][0] 117 | 118 | def open_orders(self, clOrdIDPrefix): 119 | orders = self.data['order'] 120 | # Filter to only open orders (leavesQty > 0) and those that we actually placed 121 | return [o for o in orders if str(o['clOrdID']).startswith(clOrdIDPrefix) and o['leavesQty'] > 0] 122 | 123 | def position(self, symbol): 124 | positions = self.data['position'] 125 | pos = [p for p in positions if p['symbol'] == symbol] 126 | if len(pos) == 0: 127 | # No position found; stub it 128 | return {'avgCostPrice': 0, 'avgEntryPrice': 0, 'currentQty': 0, 'symbol': symbol} 129 | return pos[0] 130 | 131 | def recent_trades(self): 132 | return self.data['trade'] 133 | 134 | # 135 | # Lifecycle methods 136 | # 137 | def error(self, err): 138 | self._error = err 139 | self.logger.error(err) 140 | self.exit() 141 | 142 | def exit(self): 143 | self.exited = True 144 | self.ws.close() 145 | 146 | # 147 | # Private methods 148 | # 149 | 150 | def __connect(self, wsURL): 151 | '''Connect to the websocket in a thread.''' 152 | self.logger.debug("Starting thread") 153 | 154 | ssl_defaults = ssl.get_default_verify_paths() 155 | sslopt_ca_certs = {'ca_certs': ssl_defaults.cafile} 156 | self.ws = websocket.WebSocketApp(wsURL, 157 | on_message=self.__on_message, 158 | on_close=self.__on_close, 159 | on_open=self.__on_open, 160 | on_error=self.__on_error, 161 | header=self.__get_auth() 162 | ) 163 | 164 | setup_custom_logger('websocket', log_level=settings.LOG_LEVEL) 165 | self.wst = threading.Thread(target=lambda: self.ws.run_forever(sslopt=sslopt_ca_certs)) 166 | self.wst.daemon = True 167 | self.wst.start() 168 | self.logger.info("Started thread") 169 | 170 | # Wait for connect before continuing 171 | conn_timeout = 5 172 | while (not self.ws.sock or not self.ws.sock.connected) and conn_timeout and not self._error: 173 | sleep(1) 174 | conn_timeout -= 1 175 | 176 | if not conn_timeout or self._error: 177 | self.logger.error("Couldn't connect to WS! Exiting.") 178 | tg_send_important_message("Couldn't connect to WS! Exiting.") 179 | self.exit() 180 | sys.exit(1) 181 | 182 | def __get_auth(self): 183 | '''Return auth headers. Will use API Keys if present in settings.''' 184 | 185 | if self.shouldAuth is False: 186 | return [] 187 | 188 | self.logger.info("Authenticating with API Key.") 189 | # To auth to the WS using an API key, we generate a signature of a nonce and 190 | # the WS API endpoint. 191 | nonce = generate_expires() 192 | return [ 193 | "api-expires: " + str(nonce), 194 | "api-signature: " + generate_signature(self.apiSecret, 'GET', '/realtime', nonce, ''), 195 | "api-key:" + self.apiKey 196 | ] 197 | 198 | def __wait_for_account(self): 199 | '''On subscribe, this data will come down. Wait for it.''' 200 | # Wait for the keys to show up from the ws 201 | while not {'margin', 'position', 'order'} <= set(self.data): 202 | sleep(0.1) 203 | 204 | def __wait_for_symbol(self, symbol): 205 | '''On subscribe, this data will come down. Wait for it.''' 206 | while not {'instrument', 'trade', 'quote'} <= set(self.data): 207 | sleep(0.1) 208 | 209 | def __send_command(self, command, args): 210 | '''Send a raw command.''' 211 | self.ws.send(json.dumps({"op": command, "args": args or []})) 212 | 213 | def __on_message(self, message): 214 | '''Handler for parsing WS messages.''' 215 | message = json.loads(message) 216 | self.logger.debug(json.dumps(message)) 217 | 218 | table = message['table'] if 'table' in message else None 219 | action = message['action'] if 'action' in message else None 220 | try: 221 | if 'subscribe' in message: 222 | if message['success']: 223 | self.logger.debug("Subscribed to %s." % message['subscribe']) 224 | else: 225 | self.error("Unable to subscribe to %s. Error: \"%s\" Please check and restart." % 226 | (message['request']['args'][0], message['error'])) 227 | elif 'status' in message: 228 | if message['status'] == 400: 229 | self.error(message['error']) 230 | if message['status'] == 401: 231 | self.error("API Key incorrect, please check and restart.") 232 | elif action: 233 | 234 | if table not in self.data: 235 | self.data[table] = [] 236 | 237 | if table not in self.keys: 238 | self.keys[table] = [] 239 | 240 | # There are four possible actions from the WS: 241 | # 'partial' - full table image 242 | # 'insert' - new row 243 | # 'update' - update row 244 | # 'delete' - delete row 245 | if action == 'partial': 246 | self.logger.debug("%s: partial" % table) 247 | self.data[table] += message['data'] 248 | # Keys are communicated on partials to let you know how to uniquely identify 249 | # an item. We use it for updates. 250 | self.keys[table] = message['keys'] 251 | elif action == 'insert': 252 | self.logger.debug('%s: inserting %s' % (table, message['data'])) 253 | self.data[table] += message['data'] 254 | 255 | # Limit the max length of the table to avoid excessive memory usage. 256 | # Don't trim orders because we'll lose valuable state if we do. 257 | if table not in ['order', 'orderBookL2'] and len(self.data[table]) > BitMEXWebsocket.MAX_TABLE_LEN: 258 | self.data[table] = self.data[table][(BitMEXWebsocket.MAX_TABLE_LEN // 2):] 259 | 260 | elif action == 'update': 261 | self.logger.debug('%s: updating %s' % (table, message['data'])) 262 | # Locate the item in the collection and update it. 263 | for updateData in message['data']: 264 | item = findItemByKeys(self.keys[table], self.data[table], updateData) 265 | if not item: 266 | continue # No item found to update. Could happen before push 267 | 268 | # Log executions 269 | if table == 'order': 270 | is_canceled = 'ordStatus' in updateData and updateData['ordStatus'] == 'Canceled' 271 | if 'cumQty' in updateData and not is_canceled: 272 | contExecuted = updateData['cumQty'] - item['cumQty'] 273 | if contExecuted > 0: 274 | instrument = self.get_instrument(item['symbol']) 275 | self.logger.info("Execution: %s %d Contracts of %s at %.*f" % 276 | (item['side'], contExecuted, item['symbol'], 277 | instrument['tickLog'], item['price'])) 278 | 279 | # Update this item. 280 | item.update(updateData) 281 | 282 | # Remove canceled / filled orders 283 | if table == 'order' and item['leavesQty'] <= 0: 284 | self.data[table].remove(item) 285 | 286 | elif action == 'delete': 287 | self.logger.debug('%s: deleting %s' % (table, message['data'])) 288 | # Locate the item in the collection and remove it. 289 | for deleteData in message['data']: 290 | item = findItemByKeys(self.keys[table], self.data[table], deleteData) 291 | self.data[table].remove(item) 292 | else: 293 | raise Exception("Unknown action: %s" % action) 294 | except: 295 | self.logger.error(traceback.format_exc()) 296 | 297 | def __on_open(self): 298 | self.logger.debug("Websocket Opened.") 299 | 300 | def __on_close(self): 301 | self.logger.info('Websocket Closed') 302 | self.exit() 303 | 304 | def __on_error(self, ws, error): 305 | if not self.exited: 306 | self.error(error) 307 | 308 | def __reset(self): 309 | self.data = {} 310 | self.keys = {} 311 | self.exited = False 312 | self._error = None 313 | 314 | 315 | def findItemByKeys(keys, table, matchData): 316 | for item in table: 317 | matched = True 318 | for key in keys: 319 | if item[key] != matchData[key]: 320 | matched = False 321 | if matched: 322 | return item 323 | 324 | if __name__ == "__main__": 325 | # create console handler and set level to debug 326 | logger = logging.getLogger() 327 | logger.setLevel(logging.DEBUG) 328 | ch = logging.StreamHandler() 329 | # create formatter 330 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 331 | # add formatter to ch 332 | ch.setFormatter(formatter) 333 | logger.addHandler(ch) 334 | ws = BitMEXWebsocket() 335 | ws.logger = logger 336 | ws.connect("https://testnet.bitmex.com/api/v1") 337 | while(ws.ws.sock.connected): 338 | sleep(1) 339 | 340 | --------------------------------------------------------------------------------