How to Build Your Own Python Trading Bot

MORITZ PUTZHAMMER

30 August 20228 min read

Table of contents

In order to get you started with the Trality Bot Code Editor and your first Python trading bot, we’ll use this post to cover a fairly basic approach to building a simple trading algorithm. It consists of standard technical analysis (TA) but also includes some features of the Trality API that can help you to create more sophisticated trading bots as you proceed. Moreover, we’ll analyze a resulting backtest, which tests our algorithm on historical data and then we will use the Optimizer to optimize our strategy's parameters for maximum profit.

Creating Trading Bots With Trality’s Python Code Editor

Trality’s state-of-the-art Python Code Editor allows users to create highly innovative and intricate algorithms in the most efficient way possible.

Once you’re satisfied with your algorithm and backtesting results, you can deploy your bot for live trading or paper trading on some of the most trusted exchanges, including Binance, Binance.US, Bitpanda, Coinbase Pro, and Kraken.

How Do Trading Bots Work?

Trading bots make decisions on behalf of a trader based on information, such as price movements within the market, generating a reaction according to a predefined set of criteria. They can interpret and merge traditional market signals such as price, volume, orders and time with more sophisticated signals, such as machine learning models, and decide whether or not it’s a good opportunity to execute a trade.

Financial Data For Trading Bots

There are two main types of raw market data that come from centralized cryptocurrency exchanges (CEX). One is quote data from the limit order book (LOB) and the other is trade data. Quotes are aggregated snapshots of a set of limit orders at a given price level that have not been matched.

Trades occur whenever an order is placed that matches with another order resting in the LOB. Raw market data is streamed from the exchange via websockets. When any update to the exchange LOB occurs, the information regarding that change is pushed via the websocket in real-time.

Within the Trality engine, candlestick data is used to represent the prices of an asset over a specific period. There are 5 main components of a candlestick: 1) the open (first) price, 2) the highest price, 3) the lowest price, 4) the close (last) price, and 5) the volume.

Each candlestick is derived from the raw market data via the raw websocket rather than listening to the price bars from the exchange. It takes time for the exchange to build and publish these bars; by generating them it is possible to minimize the latency between a bar closing and it being available to a bot. Being able to react quicker improves the execution price the bot can achieve should it want to trade.

One of the most important parts of market data is that the backtests are consistent with the live environment. This ensures that the bot's behavior will be the same whether running in a backtest or live environment.

Trality achieves this by making sure the candlestick data that the bots receive is the same in the live, virtual, and backtest environments. As a result, whatever decisions the bot makes using market data will be consistent across all environments.

Candlestick data also needs to be consistent across multiple time frames. Each 1h candlestick should be consistent with the four 15m bars that make it up. And each 5m bar is consistent with the five 1m bars that make it up. This is achieved on Trality by deriving longer-term bars from the highest fidelity data we have, ensuring that candlesticks are not only accurate but also consistent across all time frames.

Creating a Python Trading Bot

So how do we use a simple cryptocurrency trading bot to protect our assets in a turbulent year like 2022? We will show you a beginner-level example strategy, which is based on a commonly-used TA indicator: an exponential moving average (EMA), or the crossover of two EMAs to be precise.

See the Trality Documentation for a detailed explanation of the EMA crossover strategy.

In short: An EMA is a type of moving average (MA) that places greater weight and significance on the most recent data points. An exponentially weighted moving average reacts more significantly to recent price changes than a simple moving average (SMA). A crossover strategy applies two moving averages to a chart, one longer and one shorter.

When the shorter-term MA crosses above the longer-term MA, it's a buy signal, as it indicates that the trend is shifting up. Meanwhile, when the shorter-term MA crosses below the longer-term MA, it's a sell signal, indicating that the trend is shifting down.

We, therefore, develop a strategy with two EMAs (20 and 50 candles look back period). The strategy trades on 6 hour candles, making it sensitive to mid- to short-term price movements. For this scenario, the strategy allocates 80% of the account balance when taking a position.

We use the Trality's Python Code Editor to code this algorithm. Let's go through each of the main building blocks one step at a time:

Step 0: Define handler function

To begin with, every function that is annotated with our schedule decorator is run on a specified time interval and receives symbol data. We call these annotated functions handlers, but you can name them however you want. They just have to take two arguments. We call the first one state and the second one data. The second argument will always receive the symbol data for the interval you specified. In this particular bot, we trade in 6 hour candle intervals and we specify a trading symbol, which is BTCUSDT. Of course multiple symbols are possible to trade on as well!

def initialize(state):
    pass

@schedule(interval="6h", symbol="BTCUSDT")
def handler(state, data):

Step 1: Compute indicators from data

In the first step of our algorithm creation, we define two exponential moving averages (EMA), one with a shorter look-back period of 20 candles and one longer with a period of 50 candles.

   ema_short = data.ema(20).last
   ema_long = data.ema(50).last

Step 2: Fetch position for symbol

In a second step we query for any open position by symbol. By calling this function we receive a boolean value indicating whether an open position for that symbol exists or not.

    position = query_open_position_by_symbol(data.symbol,include_dust=False)
    has_position = position is not None

