Blankly – Python Backtesting Guide

10 min read

Get 10-day Free Algo Trading Course

Last Updated on March 24, 2024

Table of contents:

  1. What is Blankly?
  2. What is Blankly used for?
  3. Why should I use Blankly?
  4. Why shouldn’t I use Blankly?
  5. Is Blankly free?
  6. What are some Blankly alternatives?
  7. What exchanges and brokers does Blankly support?
  8. How does Blankly work?
  9. How to get started with Blankly?
  10. How to set API keys in Blankly?
  11. What are the components of Blankly’s strategies?
  12. How to create orders with Blankly?
  13. How to create a pairs trading strategy in Blankly?
  14. How to initialise a Blankly strategy?
  15. What is the Blankly arbitrage event?
  16. How to create price events with Blankly?
  17. How to backtest your trading strategy with Blankly?
  18. How to obtain backtesting metrics with Blankly?
  19. How to create a custom data source with Blankly?
  20. How to use technical indicators with Blankly?
  21. Where can I learn more about Blankly?
  22. Full code

What is Blankly?

Blankly is an open-source Python backtester that allows algorithmic traders to build, backtest, and run their trading algorithms for stocks, crypto, futures, and forex.

Link: https://github.com/blankly-finance/blankly

What is Blankly used for?

Blankly is mainly used by algorithmic traders for backtesting and running trading strategies in Python.

Why should I use Blankly?

  • Blankly is completely free.
  • Blankly is easy to use and beginner-friendly.
  • Is precise.
  • Has built-in support for exchanges such as Binance, Coinbase, and Alpaca.
  • Allows for backtesting using alternative data
  • Is maintained.

Why shouldn’t I use Blankly?

  • Doesn’t have advanced orders (OCO, straddle, etc.)
  • Doesn’t have advanced position handling.
  • Should have more features.
  • Some parts of the website and documentation are outdated.
  • It seems that the development has slowed down.

Is Blankly free?

Yes, Blankly is completely free and all the code for it is open-sourced. The upcoming premium part of it will feature a cloud-hosted solution.

What are some Blankly alternatives?

Blankly can be replaced with other software that can be more suitable for your needs. Here are some of them:

What exchanges and brokers does Blankly support?

Blankly supports the following exchanges and brokers at the time of writing this article:

How does Blankly work?

Blankly works by running an initialization layer that sets up your algorithm and the overall state that it will be using. Then, there are chains that you can add which can be executed on events such as each bar, each price, and each order.

Once the algorithm backtest is done, or once you stop your live trading it will start a teardown process to gracefully stop. This can be observed from the picture below:

How to get started with Blankly?

To get started with Blankly, all you need to do is to install the Blankly library and initialize the project. We do this by writing the following commands:

pip install blankly
mkdir blankly-project
cd blankly-project
blankly init

Now, you will be presented with different options that will help you customize blankly to fit your project’s needs. It will start you off with a question asking you to choose an exchange to connect to or go without one.

Blankly suggests users pick an exchange to get more accurate data, have the option to get live data, and have the ability to easily switch to a live trading algorithm. I’ll go with Alpaca.

After that, you will be asked to select what type of model you want to use. In our case, we will be creating a Strategy. For the next question, we will select that we don’t need a template, but feel free to select that option if you want to inspect it.

Finally, you can choose to add your API key for Alpaca now or later. I’ll choose later as I want to show you how you can do this by changing a file.

How to set API keys in Blankly?

To set API keys in Blankly, you can navigate to the keys.json file where you will find the supported exchanges and be able to set your API keys by changing the file. You will also see the sandbox parameter which can be set to True to use a paper trading account.

