Documentation Index
Fetch the complete documentation index at: https://mintlify.com/nkaz001/hftbacktest/llms.txt
Use this file to discover all available pages before exploring further.
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
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')