HeyTraders Documentation

Pair Trading & Statistical Arbitrage Strategies

Basic Spread Z-Score

Tags: pair_trading, spread, zscore, mean_reversion, cross_mode Functions: get_data, spread_zscore Description: Enters a pair trade when the BTC/ETH spread Z-score reaches an extreme. Requires cross mode. Code:

btc = get_data('BINANCEFUTURES:BTC/USDT:USDT')
eth = get_data('BINANCEFUTURES:ETH/USDT:USDT')

z_score = spread_zscore(eth.close, btc.close, 20)
z_now = z_score[-1]
z_prev = z_score[-2]

# Long spread (long ETH, short BTC) when z crosses below -2
long_spread = (z_now < -2) and (z_prev >= -2)
# Short spread (short ETH, long BTC) when z crosses above 2
short_spread = (z_now > 2) and (z_prev <= 2)
# Exit when z-score normalizes
exit_cond = (z_now > -0.5) and (z_now < 0.5)

emit(long_spread, entry('BINANCEFUTURES:ETH/USDT:USDT', 'LONG', Weight(0.5), when=NoPosition()))
emit(long_spread, entry('BINANCEFUTURES:BTC/USDT:USDT', 'SHORT', Weight(0.5), when=NoPosition()))
emit(short_spread, entry('BINANCEFUTURES:ETH/USDT:USDT', 'SHORT', Weight(0.5), when=NoPosition()))
emit(short_spread, entry('BINANCEFUTURES:BTC/USDT:USDT', 'LONG', Weight(0.5), when=NoPosition()))
emit(exit_cond, exit_position('BINANCEFUTURES:ETH/USDT:USDT', when=HasPosition()))
emit(exit_cond, exit_position('BINANCEFUTURES:BTC/USDT:USDT', when=HasPosition()))

OLS Hedge Ratio Pair Trade

Tags: pair_trading, ols, hedge_ratio, cointegration, cross_mode Functions: get_data, rolling_ols_beta, ts_zscore Description: Calculates a dynamic hedge ratio using rolling OLS and sizes the pair to trade that residual. This long/short example enters only for a positive, finite beta and uses one shared USDT account and equal leverage on both legs. Code:

btc = get_data('BINANCEFUTURES:BTC/USDT:USDT')
eth = get_data('BINANCEFUTURES:ETH/USDT:USDT')

# Rolling hedge ratio (beta)
beta = rolling_ols_beta(eth.close, btc.close, 60)

# Spread with hedge ratio
spread = eth.close - beta * btc.close
z_score = ts_zscore(spread, 20)
z_now = z_score[-1]
z_prev = z_score[-2]

# Trade only when spread crosses into an extreme band
long_spread = (z_now < -2) and (z_prev >= -2)
short_spread = (z_now > 2) and (z_prev <= 2)
exit_cond = (z_now > -0.3) and (z_now < 0.3)

# For ETH - beta * BTC, target BTC units = beta * ETH units.
# Translate the base-quantity ratio to capital weights before venue rounding.
beta_now = beta[-1]
eth_price = eth.close[-1]
btc_price = btc.close[-1]
if beta_now is not None and math.isfinite(beta_now) and beta_now > 0:
    if eth_price is not None and btc_price is not None and eth_price > 0 and btc_price > 0:
        gross_per_unit = eth_price + beta_now * btc_price
        if math.isfinite(gross_per_unit):
            eth_weight = eth_price / gross_per_unit
            btc_weight = beta_now * btc_price / gross_per_unit
            emit(long_spread, entry('BINANCEFUTURES:ETH/USDT:USDT', 'LONG', Weight(eth_weight), when=NoPosition()))
            emit(long_spread, entry('BINANCEFUTURES:BTC/USDT:USDT', 'SHORT', Weight(btc_weight), when=NoPosition()))
            emit(short_spread, entry('BINANCEFUTURES:ETH/USDT:USDT', 'SHORT', Weight(eth_weight), when=NoPosition()))
            emit(short_spread, entry('BINANCEFUTURES:BTC/USDT:USDT', 'LONG', Weight(btc_weight), when=NoPosition()))
