Initial project scaffold: five quant strategies for Hyperliquid Testnet
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/.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
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,
|
||||
))
|
||||
Reference in New Issue
Block a user