Guides
20 min

We built the simplest perpetual quoter we could

We’ve built an extremely simple market making bot to use in two ways: 1) as a template which automates the boring parts and 2) as a helper to semi-automate your trading while you manage positions on-screen.

Thalex X
Published on
Dec 8, 2024
Written by
Mart

Overview

At Thalex, we believe every trader should learn how to code. It gives you leverage in your ability to structure your thinking and structure your trades.

You don’t need to be a seasoned developer to trade algorithmically. AI is shrinking learning curves from years to months or even weeks. Besides, algorithmic trading is most often a matter of keeping your code as simple as possible.

We've created a very simple Perpetual Quoter bot to demonstrate trading via API in only 160 lines of code. You can use it as a starting point for you own more complex market maker bots, or to get into farming MQP. You can find the code on our GitHub. Read on for a walkthrough of the code itself!

Disclaimer: This blog post is for educational purposes only and does not constitute financial advice. Trading and investing involve risk, we do not guarantee any profits and cannot be held responsible for any financial losses incurred as a result of using this algorithm. Use at your own risk.


How to run locally

This code can be easily run on your local machine.

  1. Create a new directory for the quoter to run in.
  2. Download the quoter script manually or use curl to download it into your new directory
curl -O https://raw.githubusercontent.com/thalextech/thalex_py/master/examples/simple_quoter.py
  1. Download the keys template and rename it to keys.py, or use curl to put it next to the simple_quoter.py script
curl https://github.com/thalextech/thalex_py/blob/master/_keys.py > keys.py
  1. Open the keys file and insert your public and private keys. You can generate them in the API page in your Thalex account.
  2. Set up a Python venv environment in the new directory, activate it, then install the thalex package
python -m venv .venv
source .venv/bin/activate
pip install thalex