# Keep exits independent of whether a new entry can be sized.
emit(exit_cond, exit_position('BINANCEFUTURES:ETH/USDT:USDT', when=HasPosition()))
emit(exit_cond, exit_position('BINANCEFUTURES:BTC/USDT:USDT', when=HasPosition()))

Kalman Filter Pair Trade

Tags: pair_trading, kalman, adaptive, hedge_ratio, cross_mode Functions: get_data, kalman_hedge_ratio, ts_zscore Description: Estimates a dynamic hedge ratio using a Kalman filter and sizes the pair to trade that residual. This long/short example enters only for a positive, finite beta and uses one shared USDT account and equal leverage on both legs. Code:

btc = get_data('BINANCEFUTURES:BTC/USDT:USDT')
eth = get_data('BINANCEFUTURES:ETH/USDT:USDT')

# Kalman filter hedge ratio
beta = kalman_hedge_ratio(eth.close, btc.close)

# Spread
spread = eth.close - beta * btc.close
z_score = ts_zscore(spread, 20)
z_now = z_score[-1]
z_prev = z_score[-2]

long_spread = (z_now < -2) and (z_prev >= -2)
short_spread = (z_now > 2) and (z_prev <= 2)
exit_cond = (z_now > -0.5) and (z_now < 0.5)

# For ETH - beta * BTC, target BTC units = beta * ETH units.
# Translate the base-quantity ratio to capital weights before venue rounding.
beta_now = beta[-1]
eth_price = eth.close[-1]
btc_price = btc.close[-1]
if beta_now is not None and math.isfinite(beta_now) and beta_now > 0:
    if eth_price is not None and btc_price is not None and eth_price > 0 and btc_price > 0:
        gross_per_unit = eth_price + beta_now * btc_price
        if math.isfinite(gross_per_unit):
            eth_weight = eth_price / gross_per_unit
            btc_weight = beta_now * btc_price / gross_per_unit
            emit(long_spread, entry('BINANCEFUTURES:ETH/USDT:USDT', 'LONG', Weight(eth_weight), when=NoPosition()))
            emit(long_spread, entry('BINANCEFUTURES:BTC/USDT:USDT', 'SHORT', Weight(btc_weight), when=NoPosition()))
            emit(short_spread, entry('BINANCEFUTURES:ETH/USDT:USDT', 'SHORT', Weight(eth_weight), when=NoPosition()))
            emit(short_spread, entry('BINANCEFUTURES:BTC/USDT:USDT', 'LONG', Weight(btc_weight), when=NoPosition()))
# Keep exits independent of whether a new entry can be sized.
emit(exit_cond, exit_position('BINANCEFUTURES:ETH/USDT:USDT', when=HasPosition()))
emit(exit_cond, exit_position('BINANCEFUTURES:BTC/USDT:USDT', when=HasPosition()))

Ratio Trading

Tags: pair_trading, ratio, simple, cross_mode Functions: get_data, ts_zscore Description: Trades when the price ratio between two assets deviates from its mean. Code:

btc = get_data('BINANCEFUTURES:BTC/USDT:USDT')
eth = get_data('BINANCEFUTURES:ETH/USDT:USDT')

# Price ratio
ratio = eth.close / btc.close
z_score = ts_zscore(ratio, 30)
z_now = z_score[-1]
z_prev = z_score[-2]

# Ratio crosses too low: long ETH, short BTC
long_eth = (z_now < -2) and (z_prev >= -2)
# Ratio crosses too high: short ETH, long BTC
short_eth = (z_now > 2) and (z_prev <= 2)
exit_cond = (z_now > -0.5) and (z_now < 0.5)

emit(long_eth, entry('BINANCEFUTURES:ETH/USDT:USDT', 'LONG', Weight(0.5), when=NoPosition()))
emit(long_eth, entry('BINANCEFUTURES:BTC/USDT:USDT', 'SHORT', Weight(0.5), when=NoPosition()))
emit(short_eth, entry('BINANCEFUTURES:ETH/USDT:USDT', 'SHORT', Weight(0.5), when=NoPosition()))
emit(short_eth, entry('BINANCEFUTURES:BTC/USDT:USDT', 'LONG', Weight(0.5), when=NoPosition()))
emit(exit_cond, exit_position('BINANCEFUTURES:ETH/USDT:USDT', when=HasPosition()))
emit(exit_cond, exit_position('BINANCEFUTURES:BTC/USDT:USDT', when=HasPosition()))

