Skip to main content

Quick Start

Get up and running with HftBacktest by building a simple market-making strategy. This guide shows you how to create, configure, and run your first backtest.
Make sure you have installed HftBacktest before proceeding.

What You’ll Build

In this quickstart, you’ll create a basic market-making algorithm that:
  • Places bid and ask orders around a reservation price
  • Manages order queue positions
  • Accounts for latency and order fills
  • Handles position limits and risk

Choose Your Language

Python Quick Start

Step 1: Import Dependencies

import numpy as np
from numba import njit
from hftbacktest import BacktestAsset, HashMapMarketDepthBacktest, BUY, SELL, GTX, LIMIT
The @njit decorator from Numba compiles your strategy to machine code for high performance.

Step 2: Define Your Strategy

Create a market-making algorithm using the @njit decorator:
@njit
def market_making_algo(hbt):
    asset_no = 0
    tick_size = hbt.depth(asset_no).tick_size
    lot_size = hbt.depth(asset_no).lot_size

    # Main strategy loop - elapse 10ms at a time
    while hbt.elapse(10_000_000) == 0:  # nanoseconds
        hbt.clear_inactive_orders(asset_no)

        # Strategy parameters
        a = 1  # Alpha weight
        b = 1  # Risk aversion
        c = 1  # Volatility weight
        hs = 1  # Half spread multiplier

        # Calculate alpha (forecast), volatility, and risk
        forecast = 0  # Your alpha signal
        volatility = 0  # Market volatility measure
        position = hbt.position(asset_no)
        risk = (c + volatility) * position
        half_spread = (c + volatility) * hs

        # Position limits
        max_notional_position = 1000
        notional_qty = 100

        # Get market depth
        depth = hbt.depth(asset_no)
        mid_price = (depth.best_bid + depth.best_ask) / 2.0

        # Calculate reservation price and quotes
        reservation_price = mid_price + a * forecast - b * risk
        new_bid = reservation_price - half_spread
        new_ask = reservation_price + half_spread

        # Convert to tick prices
        new_bid_tick = min(np.round(new_bid / tick_size), depth.best_bid_tick)
        new_ask_tick = max(np.round(new_ask / tick_size), depth.best_ask_tick)

        # Calculate order quantity
        order_qty = np.round(notional_qty / mid_price / lot_size) * lot_size

        # Elapse processing time (1ms)
        if hbt.elapse(1_000_000) != 0:
            return False

        # Manage existing orders
        last_order_id = -1
        update_bid = True
        update_ask = True
        buy_limit_exceeded = position * mid_price > max_notional_position
        sell_limit_exceeded = position * mid_price < -max_notional_position

        orders = hbt.orders(asset_no)
        order_values = orders.values()
        while order_values.has_next():
            order = order_values.get()
            if order.side == BUY:
                if order.price_tick == new_bid_tick or buy_limit_exceeded:
                    update_bid = False
                if order.cancellable and (update_bid or buy_limit_exceeded):
                    hbt.cancel(asset_no, order.order_id, False)
                    last_order_id = order.order_id
            elif order.side == SELL:
                if order.price_tick == new_ask_tick or sell_limit_exceeded:
                    update_ask = False
                if order.cancellable and (update_ask or sell_limit_exceeded):
                    hbt.cancel(asset_no, order.order_id, False)
                    last_order_id = order.order_id

        # Submit new orders
        if update_bid and not buy_limit_exceeded:
            order_id = new_bid_tick
            hbt.submit_buy_order(
                asset_no, order_id, new_bid_tick * tick_size, 
                order_qty, GTX, LIMIT, False
            )
            last_order_id = order_id

        if update_ask and not sell_limit_exceeded:
            order_id = new_ask_tick
            hbt.submit_sell_order(
                asset_no, order_id, new_ask_tick * tick_size,
                order_qty, GTX, LIMIT, False
            )
            last_order_id = order_id

        # Wait for order response
        if last_order_id >= 0:
            timeout = 5_000_000_000  # 5 seconds
            if not hbt.wait_order_response(asset_no, last_order_id, timeout):
                return False

    return True

Step 3: Configure the Backtest

