├── requirements.txt ├── config.py ├── LICENSE ├── README.md ├── .gitignore └── run.py /requirements.txt: -------------------------------------------------------------------------------- 1 | pybit>=1.1.18 2 | numpy>=1.18.0 -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration. 3 | Use at your own risk. 4 | """ 5 | 6 | # Auth stuff. 7 | API_KEY = '' 8 | PRIVATE_KEY = '' 9 | 10 | # Set the market symbol and the coin associated with the market. 11 | SYMBOL = 'BTCUSD' 12 | COIN = 'BTC' 13 | 14 | # How much of your balance to use in decimal 15 | # i.e. 1 = 100% (1x), 0.1 = 10% (0.1x), 100 = 10000% = (100x) 16 | EQUITY = 20 17 | 18 | # Number of orders and the range. 19 | RANGE = 0.04 # in decimal of last price i.e. 0.1 = 10% of last price 20 | NUM_ORDERS = 20 # number of orders ON EACH SIDE i.e. 20 = 20 buys, 20 sells 21 | 22 | # How many times should we check for updates. 23 | POLLING_RATE = 2 # in per seconds i.e. time.sleep(1/polling_rate) 24 | 25 | # Take profit distance in percentage of price. 26 | TP_DIST = 0.003 27 | 28 | # Stop distance...SET IT LARGER THAN THE ORDER RANGE/2 FOR OBVIOUS REASONS. 29 | # If STOP_DIST = None or 0, no stop will be set. 30 | STOP_DIST = 0.025 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 verata-veritatis 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bybit-market-maker-v2 2 | A very primitive Python-based market maker bot for Bybit, meant to be used a sample. Users can fork this repository and customize their own algorithms. Uses the [`pybit`](https://github.com/verata-veritatis/pybit) module. 3 | 4 | [![Python 3.6](https://img.shields.io/badge/python-3.6%20|%203.7%20|%203.8-blue.svg)](https://www.python.org/downloads/) 5 | 6 | ![Market Maker](https://i.imgur.com/XZc8tUg.png) 7 | 8 | ## Usage 9 | Be sure you have [`pybit`](https://github.com/verata-veritatis/pybit) installed: 10 | ``` 11 | pip install pybit 12 | ``` 13 | Next, clone or download this repository and extract. Modify `config.py` to your liking, navigate to the project via CLI, and `python run.py`. 14 | 15 | ## How It Works 16 | - A given number of long and short orders are spaced evenly from the current last price up to a user-defined range. The last price at the time of placement is considered the *median*. 17 | - If a single side of orders begins to fill, the bot cancels the other side of orders and places a take-profit at a user-defined distance away from the entry price. 18 | - A stop-loss can be set at a distance that is also user-defined. 19 | - If the position is closed, whether by close orders or stop loss, new long and short orders are placed and the cycle continues. 20 | - This strategy has little-to-no risk management and is meant to be used by the user as a "starting point". By default, a pretty distant stop-loss is used. It hurts. 21 | 22 | ## Disclaimer 23 | *This project is still in the early stages of development. Please refrain from using the bot on livenet until it is stable!* 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Example user template template 3 | ### Example user template 4 | 5 | # IntelliJ project files 6 | .idea 7 | *.iml 8 | out 9 | gen 10 | ### Python template 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # C extensions 17 | *.so 18 | 19 | # Distribution / packaging 20 | .Python 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | share/python-wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .nox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | nosetests.xml 57 | coverage.xml 58 | *.cover 59 | *.py,cover 60 | .hypothesis/ 61 | .pytest_cache/ 62 | cover/ 63 | 64 | # Translations 65 | *.mo 66 | *.pot 67 | 68 | # Django stuff: 69 | *.log 70 | local_settings.py 71 | db.sqlite3 72 | db.sqlite3-journal 73 | 74 | # Flask stuff: 75 | instance/ 76 | .webassets-cache 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | 84 | # PyBuilder 85 | .pybuilder/ 86 | target/ 87 | 88 | # Jupyter Notebook 89 | .ipynb_checkpoints 90 | 91 | # IPython 92 | profile_default/ 93 | ipython_config.py 94 | 95 | # pyenv 96 | # For a library or package, you might want to ignore these files since the code is 97 | # intended to run in multiple environments; otherwise, check them in: 98 | # .python-version 99 | 100 | # pipenv 101 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 102 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 103 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 104 | # install all needed dependencies. 105 | #Pipfile.lock 106 | 107 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 108 | __pypackages__/ 109 | 110 | # Celery stuff 111 | celerybeat-schedule 112 | celerybeat.pid 113 | 114 | # SageMath parsed files 115 | *.sage.py 116 | 117 | # Environments 118 | .env 119 | .venv 120 | env/ 121 | venv/ 122 | ENV/ 123 | env.bak/ 124 | venv.bak/ 125 | 126 | # Spyder project settings 127 | .spyderproject 128 | .spyproject 129 | 130 | # Rope project settings 131 | .ropeproject 132 | 133 | # mkdocs documentation 134 | /site 135 | 136 | # mypy 137 | .mypy_cache/ 138 | .dmypy.json 139 | dmypy.json 140 | 141 | # Pyre type checker 142 | .pyre/ 143 | 144 | # pytype static type analyzer 145 | .pytype/ 146 | 147 | # Cython debug symbols 148 | cython_debug/ 149 | 150 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from pybit import HTTP 2 | from pybit.exceptions import InvalidRequestError 3 | from datetime import datetime as dt 4 | 5 | import numpy as np 6 | import time 7 | 8 | import config 9 | 10 | 11 | def _print(message, level='info'): 12 | """ 13 | Just a custom print function. Better than logging. 14 | """ 15 | if level == 'position': 16 | print(f'{dt.utcnow()} - {message}.', end='\r') 17 | else: 18 | print(f'{dt.utcnow()} - {level.upper()} - {message}.') 19 | 20 | 21 | def scale_qtys(x, n): 22 | """ 23 | Will create a list of qtys on both long and short 24 | side that scale additively i.e. 25 | [5, 4, 3, 2, 1, -1, -2, -3, -4, -5]. 26 | 27 | x: How much of your balance to use. 28 | n: Number of orders. 29 | """ 30 | n_ = x / ((n + n ** 2) / 2) 31 | long_qtys = [int(n_ * i) for i in reversed(range(1, n + 1))] 32 | short_qtys = [-i for i in long_qtys] 33 | return long_qtys + short_qtys[::-1] 34 | 35 | 36 | if __name__ == '__main__': 37 | print('\n--- SAMPLE MARKET MAKER V2 ---') 38 | print('For pybit, created by verata_veritatis.') 39 | 40 | if not config.API_KEY or not config.PRIVATE_KEY: 41 | raise PermissionError('An API key is required to run this program.') 42 | 43 | print('\nUSE AT YOUR OWN RISK!!!\n') 44 | time.sleep(1) 45 | 46 | _print('Opening session') 47 | s = HTTP( 48 | api_key=config.API_KEY, 49 | api_secret=config.PRIVATE_KEY, 50 | logging_level=50, 51 | retry_codes={10002, 10006}, 52 | ignore_codes={20001, 30034}, 53 | force_retry=True, 54 | retry_delay=3 55 | ) 56 | 57 | # Auth sanity test. 58 | try: 59 | s.get_wallet_balance() 60 | except InvalidRequestError as e: 61 | raise PermissionError('API key is invalid.') 62 | else: 63 | _print('Authenticated sanity check passed') 64 | 65 | # Set leverage to cross. 66 | try: 67 | s.set_leverage( 68 | symbol=config.SYMBOL, 69 | leverage=0 70 | ) 71 | except InvalidRequestError as e: 72 | if e.status_code == 34015: 73 | _print('Margin is already set to cross') 74 | else: 75 | _print('Forced cross margin') 76 | 77 | print('\n------------------------------\n') 78 | 79 | # Main loop. 80 | while True: 81 | 82 | # Cancel orders. 83 | s.cancel_all_active_orders( 84 | symbol=config.SYMBOL 85 | ) 86 | 87 | # Close position if open. 88 | s.close_position( 89 | symbol=config.SYMBOL 90 | ) 91 | 92 | # Grab the last price. 93 | _print('Checking last price') 94 | last = float(s.latest_information_for_symbol( 95 | symbol=config.SYMBOL 96 | )['result'][0]['last_price']) 97 | price_range = config.RANGE * last 98 | 99 | # Create order price span. 100 | _print('Generating order prices') 101 | prices = np.linspace( 102 | last - price_range/2, last + price_range/2, config.NUM_ORDERS * 2 103 | ) 104 | tp_dp = config.TP_DIST * last 105 | 106 | # Scale quantity additively (1x, 2x, 3x, 4x). 107 | _print('Generating order quantities') 108 | balance_in_usd = float(s.get_wallet_balance( 109 | coin=config.COIN 110 | )['result'][config.COIN]['available_balance']) * last 111 | available_equity = balance_in_usd * config.EQUITY 112 | qtys = scale_qtys(available_equity, config.NUM_ORDERS) 113 | 114 | # Prepare orders. 115 | orders = [ 116 | { 117 | 'symbol': config.SYMBOL, 118 | 'side': 'Buy' if qtys[k] > 0 else 'Sell', 119 | 'order_type': 'Limit', 120 | 'qty': abs(qtys[k]), 121 | 'price': int(prices[k]), 122 | 'time_in_force': 'GoodTillCancel', 123 | } for k in range(len(qtys)) 124 | ] 125 | _print('Submitting orders') 126 | responses = s.place_active_order_bulk(orders=orders) 127 | 128 | # Let's create an ID list of buys and sells as a dict. 129 | _print('Orders submitted successfully') 130 | order_ids = { 131 | 'Buy': [i['result']['order_id'] 132 | for i in responses if i['result']['side'] == 'Buy'], 133 | 'Sell': [i['result']['order_id'] 134 | for i in responses if i['result']['side'] == 'Sell'], 135 | } 136 | 137 | # In-position loop. 138 | while True: 139 | 140 | # Await position. 141 | _print('Awaiting position') 142 | while not abs(s.my_position( 143 | symbol=config.SYMBOL 144 | )['result']['size']): 145 | time.sleep(1 / config.POLLING_RATE) 146 | 147 | # When we have a position, get the size and cancel all the 148 | # opposing orders. 149 | if s.my_position( 150 | symbol=config.SYMBOL 151 | )['result']['side'] == 'Buy': 152 | to_cancel = [{ 153 | 'symbol': config.SYMBOL, 154 | 'order_id': i 155 | } for i in order_ids['Sell']] 156 | elif s.my_position( 157 | symbol=config.SYMBOL 158 | )['result']['side'] == 'Sell': 159 | to_cancel = [{ 160 | 'symbol': config.SYMBOL, 161 | 'order_id': i 162 | } for i in order_ids['Buy']] 163 | else: 164 | # Position was closed immediately for some reason. Restart. 165 | _print('Position closed unexpectedly—resetting') 166 | break 167 | s.cancel_active_order_bulk( 168 | orders=to_cancel 169 | ) 170 | 171 | # Set a TP. 172 | p = s.my_position(symbol=config.SYMBOL)['result'] 173 | e = float(p['entry_price']) 174 | tp_response = s.place_active_order( 175 | symbol=config.SYMBOL, 176 | side='Sell' if p['side'] == 'Buy' else 'Buy', 177 | order_type='Limit', 178 | qty=p['size'], 179 | price=int(e + tp_dp if p['side'] == 'Buy' else e - tp_dp), 180 | time_in_force='GoodTillCancel', 181 | reduce_only=True 182 | ) 183 | curr_size = p['size'] 184 | 185 | # Set a position stop. 186 | if config.STOP_DIST: 187 | e = float(p['entry_price']) 188 | if p['side'] == 'Buy': 189 | stop_price = e - (e * config.STOP_DIST) 190 | else: 191 | stop_price = e + (e * config.STOP_DIST) 192 | s.set_trading_stop( 193 | symbol=config.SYMBOL, 194 | stop_loss=int(stop_price) 195 | ) 196 | 197 | # Monitor position. 198 | print('\n------------------------------\n') 199 | while p['size']: 200 | 201 | # Get the size with sign based on side. 202 | sign = p['size'] if p['side'] == 'Buy' else -p['size'] 203 | pnl_sign = '+' if float(p['unrealised_pnl']) > 0 else '-' 204 | 205 | # Show status. 206 | _print( 207 | f'Size: {sign} ({float(p["effective_leverage"]):.2f}x), ' 208 | f'Entry: {float(p["entry_price"]):.2f}, ' 209 | f'Balance: {float(p["wallet_balance"]):.8f}, ' 210 | f'PNL: {pnl_sign}{abs(float(p["unrealised_pnl"])):.8f}', 211 | level='position' 212 | ) 213 | 214 | # Sleep and re-fetch. 215 | time.sleep(1 / config.POLLING_RATE) 216 | p = s.my_position(symbol=config.SYMBOL)['result'] 217 | 218 | # If size has changed, update TP based on entry and size. 219 | if p['size'] > curr_size: 220 | e = float(p['entry_price']) 221 | tp_price = e + tp_dp if p['side'] == 'Buy' else e - tp_dp 222 | s.replace_active_order( 223 | symbol=config.SYMBOL, 224 | order_id=tp_response['result']['order_id'], 225 | p_r_price=int(tp_price), 226 | p_r_qty=p['size'] 227 | ) 228 | curr_size = p['size'] 229 | 230 | # Position has closed—get PNL information. 231 | print(' ' * 120, end='\r') 232 | pnl_r = s.closed_profit_and_loss( 233 | symbol=config.SYMBOL 234 | )['result']['data'][0] 235 | 236 | # Store PNL data as string. 237 | side = 'Buy' if pnl_r['side'].lower() == 'sell' else 'Sell' 238 | pos = f'{side} {pnl_r["qty"]}' 239 | prc = f'{pnl_r["avg_entry_price"]} -> {pnl_r["avg_exit_price"]}' 240 | 241 | # Display PNL info. 242 | _print(f'Position closed successfully: {pos} ({prc})') 243 | print('\n------------------------------\n') 244 | break 245 | --------------------------------------------------------------------------------