Skip to main content
This example is for educational purposes only and demonstrates effective strategies for high-frequency market-making schemes. All backtests are based on a 0.005% rebate, the highest market maker rebate available on Binance Futures. See Binance Upgrades USDⓢ-Margined Futures Liquidity Provider Program for more details.

Plain High-Frequency Grid Trading

This is a high-frequency version of grid trading that keeps posting orders on grids centered around the mid-price, maintaining a fixed interval and a set number of grids.
import numpy as np
from numba import njit, uint64, float64
from numba.typed import Dict
from hftbacktest import BUY, SELL, GTX, LIMIT

@njit
def gridtrading(hbt, recorder):
    asset_no = 0
    tick_size = hbt.depth(asset_no).tick_size
    grid_num = 20
    max_position = 5
    grid_interval = tick_size * 10
    half_spread = tick_size * 20

    # Running interval in nanoseconds.
    while hbt.elapse(100_000_000) == 0:
        # Clears cancelled, filled or expired orders.        
        hbt.clear_inactive_orders(asset_no)

        depth = hbt.depth(asset_no)
        position = hbt.position(asset_no)
        orders = hbt.orders(asset_no)
        
        best_bid = depth.best_bid
        best_ask = depth.best_ask
        
        mid_price = (best_bid + best_ask) / 2.0

        order_qty = 0.1
        
        # Aligns the prices to the grid.
        bid_price = np.floor((mid_price - half_spread) / grid_interval) * grid_interval
        ask_price = np.ceil((mid_price + half_spread) / grid_interval) * grid_interval

        # Updates quotes.
        new_bid_orders = Dict.empty(np.uint64, np.float64)
        if position < max_position and np.isfinite(bid_price):
            for i in range(grid_num):
                bid_price_tick = round(bid_price / tick_size)
                new_bid_orders[uint64(bid_price_tick)] = bid_price
                bid_price -= grid_interval

        new_ask_orders = Dict.empty(np.uint64, np.float64)
        if position > -max_position and np.isfinite(ask_price):
            for i in range(grid_num):
                ask_price_tick = round(ask_price / tick_size)
                new_ask_orders[uint64(ask_price_tick)] = ask_price
                ask_price += grid_interval
                
        order_values = orders.values();
        while order_values.has_next():
            order = order_values.get()
            if order.cancellable:
                if (
                    (order.side == BUY and order.order_id not in new_bid_orders)
                    or (order.side == SELL and order.order_id not in new_ask_orders)
                ):
                    hbt.cancel(asset_no, order.order_id, False)
                    
        for order_id, order_price in new_bid_orders.items():
            if order_id not in orders:
                hbt.submit_buy_order(asset_no, order_id, order_price, order_qty, GTX, LIMIT, False)
                
        for order_id, order_price in new_ask_orders.items():
            if order_id not in orders:
                hbt.submit_sell_order(asset_no, order_id, order_price, order_qty, GTX, LIMIT, False)
        
        recorder.record(hbt)
    return True

High-Frequency Grid Trading with Skewing

By incorporating position-based skewing, the strategy’s risk-adjusted returns can be improved:
@njit
def gridtrading(hbt, recorder, skew):
    asset_no = 0
    tick_size = hbt.depth(asset_no).tick_size
    grid_num = 20
    max_position = 5
    grid_interval = tick_size * 10
    half_spread = tick_size * 20

    while hbt.elapse(100_000_000) == 0:
        hbt.clear_inactive_orders(asset_no)

        depth = hbt.depth(asset_no)
        position = hbt.position(asset_no)
        orders = hbt.orders(asset_no)
        
        best_bid = depth.best_bid
        best_ask = depth.best_ask
        
        mid_price = (best_bid + best_ask) / 2.0
        
        order_qty = 0.1
        
        # The personalized price that considers skewing based on inventory risk
        # as described in the Stoikov-Avallaneda market-making paper.
        # https://math.nyu.edu/~avellane/HighFrequencyTrading.pdf
        reservation_price = mid_price - skew * tick_size * position

        # Limit the price to the best bid and best ask to ensure market making
        bid_price = np.minimum(reservation_price - half_spread, best_bid)
        ask_price = np.maximum(reservation_price + half_spread, best_ask)

        # Aligns the prices to the grid.
        bid_price = np.floor(bid_price / grid_interval) * grid_interval
        ask_price = np.ceil(ask_price / grid_interval) * grid_interval
        
        # ... (rest of the order management code)
        
        recorder.record(hbt)
    return True

Key Parameters

  • grid_num: Number of price levels on each side
  • grid_interval: Price spacing between grid levels
  • half_spread: Distance from mid-price to first grid level
  • skew: Position-based price adjustment factor
  • max_position: Maximum allowed position size

Performance Considerations

Weak Skew (skew = 1)

  • More balanced position management
  • Higher number of trades
  • More stable equity curve

Strong Skew (skew = 10)

  • More aggressive position control
  • Lower maximum position exposure
  • Better Sharpe ratio but lower absolute returns
For generating order latency from the feed data file, which uses feed latency as order latency, please see Advanced Latency Modeling.

Multiple Assets

The grid trading strategy can be applied across multiple assets simultaneously. You’ll need to find proper parameters for each asset to achieve better performance:
def backtest(args):
    asset_name, asset_info = args

    # Obtains the mid-price to determine order quantity
    snapshot = np.load(f'data/{asset_name}_20230630_eod.npz')['data']
    best_bid = max(snapshot[snapshot['ev'] & BUY_EVENT == BUY_EVENT]['px'])
    best_ask = min(snapshot[snapshot['ev'] & SELL_EVENT == SELL_EVENT]['px'])
    mid_price = (best_bid + best_ask) / 2.0
    
    asset = (
        BacktestAsset()
            .data([f'data/{asset_name}_{date}.npz' for date in range(20230701, 20230732)])
            .initial_snapshot(f'data/{asset_name}_20230630_eod.npz')
            .linear_asset(1.0) 
            .intp_order_latency(latency_data)
            .log_prob_queue_model2() 
            .no_partial_fill_exchange()
            .trading_value_fee_model(-0.00005, 0.0007)
            .tick_size(asset_info['tick_size'])
            .lot_size(asset_info['lot_size'])
            .roi_lb(0)
            .roi_ub(mid_price * 5)
    )
    hbt = ROIVectorMarketDepthBacktest([asset])

    # Set order quantity to $100 notional value
    order_qty = max(round((100 / mid_price) / asset_info['lot_size']), 1) * asset_info['lot_size']

    half_spread = mid_price * 0.0008
    grid_interval = mid_price * 0.0008
    skew = mid_price * 0.000025

    recorder = Recorder(1, 50_000_000)
    gridtrading(hbt, recorder.recorder, half_spread, grid_interval, skew, order_qty)
    hbt.close()
    recorder.to_npz(f'stats/gridtrading_{asset_name}.npz')

Build docs developers (and LLMs) love