Impact of Order Latency

This example illustrates the impact of order latency on the performance of the strategy.

Note: 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.

[1]:
from numba import njit
import numpy as np

from numba.typed import Dict

from hftbacktest import (
    HftBacktest,
    NONE,
    NEW,
    GTX,
    BUY,
    SELL,
    ConstantLatency,
    FeedLatency,
    IntpOrderLatency,
    SquareProbQueueModel,
    Linear,
    Stat
)

@njit
def measure_trading_intensity(order_arrival_depth, out):
    max_tick = 0
    for depth in order_arrival_depth:
        if not np.isfinite(depth):
            continue

        # Sets the tick index to 0 for the nearest possible best price
        # as the order arrival depth in ticks is measured from the mid-price
        tick = round(depth / .5) - 1

        # In a fast-moving market, buy trades can occur below the mid-price (and vice versa for sell trades)
        # since the mid-price is measured in a previous time-step;
        # however, to simplify the problem, we will exclude those cases.
        if tick < 0 or tick >= len(out):
            continue

        # All of our possible quotes within the order arrival depth,
        # excluding those at the same price, are considered executed.
        out[:tick] += 1

        max_tick = max(max_tick, tick)
    return out[:max_tick]

@njit
def linear_regression(x, y):
    sx = np.sum(x)
    sy = np.sum(y)
    sx2 = np.sum(x ** 2)
    sxy = np.sum(x * y)
    w = len(x)
    slope = (w * sxy - sx * sy) / (w * sx2 - sx**2)
    intercept = (sy - slope * sx) / w
    return slope, intercept

@njit
def compute_coeff(xi, gamma, delta, A, k):
    inv_k = np.divide(1, k)
    c1 = 1 / (xi * delta) * np.log(1 + xi * delta * inv_k)
    c2 = np.sqrt(np.divide(gamma, 2 * A * delta * k) * ((1 + xi * delta * inv_k) ** (k / (xi * delta) + 1)))
    return c1, c2