Correlation Breakdown Trade

Tags: pair_trading, correlation, breakdown, divergence, cross_mode Functions: get_data, ts_corr, ts_zscore Description: Bets on convergence when highly correlated assets temporarily diverge. Code:

btc = get_data('BINANCEFUTURES:BTC/USDT:USDT')
eth = get_data('BINANCEFUTURES:ETH/USDT:USDT')

# Rolling correlation
corr = ts_corr(btc.close, eth.close, 30)

# Returns
btc_ret = (btc.close - btc.close.shift(1)) / btc.close.shift(1)
eth_ret = (eth.close - eth.close.shift(1)) / eth.close.shift(1)

# Spread of returns
ret_spread = eth_ret - btc_ret
z_score = ts_zscore(ret_spread, 20)
z_now = z_score[-1]
z_prev = z_score[-2]
corr_now = corr[-1]

# High correlation but divergent returns
high_corr = corr_now > 0.8
long_eth = high_corr and (z_now < -2) and (z_prev >= -2)
short_eth = high_corr and (z_now > 2) and (z_prev <= 2)
exit_cond = (z_now > -0.5) and (z_now < 0.5)

emit(long_eth, entry('BINANCEFUTURES:ETH/USDT:USDT', 'LONG', Weight(0.5), when=NoPosition()))
emit(long_eth, entry('BINANCEFUTURES:BTC/USDT:USDT', 'SHORT', Weight(0.5), when=NoPosition()))
emit(short_eth, entry('BINANCEFUTURES:ETH/USDT:USDT', 'SHORT', Weight(0.5), when=NoPosition()))
emit(short_eth, entry('BINANCEFUTURES:BTC/USDT:USDT', 'LONG', Weight(0.5), when=NoPosition()))
emit(exit_cond, exit_position('BINANCEFUTURES:ETH/USDT:USDT', when=HasPosition()))
emit(exit_cond, exit_position('BINANCEFUTURES:BTC/USDT:USDT', when=HasPosition()))

Cross-Exchange Spread (Perpetual)

Tags: arbitrage, cross_exchange, spread, perpetual Functions: get_data, ts_zscore Description: Trades Binance BTC perpetuals from the normalized price spread between Binance Futures and Hyperliquid. Hyperliquid is a data-only get_data() signal input; the trade leg stays on Binance Futures. Code:

# Binance Futures BTC/USDT (traded leg)
btc_binance = get_data('BINANCEFUTURES:BTC/USDT:USDT')
# Hyperliquid BTC/USDC perpetual (data-only signal input)
btc_hyperliquid = get_data('HYPERLIQUID:BTC/USDC:USDC')

# Cross-venue premium percentage
premium = (btc_hyperliquid.close - btc_binance.close) / btc_binance.close * 100

# Z-score of premium
z_score = ts_zscore(premium, 30)
z_now = z_score[-1]
z_prev = z_score[-2]

# Hyperliquid premium crosses too high: exit the Binance position
high_premium = (z_now > 2) and (z_prev <= 2)
# Hyperliquid trades at a relative discount: buy Binance BTC perpetual
low_premium = (z_now < -2) and (z_prev >= -2)

# Trade legs stay on Binance Futures
emit(low_premium, entry('BINANCEFUTURES:BTC/USDT:USDT', 'LONG', Weight(0.5), when=NoPosition()))
emit(high_premium, exit_position('BINANCEFUTURES:BTC/USDT:USDT', when=HasPosition()))

Futures-Spot Basis Trade

Tags: arbitrage, basis, futures, spot, funding, cash_carry Functions: get_data, ts_zscore Description: Bets on basis contraction when the futures-spot price differential widens. Cash-and-carry: short futures + long spot. Code:

# Spot
btc_spot = get_data('BINANCE:BTC/USDT')
# Perpetual futures
btc_perp = get_data('BINANCEFUTURES:BTC/USDT:USDT')