Set up your backtest with market data and parameters:
if __name__ == '__main__':
    # Configure the asset
    asset = (
        BacktestAsset()
            .data([
                'data/btcusdt_20220831.npz',
                'data/btcusdt_20220901.npz',
            ])
            .initial_snapshot('data/btcusdt_20220830_eod.npz')
            .linear_asset(1.0)
            .intp_order_latency([
                'latency/live_order_latency_20220831.npz',
                'latency/live_order_latency_20220901.npz',
            ])
            .power_prob_queue_model(2.0)
            .no_partial_fill_exchange()
            .trading_value_fee_model(-0.00005, 0.0007)  # Maker/taker fees
            .tick_size(0.1)
            .lot_size(0.001)
    )

    # Create backtest and run
    hbt = HashMapMarketDepthBacktest([asset])
    market_making_algo(hbt)

Step 4: Run the Backtest

python your_strategy.py
This example assumes market maker rebates (negative maker fees). Adjust the fee model based on your exchange’s fee structure.

Understanding the Code

Key Components

1

Market Depth Access

Get real-time market data including best bid/ask and order book:
depth = hbt.depth(asset_no)
mid_price = (depth.best_bid + depth.best_ask) / 2.0
2

Position Management

Track your current position and calculate risk:
position = hbt.position(asset_no)
risk = (c + volatility) * position
3

Order Submission

Submit limit orders with Good-Til-Crossing (GTX) time-in-force:
hbt.submit_buy_order(asset_no, order_id, price, qty, GTX, LIMIT, False)
4

Latency Simulation

Elapse time to simulate processing and network latency:
hbt.elapse(1_000_000)  # 1ms in nanoseconds

Order Types and Time-in-Force

Order TypeDescription
LIMITLimit order at specified price
MARKETMarket order (immediate execution)
Time-in-ForceDescription
GTXGood-Til-Crossing (post-only, maker-only)
GTCGood-Til-Cancelled
FOKFill-Or-Kill
IOCImmediate-Or-Cancel

Configuration Options

Asset Configuration

asset = (
    BacktestAsset()
        .data(['data.npz'])                    # Market data files
        .initial_snapshot('snapshot.npz')       # Starting order book
        .linear_asset(1.0)                      # Linear asset (futures)
        .intp_order_latency(['latency.npz'])   # Order latency model
        .power_prob_queue_model(2.0)           # Queue position model
        .no_partial_fill_exchange()            # Exchange model
        .trading_value_fee_model(-0.00005, 0.0007)  # Fees (maker, taker)
        .tick_size(0.1)                        # Minimum price increment
        .lot_size(0.001)                       # Minimum quantity increment
)

Fee Models

# Negative maker fee (rebate), positive taker fee
.trading_value_fee_model(-0.00005, 0.0007)

Queue Models

The queue model determines order fill probability based on queue position:
ModelParameterDescription
power_prob_queue_model2.0Power law with exponent 2.0 (conservative)
power_prob_queue_model3.0Power law with exponent 3.0 (moderate)
prob_queue_modelCustomYour custom probability function
Higher exponents make it harder to get filled when you’re back in the queue, simulating more competitive markets.

Next Steps

Now that you’ve run your first backtest, explore these topics:

Data Preparation

Learn how to prepare and format market data

Market Depth & Trades

Work with order book data and trade feeds

Strategy Examples

Explore trading strategies and examples

API Reference

Dive deep into the complete API

Common Patterns

Time Management

# Elapse time in strategy loop
while hbt.elapse(10_000_000) == 0:  # 10ms intervals
    # Strategy logic
    pass

# Elapse processing time
hbt.elapse(1_000_000)  # 1ms processing delay

Order Management

# Clear filled/cancelled orders
hbt.clear_inactive_orders(asset_no)

# Cancel an order
hbt.cancel(asset_no, order_id, wait=False)

# Wait for order response
hbt.wait_order_response(asset_no, order_id, timeout)

Position Limits

max_notional_position = 1000
position = hbt.position(asset_no)

buy_limit_exceeded = position * mid_price > max_notional_position
sell_limit_exceeded = position * mid_price < -max_notional_position

if not buy_limit_exceeded:
    hbt.submit_buy_order(...)

Troubleshooting

Check your order prices and queue model. Your orders may be too far from the market or getting stuck in queue. Try:
  • Reducing spread
  • Using more aggressive queue model
  • Checking your order price logic
Ensure all variables are typed consistently within the @njit function. Numba requires static typing.
Make sure your data files are in the correct location. Use absolute paths or paths relative to where you run the script:
import os
data_path = os.path.join(os.path.dirname(__file__), 'data', 'btcusdt.npz')
  • Python: Ensure @njit decorator is applied
  • Rust: Build with --release flag
  • Reduce data volume for initial testing
  • Increase time intervals (e.g., 100ms instead of 10ms)

Example Data

Looking for data to test with? Check out:

Build docs developers (and LLMs) love