@njit
def gridtrading_glft_mm(hbt, stat):
    arrival_depth = np.full(10_000_000, np.nan, np.float64)
    mid_price_chg = np.full(10_000_000, np.nan, np.float64)

    t = 0
    prev_mid_price_tick = np.nan
    mid_price_tick = np.nan

    tmp = np.zeros(500, np.float64)
    ticks = np.arange(len(tmp)) + .5

    A = np.nan
    k = np.nan
    volatility = np.nan
    gamma = 0.05
    delta = 1
    adj1 = 1
    adj2 = 0.05

    order_qty = 1
    max_position = 20
    grid_num = 20

    # Checks every 100 milliseconds.
    while hbt.elapse(100_000):
        #--------------------------------------------------------
        # Records market order's arrival depth from the mid-price.
        if not np.isnan(mid_price_tick):
            depth = -np.inf
            for trade in hbt.last_trades:
                side = trade[3]
                trade_price_tick = trade[4] / hbt.tick_size

                if side == BUY:
                    depth = np.nanmax([trade_price_tick - mid_price_tick, depth])
                else:
                    depth = np.nanmax([mid_price_tick - trade_price_tick, depth])
            arrival_depth[t] = depth

        hbt.clear_last_trades()

        prev_mid_price_tick = mid_price_tick
        mid_price_tick = (hbt.best_bid_tick + hbt.best_ask_tick) / 2.0

        # Records the mid-price change for volatility calculation.
        mid_price_chg[t] = mid_price_tick - prev_mid_price_tick

        #--------------------------------------------------------
        # Calibrates A, k and calculates the market volatility.

        # Updates A, k, and the volatility every 5-sec.
        if t % 50 == 0:
            # Window size is 10-minute.
            if t >= 6_000 - 1:
                # Calibrates A, k
                tmp[:] = 0
                lambda_ = measure_trading_intensity(arrival_depth[t + 1 - 6_000:t + 1], tmp)
                lambda_ = lambda_[:70] / 600
                x = ticks[:len(lambda_)]
                y = np.log(lambda_)
                k_, logA = linear_regression(x, y)
                A = np.exp(logA)
                k = -k_

                # Updates the volatility.
                volatility = np.nanstd(mid_price_chg[t + 1 - 6_000:t + 1]) * np.sqrt(10)

        #--------------------------------------------------------
        # Computes bid price and ask price.

        c1, c2 = compute_coeff(gamma, gamma, delta, A, k)

        half_spread = (c1 + 1 / 2 * c2 * volatility) * adj1
        skew = c2 * volatility * adj2

        bid_depth = half_spread + skew * hbt.position
        ask_depth = half_spread - skew * hbt.position

        # If the depth is invalid, set a large spread to prevent execution.
        if not np.isfinite(bid_depth):
            bid_depth = 1_000
        if not np.isfinite(ask_depth):
            ask_depth = 1_000

        bid_price = min(round(mid_price_tick - bid_depth), hbt.best_bid_tick) * hbt.tick_size
        ask_price = max(round(mid_price_tick + ask_depth), hbt.best_ask_tick) * hbt.tick_size

        grid_interval = round(max(half_spread, 1)) * hbt.tick_size
        bid_price = np.floor(bid_price / grid_interval) * grid_interval
        ask_price = np.ceil(ask_price / grid_interval) * grid_interval

        #--------------------------------------------------------
        # Updates quotes.

        hbt.clear_inactive_orders()

        # Creates a new grid for buy orders.
        new_bid_orders = Dict.empty(np.int64, np.float64)
        if hbt.position < max_position:
            for i in range(grid_num):
                bid_price -= i * grid_interval
                bid_price_tick = round(bid_price / hbt.tick_size)

                # order price in tick is used as order id.
                new_bid_orders[bid_price_tick] = bid_price
        for order in hbt.orders.values():
            # Cancels if an order is not in the new grid.
            if order.side == BUY and order.cancellable and order.order_id not in new_bid_orders:
                hbt.cancel(order.order_id)
        for order_id, order_price in new_bid_orders.items():
            # Posts an order if it doesn't exist.
            if order_id not in hbt.orders:
                hbt.submit_buy_order(order_id, order_price, order_qty, GTX)

        # Creates a new grid for sell orders.
        new_ask_orders = Dict.empty(np.int64, np.float64)
        if hbt.position > -max_position:
            for i in range(grid_num):
                ask_price += i * grid_interval
                ask_price_tick = round(ask_price / hbt.tick_size)

                # order price in tick is used as order id.
                new_ask_orders[ask_price_tick] = ask_price
        for order in hbt.orders.values():
            # Cancels if an order is not in the new grid.
            if order.side == SELL and order.cancellable and order.order_id not in new_ask_orders:
                hbt.cancel(order.order_id)
        for order_id, order_price in new_ask_orders.items():
            # Posts an order if it doesn't exist.
            if order_id not in hbt.orders:
                hbt.submit_sell_order(order_id, order_price, order_qty, GTX)

        t += 1

        if t >= len(arrival_depth) or t >= len(mid_price_chg):
            raise Exception

        # Records the current state for stat calculation.
        stat.record(hbt)

Order Latency from Feed Latency

[2]:
hbt = HftBacktest(
    [
        'data/ethusdt_20230401.npz',
        'data/ethusdt_20230402.npz',
        'data/ethusdt_20230403.npz',
        'data/ethusdt_20230404.npz',
        'data/ethusdt_20230405.npz',
    ],
    tick_size=0.01,
    lot_size=0.001,
    maker_fee=-0.00005,
    taker_fee=0.0007,
    order_latency=FeedLatency(),
    queue_model=SquareProbQueueModel(),
    asset_type=Linear,
    snapshot='data/ethusdt_20230331_eod.npz',
    trade_list_size=10_000
)

