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:
ramseshk
2026-08-03 11:12:20 +00:00
parent 096b5a982f
commit b59dcc3629
20 changed files with 816 additions and 2 deletions
+1
View File
@@ -0,0 +1 @@
# Package init files (make these importable)
+87
View File
@@ -0,0 +1,87 @@
"""
Avellaneda-Stoikov Market Making strategy.
A mathematical model for optimal market making based on
stochastic optimal control. Computes optimal bid/ask quotes
considering current inventory, risk aversion, volatility,
and time horizon.
Key formulas:
Reservation price: r = s - q * gamma * sigma^2 * tau
Optimal spread: delta = gamma * sigma^2 * tau + (2/gamma) * ln(1 + gamma/k)
where:
s = mid price, q = inventory, gamma = risk aversion
sigma = volatility, tau = remaining time, k = order intensity
"""
import math
from nautilus_trader.trading.strategy import Strategy
from nautilus_trader.config import StrategyConfig
from datetime import datetime, timezone
class AvellanedaStoikovConfig(StrategyConfig, frozen=True):
instrument_id: str
gamma: float = 0.1
sigma: float = 0.02
T: float = 1.0
k: float = 1.5
min_spread: float = 0.0001
max_inventory: float = 0.01
class AvellanedaStoikov(Strategy):
"""
A-S optimal market making.
Instead of predicting direction, this strategy provides
liquidity by continuously quoting bid/ask prices at an
optimal distance from the mid price. The spread widens
as inventory builds up (to discourage further accumulation)
and tightens as the time horizon approaches.
"""
def __init__(self, config: AvellanedaStoikovConfig) -> None:
super().__init__(config)
self.config = config
self.start_time: datetime | None = None
def on_start(self) -> None:
self.start_time = self.clock.utc_now()
self.subscribe_quote_ticks(self.config.instrument_id)
self.log.info(
f"A-S MM on {self.config.instrument_id} "
f"(gamma={self.config.gamma})"
)
def on_quote_tick(self, tick) -> None:
self.cancel_all_orders(self.config.instrument_id)
elapsed = (self.clock.utc_now() - self.start_time).total_seconds() / 3600
tau = max(self.config.T - elapsed, 0.01)
q = float(self.portfolio.net_position(self.config.instrument_id))
if abs(q) >= self.config.max_inventory:
return
g = self.config.gamma
s = self.config.sigma
k = self.config.k
mid = (tick.bid + tick.ask) / 2
reservation = mid - q * g * s**2 * tau
spread = g * s**2 * tau + (2 / g) * math.log(1 + g / k)
spread = max(spread, self.config.min_spread)
self.submit_order(self.order_factory.limit(
instrument_id=self.config.instrument_id,
order_side="BUY",
quantity=self.config.max_inventory / 10,
price=reservation - spread / 2,
))
self.submit_order(self.order_factory.limit(
instrument_id=self.config.instrument_id,
order_side="SELL",
quantity=self.config.max_inventory / 10,
price=reservation + spread / 2,
))
+77
View File
@@ -0,0 +1,77 @@
"""
Funding Rate Arbitrage strategy.
Hyperliquid pays funding every 8 hours. When the rate is positive,
longs pay shorts. This strategy:
1. Goes LONG spot (no funding payments)
2. Goes SHORT perp (collects funding)
3. Maintains delta neutrality
The profit comes from funding, not price direction.
"""
from nautilus_trader.trading.strategy import Strategy
from nautilus_trader.config import StrategyConfig
class FundingRateArbConfig(StrategyConfig, frozen=True):
spot_instrument: str
perp_instrument: str
min_funding_rate: float = 0.0001
rebalance_threshold: float = 0.05
position_size: float = 0.01
class FundingRateArb(Strategy):
"""
Delta-neutral funding rate carry trade.
Key idea: funding rate IS the edge. Stay neutral, collect
the payments.
"""
def __init__(self, config: FundingRateArbConfig) -> None:
super().__init__(config)
self.config = config
self.position_open = False
def on_start(self) -> None:
bar_type = f"{self.config.perp_instrument}-1-MINUTE-LAST-INTERNAL"
self.subscribe_bars(bar_type)
self.log.info(
f"Funding arb: {self.config.spot_instrument} / {self.config.perp_instrument}"
)
def on_bar(self, bar) -> None:
funding_rate = self._get_funding_rate()
if funding_rate is None:
return
spot_pos = self.portfolio.net_position(self.config.spot_instrument)
if funding_rate > self.config.min_funding_rate and spot_pos == 0:
self._open()
self.position_open = True
elif funding_rate < self.config.min_funding_rate / 2 and self.position_open:
self._close()
self.position_open = False
def _get_funding_rate(self) -> float | None:
# TODO: fetch from Hyperliquid API
return 0.0001
def _open(self) -> None:
self.submit_order(self.order_factory.market(
instrument_id=self.config.spot_instrument,
order_side="BUY",
quantity=self.config.position_size,
))
self.submit_order(self.order_factory.market(
instrument_id=self.config.perp_instrument,
order_side="SELL",
quantity=self.config.position_size,
))
def _close(self) -> None:
self.close_all_positions(self.config.spot_instrument)
self.close_all_positions(self.config.perp_instrument)
+66
View File
@@ -0,0 +1,66 @@
"""
Iceberg / TWAP detection strategy.
Large traders often split big orders into small slices to avoid
slippage. This strategy detects those patterns by watching for
recurring same-sized trades above average volume, then enters
in the same direction.
"""
from collections import deque
from nautilus_trader.trading.strategy import Strategy
from nautilus_trader.config import StrategyConfig
class IcebergDetectorConfig(StrategyConfig, frozen=True):
instrument_id: str
lookback_seconds: int = 300
volume_spike_mult: float = 3.0
min_slices: int = 4
trade_size: float = 0.001
class IcebergDetector(Strategy):
"""
Detects iceberg/TWAP execution patterns.
Logic:
1. Track trade sizes in a rolling window
2. When a trade is much larger than average, flag it
3. If same size repeats N times -> confirmed iceberg
4. Trade in the same direction
"""
def __init__(self, config: IcebergDetectorConfig) -> None:
super().__init__(config)
self.config = config
self.recent_sizes: deque[float] = deque(maxlen=100)
self.slice_count = 0
self.last_flagged_size: float | None = None
def on_start(self) -> None:
self.subscribe_trade_ticks(self.config.instrument_id)
self.log.info(f"Iceberg detector started on {self.config.instrument_id}")
def on_trade_tick(self, tick) -> None:
self.recent_sizes.append(tick.size)
avg = sum(self.recent_sizes) / len(self.recent_sizes) if self.recent_sizes else 0
if tick.size > avg * self.config.volume_spike_mult:
if tick.size == self.last_flagged_size:
self.slice_count += 1
else:
self.slice_count = 1
self.last_flagged_size = tick.size
else:
self.slice_count = 0
if self.slice_count >= self.config.min_slices:
self.log.info(
f"Iceberg: {self.slice_count} slices of size {self.last_flagged_size}"
)
self.submit_order(self.order_factory.market(
instrument_id=self.config.instrument_id,
order_side="BUY" if tick.is_buyer_maker else "SELL",
quantity=self.config.trade_size,
))
self.slice_count = 0
+76
View File
@@ -0,0 +1,76 @@
"""
Order Book Imbalance strategy.
Enters positions when bid/ask volume at the top of the order book
shows a significant directional skew. The idea: when one side of
the book is much heavier, price tends to move toward the thinner
side as the heavy side absorbs market orders.
"""
from nautilus_trader.trading.strategy import Strategy
from nautilus_trader.config import StrategyConfig
class OrderBookImbalanceConfig(StrategyConfig, frozen=True):
instrument_id: str
depth: int = 10
imbalance_threshold: float = 0.6
trade_size: float = 0.001
max_position: float = 0.003
cooldown_bars: int = 5
class OrderBookImbalance(Strategy):
"""
Trades on L2 order book imbalance.
- imbalance > threshold -> bid side heavy -> buy
- imbalance < 1-threshold -> ask side heavy -> sell
"""
def __init__(self, config: OrderBookImbalanceConfig) -> None:
super().__init__(config)
self.config = config
self.bars_since_last_trade = 0
def on_start(self) -> None:
self.subscribe_order_book_deltas(
self.config.instrument_id,
depth=self.config.depth,
)
self.log.info(
f"OFI started on {self.config.instrument_id} "
f"(depth={self.config.depth})"
)
def on_order_book_deltas(self, deltas) -> None:
self.bars_since_last_trade += 1
if self.bars_since_last_trade < self.config.cooldown_bars:
return
book = self.cache.order_book(self.config.instrument_id)
if not book or len(book.bids) == 0 or len(book.asks) == 0:
return
depth = min(self.config.depth, len(book.bids), len(book.asks))
bid_vol = sum(book.bids[i].size for i in range(depth))
ask_vol = sum(book.asks[i].size for i in range(depth))
total = bid_vol + ask_vol
if total == 0:
return
imbalance = bid_vol / total
pos = self.portfolio.net_position(self.config.instrument_id)
if imbalance > self.config.imbalance_threshold and pos <= 0:
self._enter("BUY")
self.bars_since_last_trade = 0
elif imbalance < (1 - self.config.imbalance_threshold) and pos >= 0:
self._enter("SELL")
self.bars_since_last_trade = 0
def _enter(self, side: str) -> None:
self.submit_order(self.order_factory.market(
instrument_id=self.config.instrument_id,
order_side=side,
quantity=self.config.trade_size,
))
+95
View File
@@ -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,
))