High-Frequency Grid Trading
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.
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.
[1]:
from numba import njit
import pandas as pd
import numpy as np
from numba.typed import Dict
from hftbacktest import NONE, NEW, HftBacktest, GTX, FeedLatency, SquareProbQueueModel, BUY, SELL, Linear, Stat, reset
@njit
def gridtrading(hbt, stat):
max_position = 5
grid_interval = hbt.tick_size * 10
grid_num = 20
half_spread = hbt.tick_size * 20
# Running interval in microseconds
while hbt.elapse(100_000):
# Clears cancelled, filled or expired orders.
hbt.clear_inactive_orders()
mid_price = (hbt.best_bid + hbt.best_ask) / 2.0
bid_order_begin = np.floor((mid_price - half_spread) / grid_interval) * grid_interval
ask_order_begin = np.ceil((mid_price + half_spread) / grid_interval) * grid_interval
order_qty = 0.1
last_order_id = -1
# 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_order_begin -= i * grid_interval
bid_order_tick = round(bid_order_begin / hbt.tick_size)
# Do not post buy orders above the best bid.
if bid_order_tick > hbt.best_bid_tick:
continue
# order price in tick is used as order id.
new_bid_orders[bid_order_tick] = bid_order_begin
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)
last_order_id = 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)
last_order_id = order_id
# 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_order_begin += i * grid_interval
ask_order_tick = round(ask_order_begin / hbt.tick_size)
# Do not post sell orders below the best ask.
if ask_order_tick < hbt.best_ask_tick:
continue
# order price in tick is used as order id.
new_ask_orders[ask_order_tick] = ask_order_begin
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)
last_order_id = 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)
last_order_id = order_id
# All order requests are considered to be requested at the same time.
# Waits until one of the order responses is received.
if last_order_id >= 0:
if not hbt.wait_order_response(last_order_id):
return False
# Records the current state for stat calculation.
stat.record(hbt)
return True
[2]:
hbt = HftBacktest(
[
'data/ethusdt_20221003.npz',
'data/ethusdt_20221004.npz',
'data/ethusdt_20221005.npz',
'data/ethusdt_20221006.npz',
'data/ethusdt_20221007.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_20221002_eod.npz'
)
stat = Stat(hbt)
Load data/ethusdt_20221003.npz
[3]:
%%time
gridtrading(hbt, stat.recorder)
Load data/ethusdt_20221004.npz
Load data/ethusdt_20221005.npz
Load data/ethusdt_20221006.npz
Load data/ethusdt_20221007.npz
CPU times: user 3min 58s, sys: 6.03 s, total: 4min 4s
Wall time: 4min 5s
[3]:
True
[4]:
stat.summary(capital=15_000)
=========== Summary ===========
Sharpe ratio: 20.9
Sortino ratio: 22.4
Risk return ratio: 211.5
Annualised return: 330.53 %
Max. draw down: 1.56 %
The number of trades per day: 5954
Avg. daily trading volume: 595
Avg. daily trading amount: 798115
Max leverage: 0.52
Median leverage: 0.21
High-Frequency Grid Trading with Skewing
By incorporating position-based skewing, the strategy’s risk-adjusted returns can be improved.
[5]:
@njit
def gridtrading(hbt, stat, skew):
max_position = 5
grid_interval = hbt.tick_size * 10
grid_num = 20
half_spread = hbt.tick_size * 20
# Running interval in microseconds
while hbt.elapse(100_000):
# Clears cancelled, filled or expired orders.
hbt.clear_inactive_orders()
mid_price = (hbt.best_bid + hbt.best_ask) / 2.0
reservation_price = mid_price - skew * hbt.position * hbt.tick_size
bid_order_begin = np.floor((reservation_price - half_spread) / grid_interval) * grid_interval
ask_order_begin = np.ceil((reservation_price + half_spread) / grid_interval) * grid_interval
order_qty = 0.1 # np.round(notional_order_qty / mid_price / hbt.lot_size) * hbt.lot_size
last_order_id = -1
# Creates a new grid for buy orders.
new_bid_orders = Dict.empty(np.int64, np.float64)
if hbt.position < max_position: # hbt.position * mid_price < max_notional_position
for i in range(grid_num):
bid_order_begin -= i * grid_interval
bid_order_tick = round(bid_order_begin / hbt.tick_size)
# Do not post buy orders above the best bid.
if bid_order_tick > hbt.best_bid_tick:
continue
# order price in tick is used as order id.
new_bid_orders[bid_order_tick] = bid_order_begin
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)
last_order_id = 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)
last_order_id = order_id
# Creates a new grid for sell orders.
new_ask_orders = Dict.empty(np.int64, np.float64)
if hbt.position > -max_position: # hbt.position * mid_price > -max_notional_position
for i in range(grid_num):
ask_order_begin += i * grid_interval
ask_order_tick = round(ask_order_begin / hbt.tick_size)
# Do not post sell orders below the best ask.
if ask_order_tick < hbt.best_ask_tick:
continue
# order price in tick is used as order id.
new_ask_orders[ask_order_tick] = ask_order_begin
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)
last_order_id = 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)
last_order_id = order_id
# All order requests are considered to be requested at the same time.
# Waits until one of the order responses is received.
if last_order_id >= 0:
if not hbt.wait_order_response(last_order_id):
return False
# Records the current state for stat calculation.
stat.record(hbt)
return True
Weak skew
[6]:
reset(
hbt,
[
'data/ethusdt_20221003.npz',
'data/ethusdt_20221004.npz',
'data/ethusdt_20221005.npz',
'data/ethusdt_20221006.npz',
'data/ethusdt_20221007.npz'
],
snapshot='data/ethusdt_20221002_eod.npz'
)
stat = Stat(hbt)
skew = 1
gridtrading(hbt, stat.recorder, skew)
stat.summary(capital=15_000)
Load data/ethusdt_20221003.npz
Load data/ethusdt_20221004.npz
Load data/ethusdt_20221005.npz
Load data/ethusdt_20221006.npz
Load data/ethusdt_20221007.npz
=========== Summary ===========
Sharpe ratio: 18.0
Sortino ratio: 17.5
Risk return ratio: 169.2
Annualised return: 166.77 %
Max. draw down: 0.99 %
The number of trades per day: 6488
Avg. daily trading volume: 648
Avg. daily trading amount: 870207
Max leverage: 0.50
Median leverage: 0.10
Strong skew
[7]:
reset(
hbt,
[
'data/ethusdt_20221003.npz',
'data/ethusdt_20221004.npz',
'data/ethusdt_20221005.npz',
'data/ethusdt_20221006.npz',
'data/ethusdt_20221007.npz'
],
snapshot='data/ethusdt_20221002_eod.npz'
)
stat = Stat(hbt)
skew = 10
gridtrading(hbt, stat.recorder, skew)
stat.summary(capital=15_000)
Load data/ethusdt_20221003.npz
Load data/ethusdt_20221004.npz
Load data/ethusdt_20221005.npz
Load data/ethusdt_20221006.npz
Load data/ethusdt_20221007.npz
=========== Summary ===========
Sharpe ratio: 29.3
Sortino ratio: 33.4
Risk return ratio: 735.4
Annualised return: 100.30 %
Max. draw down: 0.14 %
The number of trades per day: 6636
Avg. daily trading volume: 663
Avg. daily trading amount: 889749
Max leverage: 0.51
Median leverage: 0.02