{
    "alpaca": {
        "example-portfolio": {
            "API_KEY": "********************",
            "API_SECRET": "********************",
            "sandbox": false
        }
    },

Above, I’ll place my Alpaca API keys and set the sandbox parameter to true.

What are the components of Blankly’s strategies?

The main two components of Blankly’s strategies are the initializer and price data event handler. The initializer is used to lay out the foundation of your algorithm and set the state variables that are to be used.

For example, here is a state that is aware of the traded symbol and constrains the data that will be passed to a deque that can hold a maximum of 150 candles. Once a new candle arrives, the oldest one will be pushed out. It also sets the position to be false.

def init(symbol, state: blankly.StrategyState):
    # This gets the past 150 data points as a deque to reduce memory usage
    state.variables['history'] = state.interface.history(
        symbol, to=150, return_as='deque'
    )['close']
    state.variables['owns_position'] = False

When it comes to the price events, here is an example of a price event that takes in the candles and calculates the RSI upon which it makes a market buy order and sets the position state to True if the RSI is oversold:

def price_event(price, symbol, state: StrategyState):
    state.variables['history'].append(price)
    rsi = blankly.indicators.rsi(state.variables['history'])
    if rsi[-1] < 30 and not state.variables['owns_position']:
        buy = int(state.interface.cash / price) # calculate number of shares
        state.interface.market_order(symbol, side='buy', size=buy)
        state.variables['owns_position'] = True

Now that you have a sense of how Blankly functions, we can go into it a bit deeper and code a pairs trading strategy.

How to create orders with Blankly?

To create orders with Blankly, we can utilise the state to execute orders such as the market, limit, and stop orders. An example of a Blankly order is as follows:

order = interface.market_order('BTC-USD', 'buy', 10)

How to create a pairs trading strategy in Blankly?

To create a pairs trading strategy in Blankly, we will need to build out the main components of the trading strategy that we’ll be using.

In this case, it is a pairs trade strategy that bets that if the two assets diverge – they will likely converge again. As you can see, this strategy relies on a couple of prerequisites such as cointegration between the two assets.

To learn more about pairs trading, read our blog here and if you want to find out how you can obtain plausible pairs with machine learning, check out this blog post.

The assets that we will use for this example are Box (BOX) and Dropbox (DBX) as we already have a sense of their cointegrated nature based on our previous articles and analyses. For this example, we will hold at maximum only one position (1 short and 1 long) at a time and deploy 100% of our cash.

We will enter a position if one stock has moved 5% or more than the other one over the course of the last five days. We will short the top one and long the bottom one until it reverses.

Let’s build out the initializer while thinking about what the primary state variables of our strategy will be.

How to initialise a Blankly strategy?

To initialize a Blankly strategy, we will build out the Blankly initializer with a state that will support the execution of our strategy.

Our state will need to keep track of the following parts:

  1. The close price history of BOX
  2. The close price history of DBX
  3. Percentage move
  4. Position status
  5. Trade status (did we long BOX or DBX)

We will cap the close histories for both prices to hold a maximum of 5 candles as we don’t need more than that. We will also be using the Futures version of the Blankly strategy state as we will be longing and shorting the stocks.

Let’s code out the state that we want to be initialised:

import blankly

def init(symbol, state: blankly.FuturesStrategyState):
    state.variables["dbx_history"] = state.interface.history(
        "BOX", to=5, return_as="deque"
    )["close"]
    state.variables["box_history"] = state.interface.history(
        "DBX", to=5, return_as="deque"
    )["close"]

    state.variables["dbx_change"] = 0
    state.variables["box_change"] = 0

    state.variables["in_position"] = False
    state.variables["dbx_long"] = False
    state.variables["box_long"] = False

    state.variables["dbx_size"] = 0
    state.variables["box_size"] = 0

The next step is to create a price_event that will be intaking the asset data. The issue here, as with most backtesting libraries, is that the initialised state is often localised to a single asset.

In other words, if we passed the price_event in the usually way of attaching each stock separately, we would have issues with:

  • Synchronising the dequeues
  • Sharing a state (would need to build custom solutions or use global variables)
  • Executing orders properly

Thankfully, Blankly offers an arbitrage event which is a wrapper for the usual price_event.

What is the Blankly arbitrage event?

The Blankly arbitrage event is a wrapper that intakes a price event, resolution of asset data, the initialisation state, and multiple assets to obtain the data for. An example of this event is the following:

add_arbitrage_event(
    callback: typing.Callable,
    symbols: list, 
    resolution: typing.Union[str, float],
    init: typing.Callable = None,
    **kwargs
)

Now that we know that this is the solution to a shared-state issue, we can move on to creating our price event that will be passed as the callback parameter to the arbitrage event.

How to create price events with Blankly?

To create price events with Blankly, all you need to do is to pass the price data, symbol and the state that the price event will be using. In our pairs trade example, we expect both symbols being passed at the same time to the price event.

Now, let’s set up the logic of the trade inside our price event. We will collect the data and check if the dequeue has been filled with 5 days worth of price data. If it has, we can calculate the percentage moves for each stock over the last 5 days.

Then, we can compare the difference between the two percentage moves. If the difference is more than 5% we will short the over-performing stock and long the under-performing one, and vice-versa.

We will only stay in one position at a time and will deploy our whole trading portfolio by shorting and longing each asset using 50% of the portfolio.

def price_event(price, symbol, state: blankly.FuturesStrategyState):
    # Add the new price to the history
    state.variables["dbx_history"].append(price["DBX"])
    state.variables["box_history"].append(price["BOX"])

    # Check if we have enough data to calculate the percentage change
    if (
        len(state.variables["dbx_history"]) == 5
        and len(state.variables["box_history"]) == 5
    ):
        # Calculate the percentage change
        state.variables["dbx_change"] = (
            state.variables["dbx_history"][-1] - state.variables["dbx_history"][0]
        ) / state.variables["dbx_history"][0]
        state.variables["box_change"] = (
            state.variables["box_history"][-1] - state.variables["box_history"][0]
        ) / state.variables["box_history"][0]

        # calculate the difference between the two stocks
        diff = state.variables["dbx_change"] - state.variables["box_change"]

        # If the difference is greater than 5% we will enter a position
        # We long the stock that is underperforming and short the stock that is overperforming
        if diff > 0.05 and not state.variables["in_position"]:
            # Calculate the size of the position
            # We allocate 40% of our portfolio to each position
            cash = state.interface.cash
            state.variables["dbx_size"] = blankly.trunc(
                (cash * 0.5 / state.interface.get_price("DBX")), 2
            )
            state.variables["box_size"] = blankly.trunc(
                (cash * 0.5 / state.interface.get_price("BOX")), 2
            )

            # Long BOX and short DBX
            try:
                state.interface.market_order(
                    "DBX", side="sell", size=state.variables["dbx_size"]
                )
                state.interface.market_order(
                    "BOX", side="buy", size=state.variables["box_size"]
                )
            except Exception as e:
                print(e)
                return

            state.variables["in_position"] = True
            state.variables["dbx_long"] = False
            state.variables["box_long"] = True

        # If the difference is less than -5% we will enter a position
        # We long the stock that is underperforming and short the stock that is overperforming
        elif diff < -0.05 and not state.variables["in_position"]:
            # We allocate 50% of our portfolio to each position
            cash = state.interface.cash
            state.variables["dbx_size"] = blankly.trunc(
                (cash * 0.5 / state.interface.get_price("DBX")), 2
            )
            state.variables["box_size"] = blankly.trunc(
                (cash * 0.5 / state.interface.get_price("BOX")), 2
            )

            # Short BOX and long DBX
            try:
                state.interface.market_order(
                    "BOX", side="sell", size=state.variables["box_size"]
                )
                state.interface.market_order(
                    "DBX", side="buy", size=state.variables["dbx_size"]
                )
            except Exception as e:
                print(e)
                return

            state.variables["in_position"] = True
            state.variables["dbx_long"] = True
            state.variables["box_long"] = False

Now that we have the logic in place when it comes to entering a trade, we can create logic when the tables turn and we want to reverse our position:

        # If the position has reversed we reverse our position
        elif diff > 0.05 and state.variables["dbx_long"]:
            # Short DBX and long BOX
            try:
                state.interface.market_order(
                    "DBX", side="sell", size=state.variables["dbx_size"]
                )
                state.interface.market_order(
                    "BOX", side="buy", size=state.variables["box_size"]
                )
            except Exception as e:
                print(e)
                return

            state.variables["in_position"] = False
            state.variables["dbx_long"] = False
            state.variables["box_long"] = False

        elif diff < -0.05 and state.variables["box_long"]:
            # Long DBX and short BOX
            try:
                state.interface.market_order(
                    "BOX", side="sell", size=state.variables["box_size"]
                )
                state.interface.market_order(
                    "DBX", side="buy", size=state.variables["dbx_size"]
                )
            except Exception as e:
                print(e)
                return

            state.variables["in_position"] = False
            state.variables["dbx_long"] = False
            state.variables["box_long"] = False

Notice how I placed the trades inside a try and except block, this is done as I’d like to know if something is off with particular trades during backtesting. Now that we have the execution logic in place, we can move onto backtesting our strategy.

How to backtest your trading strategy with Blankly?

To backtest your trading strategy with Blankly, you will need to initialise the exchange object, the type of strategy that you want to use, and pass the events to the strategy with adequate parameters. After that, you can run the backtest.

For our example, we’ll be using the Alpaca exchange and the Futures strategy type. When it comes to the price data and events, we will wrap the price event into an arbitrage event and pass both stocks while requesting daly stock data.

Finally, we will issue a backtest on the past 3 years of data:

if __name__ == "__main__":
    exchange = blankly.Alpaca()
    strategy = blankly.FuturesStrategy(exchange)

    strategy.add_arbitrage_event(
        price_event, symbols=["BOX", "DBX"], resolution="1d", init=init
    )
    backtest_result = strategy.backtest(initial_values={"USD": 10000}, to="3y")
    print(backtest_result)

How to obtain backtesting metrics with Blankly?

To obtain backtesting metrics with Blankly, all you need to do is to print the contents of the backtesting result. For example, these are the metrics of our pairs trading strategy:

Blankly Metrics: 
Calmar Ratio:                      0.14
Compound Annual Growth Rate (%):   4.0%
Conditional Value-at-Risk:         11.21
Cumulative Returns (%):            14.000000000000002%
Max Drawdown (%):                  28.999999999999996%
Resampled Time:                    86400.0
Risk Free Return Rate:             0.0
Sharpe Ratio:                      0.27
Sortino Ratio:                     0.3
Value-at-Risk:                     141.2
Variance (%):                      2.4%
Volatility:                        0.15

Another thing that Blankly does is that it generates an HTML Bokeh report of the strategy that you can observe. Here is how ours looks like:

How to create a custom data source with Blankly?

Instead of only downloading data from your exchange, you can use any custom data that contains the columns openhighlowclose volume and time. Then, all you need to do is to pass the data to the Blankly reader:

if __name__ == "__main__":
    # Use this
      exchange = blankly.Alpaca()
    model = Strategy(exchange)
    reader = blankly.data.PriceReader('./custom.csv', 'CUSTOM-USD')
    model.backtester.add_custom_prices(reader)
    
    model.backtest(args={}, initial_values={
      "USD": 10000
    })

In addition to custom data sources, Blanky allows you to backtest using alternative data such as tweets. See more here.

How to use technical indicators with Blankly?

To use technical indicators with Blankly, you have three approaches. The first one is to utilise their built-in indicators, the second approach is to use another library to calculate them, and the third option is to build you own.

Here is an example of how one can use a built-in Blankly indicator:

rsi = blankly.indicators.rsi(state.variables['history'])

You can read more about the implemented indicators here.

Where can I learn more about Blankly?

You can learn more about blankly by accessing their GitHub repo and checking out their documentation.

Full code

GitHub repository

Igor Radovanovic