b59dcc3629
Set up the directory structure and wrote placeholder logic for: - Order Book Imbalance: trades on L2 bid/ask skew - Iceberg/TWAP detection: follows whale accumulation patterns - Funding rate arbitrage: delta-neutral carry on perp funding - Pairs trading: BTC/ETH spread mean reversion - Avellaneda-Stoikov market making: optimal bid/ask quoting Also added shared risk manager, portfolio tracker, and a plain-language strategy walkthrough in docs/.
96 lines
3.2 KiB
Python
96 lines
3.2 KiB
Python
"""
|
|
Pairs Trading strategy (BTC-PERP / ETH-PERP).
|
|
|
|
Computes the Z-score of the BTC-ETH spread over a rolling window.
|
|
When the spread moves beyond a threshold, trades mean reversion.
|
|
|
|
- Z > +2: BTC expensive -> short BTC, long ETH
|
|
- Z < -2: BTC cheap -> long BTC, short ETH
|
|
"""
|
|
import numpy as np
|
|
from nautilus_trader.trading.strategy import Strategy
|
|
from nautilus_trader.config import StrategyConfig
|
|
|
|
|
|
class PairsTradingConfig(StrategyConfig, frozen=True):
|
|
pair: tuple[str, str]
|
|
z_entry: float = 2.0
|
|
z_exit: float = 0.5
|
|
lookback_hours: int = 24
|
|
trade_size: float = 0.001
|
|
hedge_ratio: float = 0.05
|
|
|
|
|
|
class PairsTrading(Strategy):
|
|
"""
|
|
Statistical arbitrage on BTC/ETH spread.
|
|
|
|
Assumes BTC and ETH are cointegrated — the spread between
|
|
them tends to revert to a mean. Trades the deviations.
|
|
"""
|
|
|
|
def __init__(self, config: PairsTradingConfig) -> None:
|
|
super().__init__(config)
|
|
self.config = config
|
|
self.price_history: dict[str, list[float]] = {
|
|
self.config.pair[0]: [],
|
|
self.config.pair[1]: [],
|
|
}
|
|
self.position_open = False
|
|
|
|
def on_start(self) -> None:
|
|
for inst in self.config.pair:
|
|
self.subscribe_bars(f"{inst}-1-MINUTE-LAST-INTERNAL")
|
|
self.log.info(f"Pairs trading: {self.config.pair[0]} / {self.config.pair[1]}")
|
|
|
|
def on_bar(self, bar) -> None:
|
|
inst_id = str(bar.bar_type.instrument_id)
|
|
if inst_id not in self.price_history:
|
|
return
|
|
|
|
self.price_history[inst_id].append(bar.close.as_double())
|
|
|
|
a_hist = self.price_history[self.config.pair[0]]
|
|
b_hist = self.price_history[self.config.pair[1]]
|
|
if len(a_hist) < 100 or len(b_hist) < 100:
|
|
return
|
|
|
|
maxlen = self.config.lookback_hours * 60
|
|
self.price_history[self.config.pair[0]] = a_hist[-maxlen:]
|
|
self.price_history[self.config.pair[1]] = b_hist[-maxlen:]
|
|
|
|
a = np.array(a_hist[-100:])
|
|
b = np.array(b_hist[-100:])
|
|
spread = a - self.config.hedge_ratio * b
|
|
|
|
std = spread.std()
|
|
z = (spread[-1] - spread.mean()) / std if std > 0 else 0
|
|
|
|
self._signal(z)
|
|
|
|
def _signal(self, z: float) -> None:
|
|
btc_pos = self.portfolio.net_position(self.config.pair[0])
|
|
|
|
if z > self.config.z_entry and btc_pos <= 0:
|
|
self._trade("SELL", "BUY")
|
|
self.position_open = True
|
|
elif z < -self.config.z_entry and btc_pos >= 0:
|
|
self._trade("BUY", "SELL")
|
|
self.position_open = True
|
|
elif abs(z) < self.config.z_exit and self.position_open:
|
|
self.close_all_positions(self.config.pair[0])
|
|
self.close_all_positions(self.config.pair[1])
|
|
self.position_open = False
|
|
|
|
def _trade(self, a_side: str, b_side: str) -> None:
|
|
self.submit_order(self.order_factory.market(
|
|
instrument_id=self.config.pair[0],
|
|
order_side=a_side,
|
|
quantity=self.config.trade_size,
|
|
))
|
|
self.submit_order(self.order_factory.market(
|
|
instrument_id=self.config.pair[1],
|
|
order_side=b_side,
|
|
quantity=self.config.trade_size / self.config.hedge_ratio,
|
|
))
|