stat = Stat(hbt)

gridtrading_glft_mm(hbt, stat.recorder)

stat.summary(capital=25_000)
Load data/ethusdt_20230401.npz
Load data/ethusdt_20230402.npz
Load data/ethusdt_20230403.npz
Load data/ethusdt_20230404.npz
Load data/ethusdt_20230405.npz
=========== Summary ===========
Sharpe ratio: 4.6
Sortino ratio: 3.3
Risk return ratio: 43.1
Annualised return: 119.45 %
Max. draw down: 2.77 %
The number of trades per day: 3212
Avg. daily trading volume: 3212
Avg. daily trading amount: 5886441
Max leverage: 3.40
Median leverage: 0.22
../_images/tutorials_Impact_of_Order_Latency_3_1.png

Historical Order Latency

[3]:
latency_data = np.concatenate(
    [np.load('../latency/ethusdt_{}_latency.npz'.format(date))['data'] for date in range(20230401, 20230406)]
)

hbt = HftBacktest(
    [
        'data/ethusdt_20230401.npz',
        'data/ethusdt_20230402.npz',
        'data/ethusdt_20230403.npz',
        'data/ethusdt_20230404.npz',
        'data/ethusdt_20230405.npz',
    ],
    tick_size=0.01,
    lot_size=0.001,
    maker_fee=-0.00005,
    taker_fee=0.0007,
    order_latency=IntpOrderLatency(data=latency_data),
    queue_model=SquareProbQueueModel(),
    asset_type=Linear,
    snapshot='data/ethusdt_20230331_eod.npz',
    trade_list_size=10_000
)

stat = Stat(hbt)

gridtrading_glft_mm(hbt, stat.recorder)

stat.summary(capital=25_000)
Load data/ethusdt_20230401.npz
Load data/ethusdt_20230402.npz
Load data/ethusdt_20230403.npz
Load data/ethusdt_20230404.npz
Load data/ethusdt_20230405.npz
=========== Summary ===========
Sharpe ratio: 0.4
Sortino ratio: 0.3
Risk return ratio: 2.8
Annualised return: 11.03 %
Max. draw down: 4.00 %
The number of trades per day: 3493
Avg. daily trading volume: 3493
Avg. daily trading amount: 6401297
Max leverage: 2.47
Median leverage: 0.22
../_images/tutorials_Impact_of_Order_Latency_5_1.png

Order Latency from Amplified Feed Latency

[4]:
hbt = HftBacktest(
    [
        'data/ethusdt_20230401.npz',
        'data/ethusdt_20230402.npz',
        'data/ethusdt_20230403.npz',
        'data/ethusdt_20230404.npz',
        'data/ethusdt_20230405.npz',
    ],
    tick_size=0.01,
    lot_size=0.001,
    maker_fee=-0.00005,
    taker_fee=0.0007,
    order_latency=FeedLatency(entry_latency_mul=4, resp_latency_mul=3),
    queue_model=SquareProbQueueModel(),
    asset_type=Linear,
    snapshot='data/ethusdt_20230331_eod.npz',
    trade_list_size=10_000
)

stat = Stat(hbt)

gridtrading_glft_mm(hbt, stat.recorder)

stat.summary(capital=25_000)
Load data/ethusdt_20230401.npz
Load data/ethusdt_20230402.npz
Load data/ethusdt_20230403.npz
Load data/ethusdt_20230404.npz
Load data/ethusdt_20230405.npz
=========== Summary ===========
Sharpe ratio: 0.2
Sortino ratio: 0.2
Risk return ratio: 1.8
Annualised return: 6.66 %
Max. draw down: 3.61 %
The number of trades per day: 3193
Avg. daily trading volume: 3193
Avg. daily trading amount: 5849525
Max leverage: 4.57
Median leverage: 0.22
../_images/tutorials_Impact_of_Order_Latency_7_1.png