# Basis (futures premium/discount)
basis = (btc_perp.close - btc_spot.close) / btc_spot.close * 100
z_score = ts_zscore(basis, 30)
z_now = z_score[-1]
z_prev = z_score[-2]

# High premium: short futures, long spot (cash-and-carry)
high_basis = (z_now > 2) and (z_prev <= 2)
# Exit when basis normalizes
exit_cond = (z_now > -0.5) and (z_now < 0.5)

emit(high_basis, entry('BINANCEFUTURES:BTC/USDT:USDT', 'SHORT', Weight(0.5), when=NoPosition()))
emit(high_basis, entry('BINANCE:BTC/USDT', 'LONG', Weight(0.5), when=NoPosition()))
emit(exit_cond, exit_position('BINANCEFUTURES:BTC/USDT:USDT', when=HasPosition()))
emit(exit_cond, exit_position('BINANCE:BTC/USDT', when=HasPosition()))

Triangular Arbitrage Signal

Tags: arbitrage, triangular, cross_rate, fx Functions: get_data Description: Uses BTC/USDT, ETH/USDT, and ETH/BTC prices to generate entry and exit signals for a single ETH/BTC spot leg. It does not execute an atomic three-leg conversion. Execution requirements: Requires an account funded in BTC, ETH/BTC execution prices, and valuation rates for BTC and ETH in the reporting currency. The standard public USDT-funded historical backtest has no BTC initial-asset/reporting-currency contract and does not support this example. The ordinary Paper-start path has not been verified for it. Code:

btc_usdt = get_data('BINANCE:BTC/USDT')
eth_usdt = get_data('BINANCE:ETH/USDT')
eth_btc = get_data('BINANCE:ETH/BTC')

# Implied ETH/BTC from individual pairs
implied_eth_btc = eth_usdt.close / btc_usdt.close

# Deviation from actual ETH/BTC
deviation = (eth_btc.close - implied_eth_btc) / implied_eth_btc * 100
deviation_now = deviation[-1]
deviation_prev = deviation[-2]

# Arbitrage opportunity (spot: LONG entry + exit only)
arb_long = (deviation_now < -0.3) and (deviation_prev >= -0.3)  # ETH/BTC underpriced
arb_exit = deviation_now > 0.3   # ETH/BTC overpriced

emit(arb_long, entry('BINANCE:ETH/BTC', 'LONG', Weight(1.0), when=NoPosition()))
emit(arb_exit, exit_position('BINANCE:ETH/BTC', when=HasPosition()))

Mean Reversion Trio

Tags: pair_trading, multi_asset, mean_reversion, cross_mode Functions: get_data, ts_zscore Description: Relative value mean-reversion strategy across BTC, ETH, and SOL. Code:

btc = get_data('BINANCEFUTURES:BTC/USDT:USDT')
eth = get_data('BINANCEFUTURES:ETH/USDT:USDT')
sol = get_data('BINANCEFUTURES:SOL/USDT:USDT')

# Normalized prices (start from 1)
btc_norm = btc.close / btc.close[0]
eth_norm = eth.close / eth.close[0]
sol_norm = sol.close / sol.close[0]

# ETH vs average of BTC and SOL
eth_relative = eth_norm - (btc_norm + sol_norm) / 2
z_eth = ts_zscore(eth_relative, 30)
z_eth_now = z_eth[-1]
z_eth_prev = z_eth[-2]

# ETH underperforming: long ETH
long_eth = (z_eth_now < -2) and (z_eth_prev >= -2)
# ETH outperforming: short ETH
short_eth = (z_eth_now > 2) and (z_eth_prev <= 2)
exit_cond = (z_eth_now > -0.5) and (z_eth_now < 0.5)

emit(long_eth, entry('BINANCEFUTURES:ETH/USDT:USDT', 'LONG', Weight(0.5), when=NoPosition()))
emit(short_eth, entry('BINANCEFUTURES:ETH/USDT:USDT', 'SHORT', Weight(0.5), when=NoPosition()))
emit(exit_cond, exit_position('BINANCEFUTURES:ETH/USDT:USDT', when=HasPosition()))