Build a Crypto Market Making Bot in Python

Kollider
5 min readNov 23, 2021

This is a follow-up to our API guide. Note that this is for educational purposes only. Please don’t take any of this as financial advice.

If you’ve read our market making article, you may be excited to take the next step in understanding the strategy. But what better way to learn than by doing? In that spirit, we’ve open-sourced an example market maker (“EMM”) bot to teach you in practice.

In this tutorial, we assume that you are already familiar with using git and terminals. If this is not you, but you still want to try Kollider out, let us know! We’ll help you get started regardless. By the way, we’ve talked about how trading manually is still a viable way to trade, so you don’t need a bot to get started.

Getting the Code

To get started, you need to clone the example_market_maker code. You’ll need python and pip (version >= 3.56) to run the script. Once you’ve cloned it, go to the maker’s directory and run pip install:

cd example_market_maker
pip install -r requirements.txt

This will add requirements including Kollider’s websocket client.

Anatomy of the Example Market Maker

If you haven’t yet, quickly glance through the simplified concept behind market making, as that’ll help you pick up the rest of this tutorial more quickly.

The EMM is primarily composed of four moving parts:

  1. Connecting to Kollider’s websocket to get product and market data
  2. Calculating a reference price from that data
  3. Calculating our order prices
  4. Placing our orders (note: for safety reasons, we set the default to just “dry run” and show you the order quantity, prices, and side that it would have placed)

Let’s dig deeper.

Connecting and Getting Data

Before getting started, you need to enter your API credentials at the top of config.yaml

api_key: ""
api_secret: ""
api_passphrase: ""
ws_url: "wss://api.kollider.xyz/v1/ws/"
rest_url: "https://api.kollider.xyz/v1/"

If you don’t have an API key or secret yet, please follow the instructions on this page.

If you run the default settings of EMM (after API credentials, of course!), you should already get some data through the websocket. So, let’s explore some of the code that makes that happen.

Once you run pip install -r requirements.txt, pip will download Kollider’s websocket client, which MarketMaker subclasses.

from kollider_api_client.ws import *

The current version of EMM will subscribe to orderbook data, the index price, and the state of your existing positions (none initially for your account). To see this, you can navigate to def run()in main.py and check out the subscribe and fetch lines of code.

# Subscribing to index prices.
self.sub_index_price([self.conf["index_symbol"]])
self.sub_position_states()
self.sub_orderbook_l2(self.conf["symbol"])

MarketMaker overrides the method on_message to handle and parse all of the data coming from the websocket. If you wanted to handle some messages differently, you’d want to follow parse_msg(..) and alter the code there.

def on_message(self, _, msg):
parse_msg(self.exchange_state, msg)

Calculating Reference Price

Following the framework given in Demystifying Market Making, the market maker centres around a reference_price. In EMM, we are calculating this in the update_start_price() method

def update_start_prices(self):
# You could change this to your own reference price.
self.reference_price.update_price(self.exchange_state)

The reference prices are created by calculator classes. Currently, there are two calculations available: (1) one that uses the index price; and (2) another that uses the mid-price of the orderbook.

You can find them in the src/calculatorsdirectory. You can add your own there. They will generally follow this interface, but they don’t need to strictly adhere to that.

class PriceCalc(object):    # Calculates a reference price.
def __init__(self):
self.is_done = False
self.price = None
def is_ready(self):
return self.is_done
def get_price(self):
return self.price

def update_price(self, exchange_state: ExchangeState):
return self.price

Calculating Orders

Orders are created in two main steps. The first is to calculate the “start prices”, meaning the best bid and ask prices that are going to be closest to the centre of the market. That largely happens in update_start_prices(..) where we calculate the reference price and finally the start prices by adding an offset to that. According to the market maker article we are following, we would add a lean. This would happen in this same method, but we leave that exercise to you. It is optional and is not currently in the EMM.

Lastly, we determine our actual order prices and quantities within the create_orders method. This is controlled by the max_long_pos_btc and max_short_pos_btc settings in config.yaml and is used in the following code in create_orders:

long_btc_remaining = self.long_btc_remaining()
short_btc_remaining = self.short_btc_remaining()

And then “builds the order” inside the build_order and get_order_price methods by applying the offset_pct and stack_pct as a multiple of the index from each side’s start prices.

Note that you’ll see rounding like this on order prices because, like most exchanges, it is critical for the exchange to receive prices that actually match the prices separated by tick size of that product:

order_price = toNearest(start_price + index * start_price * conf[“trading_params”][“stack_pct”], tick_size)

Dry Run and Other Settings

As we have mentioned, this project is meant for educational purposes. With that, we have added a dry run flag in config.yaml

enable_dry_run: true # dry run will not send order and only print them to screen

Setting that flag to true prevents the EMM from sending out orders, and instead it will print the orders it would have sent. For example:

Dry run. Would place the following orders:

Ask 70 @ price 59100.5
Ask 60 @ price 59041.5
Ask 50 @ price 58982.5
Bid 50 @ price 58279.0
Bid 60 @ price 58220.5
Bid 70 @ price 58162.0

Set It and Turn It On

Now that you have added your API credentials and set the dry run to true in config.yaml, you are ready to run the EMM. You can do so from the project’s root folder by running

python src/main.py

Placing Orders

If the dry run is set to false, the EMM will attempt to place orders. This part largely happens in the converge_orders method. This takes the order list created in the create_orders method and determines whether to send new or amend previously existing orders if there has been more than a relist_tolerance (from config.yaml) change in prices.

abs((desired_order.price / order.price) — 1) > trading_params[“relist_tolerance”]

Trading Live and Moving Forward

(NOTE: the settings shown below are not meant to be a recommendation for the reader, but just to show which parameters we are talking about.)

We’ve set it to not trade by default (i.e., dry run is enabled) and let you see what orders the bot would place first. If you do set dry run to false, it would probably make sense to keep the spreads wide by increasing these settings:

min_spread: 0.02
offset_pct: 0.02
stack_pct: 0.02

Furthermore, it would make sense to lower the capital and position risk settings to minimize any potential losses as you continue to learn:

start_order_size: 10
order_step_size: 10
num_levels: 2
max_long_pos_btc: 0.002
max_short_pos_btc: 0.002

If you’re looking for ideas on how to continue expanding the EMM for your own purposes, one potential idea is to look at how others think of market making. Our EMM follows our market making article, and you can start with that by adding a lean parameter and algorithm, but there could be a multitude of ways you can vary up your MM education. You can also check out the other articles in our blog to start getting ideas of how to maybe backtest your EMM and/or adapt it to be a spread trade.

Happy learning!

Know anyone else interested in learning about bitcoin, Lightning, and trading? Follow us on Twitter and help us share the word.

--

--

Kollider

Building new ways to access cryptocurrency markets.