Once this is done, the simple_quoter.py is ready for use. Whenever you want to run it in the future, all you have to do is activate the venv again (if it's not still active) and run the script.

source .venv/bin/activate
python simple_quoter.py

Important Notes

When running the script locally, ensure your computer stays online and doesn’t go to sleep to avoid disconnections.

Similarly, remember to shut down the bot (Ctrl+C on Windows, macOS or Linux) before leaving your computer unattended.

You can also check our guide on How to run a Thalex bot on AWS to leave it running on a cloud environment without interruptions.

Also, we'll interact with different Thalex API channels and endpoints. Visit our API documentation for more information on their functionality.

Getting your setup ready

Before running the code, let’s make sure everything is properly set up.

Import Libraries and Methods

We start by importing all the libraries and methods the code will use.

libraries.png

As a reminder from the previous step, ensure the keys.py file is correctly configured with your valid keys. This file is essential for the script to authenticate the session in order to be able to use private api methods.

Parameters

At the very start of the code, you’ll find all the constant parameters:

parameters.png

This is where you customize your settings. Understanding these parameters is essential for managing your positions, orders, and risks effectively:

NETWORK: Choose Thalex.Network.TEST to connect to the testnet or Thalex.Network.PROD for the production environment. Note that we strongly recommend running this bot in TestNet first and only move to production when you're happy with the behavior. Running in production means trading with your actual money! TestNet is perfectly safe, as all the assets on TestNet are "fake money" for testing purposes.

ORDER_LABEL: An arbitrary label that the bot's orders are tagged with. This label is shown in the UI for order and trade history, and makes it easier to find/filter which orders and trades were done by this bot.

UNDERLYING: Specify the underlying asset. Use "BTCUSD" for Bitcoin or "ETHUSD" for Ethereum.

INSTRUMENT: Set the feedcode of the instrument you want to trade. Use "BTC-PERPETUAL" for Bitcoin perpetual.

PRICE_TICK and SIZE_TICK: Minimum price and order size steps. These are specified in our Trading Information documentation.

HALF_SPREAD: Determines how far from the Mark Price your quotes will be. This is defined in bps. For example, if the Mark Price is $95,000 and HALF_SPREAD is set to 2, your Bid will be $95,000 - (0.0002 * $95,000) = $94,981, and your Ask will be $95,018.

AMEND_THRESHOLD: Specify how much price movement you’ll allow before updating active orders. If set to 10 and the Mark Price moves from $95,000 to $95,009, your orders remain unchanged. If the price moves to $95,011, your orders will be updated.

SIZE: Limit your maximum position size. For instance, if set to 1, the bot stops sending Buy quotes once your position exceeds 1 BTC long, while Sell quotes remain active. When the position size drops below the limit, Buy quotes will resume. This obviously works vice versa when your position exceeds 1 BTC short.

QUOTE_ID: No adjustments needed. This is used internally by other methods.

Formatting Numbers

Lastly, there’s a small snippet of code included to handle number formatting throughout the script. It’s a minor but important addition to avoid rejections, since price and size numbers sent to the exchange API have to be rounded to the nearest tick size

rounding.png

Breaking Down the Code

Let’s dig into a step-by-step guide on what this code does. Understanding this part is key, especially if you’re planning to tweak this bot to suit your needs.

First off, we introduce a class called Quoter. This class is the backbone of the bot and handles all the essential tasks—from managing data and analyzing your orders and positions to sending out orders.

quoter.png

The __init__ method is where the class gets initialized with all the reference values necessary to run the code smoothly. Here’s what each key part does:

self.tlx: Holds a reference to a Thalex exchange connector from the library. This connector maintains a connection to Thalex and is used to call methods in the Thalex API.

self.mark: Holds the latest mark price

self.quotes: A dictionary that stores the information about your Buy and Sell orders that is returned by the exchange.

self.position: Keeps track of your current position.

Understanding the Code Structure

Up until now, we’ve been going through the code in the order it appears, from top to bottom. From this point on, we’re going to switch it up a bit.

To make the logic of the code easier to follow, we’ll start from the bottom and work our way up to the adjust_order method, which handles sending orders.

main.png

The main() function is where the application begins execution. Here’s what it does:

  1. Sets up logging: Configures logging to capture messages and save them to both the console and a file named "log.txt".
  2. Starts an infinite loop: Runs the bot continuously until an exception is raised.
  3. Creates instances:
    • A thalex.Thalex instance for connecting to the Thalex exchange.
    • A Quoter instance that contains the core quoting logic.
  4. Runs the quoting logic: Starts the quoter.quote() method as an asynchronous task.
  5. Handles exceptions:
    • Connection issues (e.g., websockets.ConnectionClosed or socket.gaierror): Logs the error and tries to reconnect after a brief pause.
    • Cancelled tasks (asyncio.CancelledError): Logs the cancellation and sets the run flag to False to break out of the loop.
    • Other exceptions: Logs the error and also sets the run flag to False to stop the loop.

This function takes care of the entire application lifecycle, including connection handling, error management, and a clean shutdown.

Running the code

The following lines at the bottom of the code are responsible for executing the main() function, starting the entire program when the script is run.

run_main.png

Methods Breakdown

With the main structure and flow explained, let's move on to the methods within the Quoter class. Each method handles specific tasks like data management, data analysis, and order execution. We'll go through them step-by-step to see how they work and contribute to the bot's functionality.

quote.png

quote establishes a connection with Thalex and subscribes to the necessary channels: instrument data (INSTRUMENT), your active orders (session.orders), and your position (account.portfolio). It directs the incoming data and enters a continuous loop to receive and process notifications from the exchange.

The 1000ms parameter represents the minimum interval between feeds. These are the possible values:

100ms, 200ms, 500ms, 1000ms, 5000ms, 60000ms or raw

When data is received, quote calls handle_notification, which updates internal data and may send out (updated) quotes. Additionally, the method includes error handling to cancel active orders created in the session if the received data is not as expected, preventing unintended executions based on incorrect information.


handle_notification.png

handle_notification method processes the notification. This depends on the type of notification it got from the exchange.

  • For session.orders, it updates information stored locally about your current orders, ensuring that the next method has the most up-to-date order information for decision-making.
  • The lwt (light weight ticker) is our "signal" that pricing has changed. It immediately checks if we need to update quotes.
  • Upon subscribing to account.portfolio, the full portfolio is sent, but afterwards only updated lines are transmitted (keyed by instrument_name). Lines are retransmitted if the position changes, or if the mark price deviates by more than 1%. When the portfolio changes, all open orders are cancelled. These will be reinserted when the next mark price comes in, based on your updated position.

update_quotes.png

update_quotes method prepares orders based on price changes.

First, it defines up to indicate if the mark price has increased or decreased. This helps determine the order priority (buy or sell) for sending orders.

It also verifies if there is position data available. We don't send any quotes before we get information on our position (in case the first ticker is sent before an update in the account.portfolio channel), in order to avoid overshooting the desired max position. Some overshooting might still happen due to the asynchronous nature of the communication with the exchange.

Next, the code calculates the order size based on the current position versus the maximum allowed position and sets the order price based on the mark price and the defined spread.

To prevent new orders from executing against your own existing orders. up helps decide the order to send first. If the price has increased, asks are prioritized to prevent higher bids from matching older asks. Conversely, if the price has decreased, bids are prioritized.

Once this logic is complete, the method forwards the relevant information for the next step, where the orders are sent.

adjust_order.png

adjust_order amends or inserts orders based on your existing orders and market conditions.

First, it checks for any open orders using confirmed and is_open.

If there are open orders, an amend order can be sent. It's only sent if the new size is 0 (effectively cancelling the order), or if the price difference between the new and old order exceeds the set threshold AMEND_THRESHOLD. If the difference is greater, the order is updated. If we were to continually update our orders, we would lose our place in the priority queue for execution.

If is_open is False and the amount is greater than 0, the method inserts a new order and updates self.quotes, which holds the current order details.

Next Steps

As we mentioned at the start, this is an extremely simple bot that is unlikely to make money. It shows the basics you need for quoting/trading via API. Over the coming weeks, we plan to release more advanced examples, but in the meantime, feel free to take our code and run with it. Add your own pricing logic, your own sizing logic, change it from a maker to a taker bot, quote options instead of perpetuals, etcetera. There is a lot of optional upside here, waiting for you to take it.

You could also use this simple quoter to accompany your screen trading. This is very helpful in collecting MQP, which rewards limit orders even if they’re not filled. You can always use TestNet to try your strategies, and if you feel ready to make some actual money, you can easily switch to production.

If you need assistance, have questions, or would like to share feedback, please feel free to contact us at [email protected].


Disclaimer

This blog post is for educational purposes only and does not constitute financial advice. Trading and investing involve risk, we do not guarantee any profits and cannot be held responsible for any financial losses incurred as a result of using this algorithm. Use at your own risk.

From the blog

The latest industry news, interviews, technologies, and resources.
View all posts