nkaz001 / hftbacktest

A high-frequency trading and market-making backtesting tool in Python and Rust, which accounts for limit orders, queue positions, and latencies, utilizing full tick data for trades and order books, with real-world crypto market-making examples for Binance Futures
MIT License
1.78k stars 357 forks source link

How to Backtest on Cross-Exchange #116

Closed zhangyiyan617 closed 1 month ago

zhangyiyan617 commented 1 month ago

I am using the backtesting framework and want to implement a cross-exchange arbitrage strategy. Specifically, could you please help me to load and synchronize market data from two different exchanges and configure the framework to execute trades on both exchanges? Thank you!

nkaz001 commented 1 month ago

I plan to provide a more detailed example later, but for now, please refer to the example below. It will give you a basic understanding of cross-asset backtesting.

One important factor not covered in this example is the need to adjust for latency when trading two assets on different exchanges located in different regions. For instance, if you're backtesting an arbitrage strategy between BTCUSDT on Binance Futures and BTCUSDT on Bybit, you must consider that Binance Futures is presumably hosted in AWS Tokyo, while Bybit operates in AWS Singapore. Since data feeds and order executions occur within their respective regions, you need to account for the latency between Tokyo and Singapore, to ensure accurate backtesting results.

It may be a bit outdated, but Geographic Latency in Crypto: How to Optimally Colocate Your AWS Trading Server to Any Exchange API provides a good explanation of latency between regions.

For even more realistic results, it’s best to collect data from one location where you actually plan to trade.

from numba import njit

import numpy as np

from hftbacktest import BacktestAsset, HashMapMarketDepthBacktest, Recorder, ALL_ASSETS, LIMIT, GTX
from hftbacktest.stats import LinearAssetRecord

@njit
def test(hbt, recorder):
    half_spread_tick = 5

    while hbt.elapse(10 * 1e9) == 0:
        hbt.clear_inactive_orders(ALL_ASSETS)

        for asset_no in range(2):
            depth = hbt.depth(asset_no)
            tick_size = depth.tick_size
            mid_price = (depth.best_bid + depth.best_ask) / 2.0

            buy_order_id = 10 * asset_no + 1
            sell_order_id = 10 * asset_no + 2

            if buy_order_id not in hbt.orders(asset_no):
                order_price = round((mid_price - half_spread_tick * tick_size) / tick_size) * tick_size
                order_qty = 1
                time_in_force = GTX
                order_type = LIMIT
                hbt.submit_buy_order(asset_no, buy_order_id, order_price, order_qty, time_in_force, order_type, False)
            else:
                hbt.cancel(asset_no, buy_order_id, False)

            if sell_order_id not in hbt.orders(asset_no):
                order_price = round((mid_price + half_spread_tick * tick_size) / tick_size) * tick_size
                order_qty = 1
                time_in_force = GTX
                order_type = LIMIT
                hbt.submit_sell_order(asset_no, sell_order_id, order_price, order_qty, time_in_force, order_type, False)
            else:
                hbt.cancel(asset_no, sell_order_id, False)

        recorder.record(hbt)

btcusdt = (
    BacktestAsset()
        .data(['usdm/btcusdt_20240809.npz'])
        .initial_snapshot('usdm/btcusdt_20240808_eod.npz')
        .linear_asset(1.0) 
        .constant_latency(10_000_000, 10_000_000)
        .risk_adverse_queue_model() 
        .no_partial_fill_exchange()
        .trading_value_fee_model(0.0002, 0.0007)
        .tick_size(0.1)
        .lot_size(0.001)
)

ethusdt = (
    BacktestAsset()
        .data(['usdm/ethusdt_20240809.npz'])
        .initial_snapshot('usdm/ethusdt_20240808_eod.npz')
        .linear_asset(1.0) 
        .constant_latency(10_000_000, 10_000_000)
        .risk_adverse_queue_model() 
        .no_partial_fill_exchange()
        .trading_value_fee_model(0.0002, 0.0007)
        .tick_size(0.01)
        .lot_size(0.001)
)

hbt = HashMapMarketDepthBacktest([btcusdt, ethusdt])

recorder = Recorder(
    hbt.num_assets,
    1000
)

test(hbt, recorder.recorder)

_ = hbt.close()
zhangyiyan617 commented 1 month ago

Thank you for providing the code example! Looking forward to more rust examples!!!