Overview
HftBacktest supports running strategies across multiple exchanges and trading pairs simultaneously. This enables arbitrage, cross-exchange hedging, and diversified market making strategies using a single unified codebase.
Architecture
Connector-Based Design
Key points:
Each connector runs as a separate process
All connectors must run on the same machine as the bot
Communication via shared memory (IPC) for minimal latency
Each connector can manage multiple trading pairs
Setup Multiple Connectors
1. Create Configuration Files
Create separate config files for each exchange:
binance_futures.toml
bybit.toml
binance_spot.toml
stream_url = "wss://fstream.binancefuture.com/ws"
api_url = "https://testnet.binancefuture.com"
order_prefix = "bf"
api_key = "BINANCE_API_KEY"
secret = "BINANCE_SECRET"
2. Start Multiple Connectors
Run each connector in a separate terminal:
Terminal 1 - Binance Futures
connector --name bf --connector binancefutures --config binance_futures.toml
connector --name bybit --connector bybit --config bybit.toml
Terminal 3 - Binance Spot
connector --name bs --connector binancespot --config binance_spot.toml
Use unique --name values for each connector. This name is used to identify the connector in your bot code.
Multi-Asset Backtesting
Example: Two-Asset Market Making
import numpy as np
from numba import njit
from hftbacktest import (
BacktestAsset,
HashMapMarketDepthBacktest,
BUY ,
SELL ,
GTX ,
LIMIT
)
@njit
def multi_asset_market_making ( hbt ):
"""Market making on two correlated assets"""
while hbt.elapse( 10_000_000 ) == 0 : # 10ms
# Process both assets
for asset_no in range ( 2 ):
hbt.clear_inactive_orders(asset_no)
depth = hbt.depth(asset_no)
tick_size = depth.tick_size
lot_size = depth.lot_size
mid_price = (depth.best_bid + depth.best_ask) / 2.0
position = hbt.position(asset_no)
# Simple market making logic
half_spread = 0.0005 * mid_price
skew = - 0.001 * position
reservation_price = mid_price + skew
new_bid = reservation_price - half_spread
new_ask = reservation_price + half_spread
new_bid_tick = min (
np.round(new_bid / tick_size),
depth.best_bid_tick
)
new_ask_tick = max (
np.round(new_ask / tick_size),
depth.best_ask_tick
)
order_qty = np.round( 100 / mid_price / lot_size) * lot_size
# Submit orders for this asset
hbt.submit_buy_order(
asset_no,
new_bid_tick,
new_bid_tick * tick_size,
order_qty,
GTX ,
LIMIT ,
False
)
hbt.submit_sell_order(
asset_no,
new_ask_tick,
new_ask_tick * tick_size,
order_qty,
GTX ,
LIMIT ,
False
)
return True
if __name__ == '__main__' :
# Asset 1: BTC on Binance Futures
asset_btc = (
BacktestAsset()
.data([ 'data/binance/btcusdt_20220831.npz' ])
.linear_asset( 1.0 )
.intp_order_latency([ 'latency/binance_20220831.npz' ])
.power_prob_queue_model( 2.0 )
.no_partial_fill_exchange()
.trading_value_fee_model( - 0.00005 , 0.0007 )
.tick_size( 0.1 )
.lot_size( 0.001 )
)
# Asset 2: ETH on Binance Futures
asset_eth = (
BacktestAsset()
.data([ 'data/binance/ethusdt_20220831.npz' ])
.linear_asset( 1.0 )
.intp_order_latency([ 'latency/binance_20220831.npz' ])
.power_prob_queue_model( 2.0 )
.no_partial_fill_exchange()
.trading_value_fee_model( - 0.00005 , 0.0007 )
.tick_size( 0.01 )
.lot_size( 0.001 )
)
# Create multi-asset backtest
hbt = HashMapMarketDepthBacktest([asset_btc, asset_eth])
multi_asset_market_making(hbt)
Multi-Exchange Live Trading (Rust)
Example: Cross-Exchange Strategy
use hftbacktest :: {
live :: {
Instrument ,
LiveBot ,
LiveBotBuilder ,
LoggingRecorder ,
ipc :: iceoryx :: IceoryxUnifiedChannel ,
},
prelude :: { Bot , HashMapMarketDepth },
};
fn prepare_multi_exchange () -> LiveBot < IceoryxUnifiedChannel , HashMapMarketDepth > {
LiveBotBuilder :: new ()
// Binance Futures - BTC
. register ( Instrument :: new (
"bf" , // connector name from --name
"btcusdt" , // lowercase for Binance
0.1 , // tick_size
0.001 , // lot_size
HashMapMarketDepth :: new ( 0.1 , 0.001 ),
0 , // asset_no = 0
))
// Bybit - BTC
. register ( Instrument :: new (
"bybit" , // connector name
"BTCUSDT" , // uppercase for Bybit
0.1 ,
0.001 ,
HashMapMarketDepth :: new ( 0.1 , 0.001 ),
1 , // asset_no = 1
))
// Binance Spot - BTC
. register ( Instrument :: new (
"bs" ,
"btcusdt" , // lowercase for Binance
0.01 ,
0.00001 ,
HashMapMarketDepth :: new ( 0.01 , 0.00001 ),
2 , // asset_no = 2
))
. build ()
. unwrap ()
}
fn cross_exchange_arbitrage < MD , I >(
hbt : & mut LiveBot < I , MD >,
) -> Result <(), ErrorKind >
where
MD : MarketDepth ,
I : LiveChannel ,
{
loop {
// Check if we should exit
if ! hbt . elapse ( 100_000_000 ) ? { // 100ms
return Ok (());
}
// Get depths from all exchanges
let depth_binance = hbt . depth ( 0 ); // Binance Futures
let depth_bybit = hbt . depth ( 1 ); // Bybit
let depth_spot = hbt . depth ( 2 ); // Binance Spot
// Check for arbitrage opportunities
let binance_mid = ( depth_binance . best_bid + depth_binance . best_ask) / 2.0 ;
let bybit_mid = ( depth_bybit . best_bid + depth_bybit . best_ask) / 2.0 ;
let spot_mid = ( depth_spot . best_bid + depth_spot . best_ask) / 2.0 ;
// Example: Futures-Spot arbitrage
let basis = binance_mid - spot_mid ;
let threshold = 10.0 ; // $10 basis
if basis > threshold {
// Sell futures, buy spot
println! ( "Arbitrage opportunity: basis = {}" , basis );
// Implement your arbitrage logic here
} else if basis < - threshold {
// Buy futures, sell spot
println! ( "Arbitrage opportunity: basis = {}" , basis );
}
}
}
fn main () {
tracing_subscriber :: fmt :: init ();
let mut hbt = prepare_multi_exchange ();
hbt . run () . unwrap ();
cross_exchange_arbitrage ( & mut hbt ) . unwrap ();
hbt . close () . unwrap ();
}
Strategy Patterns
1. Statistical Arbitrage
@njit
def stat_arb ( hbt ):
"""Trade based on price divergence between correlated assets"""
while hbt.elapse( 100_000_000 ) == 0 : # 100ms
# Get mid prices
mid_0 = (hbt.depth( 0 ).best_bid + hbt.depth( 0 ).best_ask) / 2.0
mid_1 = (hbt.depth( 1 ).best_bid + hbt.depth( 1 ).best_ask) / 2.0
# Calculate z-score of price ratio
ratio = mid_0 / mid_1
# In practice, use rolling mean and std
mean_ratio = 100.0
std_ratio = 2.0
z_score = (ratio - mean_ratio) / std_ratio
# Trade on divergence
if z_score > 2.0 :
# Ratio too high: sell asset 0, buy asset 1
print ( f "Signal: Sell asset 0, Buy asset 1 (z= { z_score } )" )
elif z_score < - 2.0 :
# Ratio too low: buy asset 0, sell asset 1
print ( f "Signal: Buy asset 0, Sell asset 1 (z= { z_score } )" )
return True
2. Cross-Exchange Market Making
@njit
def cross_exchange_mm ( hbt ):
"""Make markets on one exchange hedged by another"""
primary = 0 # Make markets here
hedge = 1 # Hedge on this exchange
while hbt.elapse( 10_000_000 ) == 0 :
hbt.clear_inactive_orders(primary)
# Use hedge exchange mid as reference price
hedge_depth = hbt.depth(hedge)
ref_price = (hedge_depth.best_bid + hedge_depth.best_ask) / 2.0
# Make markets on primary exchange around reference
primary_depth = hbt.depth(primary)
spread = 0.001 * ref_price # 10 bps
bid = ref_price - spread
ask = ref_price + spread
# Submit orders...
# (order submission code)
return True
3. Triangular Arbitrage
@njit
def triangular_arb ( hbt ):
"""Arbitrage across three related pairs"""
# Example: BTC/USDT, ETH/USDT, ETH/BTC
btc_usdt = 0
eth_usdt = 1
eth_btc = 2
while hbt.elapse( 100_000_000 ) == 0 :
# Get mid prices
btc_price = (hbt.depth(btc_usdt).best_bid +
hbt.depth(btc_usdt).best_ask) / 2.0
eth_price = (hbt.depth(eth_usdt).best_bid +
hbt.depth(eth_usdt).best_ask) / 2.0
eth_btc_price = (hbt.depth(eth_btc).best_bid +
hbt.depth(eth_btc).best_ask) / 2.0
# Calculate implied ETH/BTC from USD pairs
implied_eth_btc = eth_price / btc_price
# Check for arbitrage
diff = eth_btc_price - implied_eth_btc
threshold = 0.0001 # 1 bps
if abs (diff) > threshold:
print ( f "Triangular arb opportunity: { diff } " )
# Execute arbitrage trade
return True
Important Considerations
Different exchanges have different latencies:
Binance Market Maker endpoints: ~5-10ms
Standard endpoints: ~20-50ms
Cross-exchange strategies need to account for latency skew
Use latency_offset parameter when backtesting: asset.intp_order_latency(
[ 'latency/exchange1.npz' ],
latency_offset = 5_000_000 # 5ms offset
)
Each exchange uses different symbol conventions:
Binance : lowercase (btcusdt)
Bybit : uppercase (BTCUSDT)
Binance 1000x symbols : 1000shibusdt
Always check the exchange documentation.
Account for different fee structures: # Binance MM program
.trading_value_fee_model( - 0.00005 , 0.0007 )
# Standard tier
.trading_value_fee_model( 0.0002 , 0.0005 )
Fee differences can make or break arbitrage strategies.
Each exchange has different:
Maximum position sizes
Leverage limits
Order size restrictions
Rate limits
Implement proper risk management for each exchange.
For multi-exchange strategies:
Ensure system clock is synchronized (use NTP)
Account for timestamp differences between exchanges
Some exchanges use different timestamp precision
Best Practices
Independent Connectors Each connector is independent:
Can restart without affecting others
Isolated error handling
Separate rate limiting
Shared Memory Efficiency IPC communication is fast but:
All components on same machine
Minimal serialization overhead
Direct memory access
Atomic Operations When trading across exchanges:
Use timeouts for order confirmation
Implement rollback logic
Handle partial fills carefully
Monitor All Connections Watch for connection issues:
Implement error handlers for each exchange
Log connection states
Have reconnection logic
Example: Multi-Asset Grid Trading
Here’s a complete example trading multiple pairs across exchanges:
from hftbacktest import BacktestAsset, HashMapMarketDepthBacktest
# Load data for multiple exchanges
assets = []
# Binance Futures assets
for symbol in [ 'btcusdt' , 'ethusdt' ]:
asset = (
BacktestAsset()
.data([ f 'data/binance/ { symbol } _20240808.npz' ])
.linear_asset( 1.0 )
.intp_order_latency([ 'latency/binance_20240808.npz' ])
.power_prob_queue_model( 2.0 )
.no_partial_fill_exchange()
.trading_value_fee_model( - 0.00005 , 0.0007 )
.tick_size( 0.1 if symbol == 'btcusdt' else 0.01 )
.lot_size( 0.001 )
)
assets.append(asset)
# Bybit assets
for symbol in [ 'BTCUSDT' , 'ETHUSDT' ]:
asset = (
BacktestAsset()
.data([ f 'data/bybit/ { symbol } _20240808.npz' ])
.linear_asset( 1.0 )
.intp_order_latency(
[ 'latency/bybit_20240808.npz' ],
latency_offset = 2_000_000 # 2ms higher latency
)
.power_prob_queue_model( 2.0 )
.no_partial_fill_exchange()
.trading_value_fee_model( - 0.00005 , 0.0007 )
.tick_size( 0.1 if 'BTC' in symbol else 0.01 )
.lot_size( 0.001 )
)
assets.append(asset)
# Run backtest on all assets
hbt = HashMapMarketDepthBacktest(assets)
multi_exchange_strategy(hbt)
Resources