Step 3: Fetch position for symbol

In a third step we query for any open position by symbol. By calling this function we receive a boolean value indicating whether an open position for that symbol exists or not.

    position = query_open_position_by_symbol(data.symbol,include_dust=False)
    has_position = position is not None

Step 3: Resolve buy or sell signals

In a third step, the heart and soul of our algorithm is defined: its trading strategy. We use the order API to create orders. Specifically, the algorithm places a market order going long if the shorter EMA crosses above the longer for 80% of the account balance.

We also define a sell logic, which closes the open position if the algorithm detects an open position and the shorter crosses below the longer EMA.

  if ema_short > ema_long and not has_position: 
        order_market_target(symbol=data.symbol, target_percent=0.8)

    elif ema_short < ema_long and has_position:
         close_position(data.symbol)

Putting the pieces together

If we put all these steps together, we get the following little code snippet, which we can subsequently put through our first backtest:

def initialize(state):
    pass

@schedule(interval="6h", symbol="BTCUSDT")
def handler(state, data):


    '''
    1) Compute indicators from data and add parameters into strategy
    '''    
    ema_short = data.ema(20).last
    ema_long = data.ema(50).last    
    
    '''
    2) Fetch position for symbol
    '''
    position = query_open_position_by_symbol(data.symbol,include_dust=False)
    has_position = position is not None


    '''
    4) Resolve buy or sell signals     
    '''
    if ema_short > ema_long and not has_position: 
       order_market_target(symbol=data.symbol, target_percent=0.8)
        
    elif ema_short < ema_long and has_position:
        close_position(data.symbol)

Backtesting the Python Bot on Historical Data

In order to evaluate our beginner-level cryptocurrency trading bot, we run the above code in the Trality backtester and obtain the following results:

Backtesting
Performance of Python Trading Bot (1h) BTCUSDT EMA Crossover (20,50) 28.06.2022-28.07.2022

The figure above shows the results of our Python trading bot from June 28 2022 to July 28 2022. Backtests on Trality always include exchange fees and can be modeled to account for slippage. As evidenced by the individual positions and stats, the bot executed 4 trades and the total return over the specified period is -6.61% while benchmark performance is 10.65%.

The next and final step is the optimization phase.

Optimizing Strategy Parameters

Creating a profitable Python-based bot can be challenging. Even when you have an algorithm idea you are satisfied with, optimizing its parameters can be frustrating and time-consuming. That’s why our research team built the Optimizer.

A new feature for the backtester when creating Python Code Bots, the Optimizer will allow you to automate the parameter optimization process. When writing your bot code, you simply define relevant parameters and their respective ranges that you want to be optimized to achieve the highest PnL, and let the Optimizer do its magic.

Trality’s Optimizer Walkthrough

We have made the process for the optimization of your bot very simple. Let's say we want to find the optimal period for ema_short and ema_long to achieve the highest possible return.

As you can see from the code below, we will need to add our new feature annotation @parameter on top of the initializer. Once that is done, to use the @parameter annotations we need to add the params object to the functions and to the indicators.

@parameter(name="ema_short", type="float", default=20, min=10, max=25, enabled=True)
@parameter(name="ema_long", type="float", default=50, min=30, max=60, enabled=True)
def initialize(state, params):
    pass

@schedule(interval="6h", symbol="BTCUSDT")
def handler(state, data, params):


    '''
    1) Compute indicators from data
    '''
    ema_short = data.ema(params.ema_short).last
    ema_long = data.ema(params.ema_long).last

    '''
    2) Fetch position for symbol
    '''
    position = query_open_position_by_symbol(data.symbol,include_dust=False)
    has_position = position is not None


    '''
    4) Resolve buy or sell signals
    '''
    if ema_short > ema_long and not has_position:
       order_market_target(symbol=data.symbol, target_percent=0.8)

    elif ema_short < ema_long and has_position:
        close_position(data.symbol)

Now it’s ready to be optimized. Don’t forget to activate the optimizer under advanced settings!

Backtesting
Performance of Optimized Python Trading Bot

Running the Optimizer, it was found that the optimal parameter for ema_short is 10 and for ema_long is 30. You can see the backtesting results in the image above. With the optimal parameters, the bot managed to increase total return from -6.61% to 3.21%.

Takeaways for Your Python Trading Bot

It is important to note that this is a fairly simple trading bot, which is meant as a starting point for your analysis. Trality offers many more possibilities to create bots that will help you to significantly outperform the market. In order to do so, more nuanced elements of your code might become necessary, such as trading on multiple intervals and with multiple coins or using sophisticated order management with multiple order types. And last but not least, leverage Trality’s state-of-the-art Optimizer to automatically optimize your strategy parameters to find the best settings for maximum profit.

To find further information on our features please visit our documentation. And if you are looking to get started, we recommend taking a look at our Masterclass.


Disclaimer: None of what is found in this article should be considered investment advice. The above article is merely an opinion piece and does not represent any kind of trading advice or suggestions on how to invest, how to trade or in which assets to invest in or suggestions on how trading bots or trading algorithms can or should be used! Always do your own research before investing and always (!) only invest what you can afford to lose! Backtests are not indicative of future results.