Skip to content

Commit 634cb3a

Browse files
committed
Enable Betfair book imbalance acceptance
- Replace the skipped v2 placeholder with a real Betfair run - Use v2-native book deltas and an acceptance-local strategy - Assert the expected order and position lifecycle
1 parent 402a94b commit 634cb3a

2 files changed

Lines changed: 215 additions & 4 deletions

File tree

python/tests/acceptance/test_backtest.py

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,23 @@
4141
from nautilus_trader.model import AccountType
4242
from nautilus_trader.model import AggressorSide
4343
from nautilus_trader.model import BarType
44+
from nautilus_trader.model import BettingInstrument
45+
from nautilus_trader.model import BookAction
46+
from nautilus_trader.model import BookOrder
47+
from nautilus_trader.model import BookType
4448
from nautilus_trader.model import Currency
4549
from nautilus_trader.model import ExecAlgorithmId
50+
from nautilus_trader.model import InstrumentId
4651
from nautilus_trader.model import Money
4752
from nautilus_trader.model import OmsType
53+
from nautilus_trader.model import OrderBookDelta
54+
from nautilus_trader.model import OrderBookDeltas
4855
from nautilus_trader.model import OrderSide
4956
from nautilus_trader.model import OrderStatus
5057
from nautilus_trader.model import Price
5158
from nautilus_trader.model import Quantity
5259
from nautilus_trader.model import QuoteTick
60+
from nautilus_trader.model import Symbol
5361
from nautilus_trader.model import TradeId
5462
from nautilus_trader.model import TradeTick
5563
from nautilus_trader.model import Venue
@@ -87,6 +95,9 @@
8795
EMA_CROSS_TRAILING_STOP_STRATEGY = "strategies.acceptance:EMACrossTrailingStop"
8896
EMA_CROSS_TRAILING_STOP_CONFIG = "strategies.acceptance:EMACrossTrailingStopConfig"
8997

98+
ORDER_BOOK_IMBALANCE_STRATEGY = "strategies.acceptance:OrderBookImbalance"
99+
ORDER_BOOK_IMBALANCE_CONFIG = "strategies.acceptance:OrderBookImbalanceConfig"
100+
90101
EMA_CROSS_TRAILING_STOP_TAG = "ema-cross-trailing-stop"
91102

92103

@@ -670,12 +681,119 @@ def test_run_ema_cross_with_tick_bar_spec(self):
670681
assert result.total_orders > 0
671682

672683

673-
@pytest.mark.skip(
674-
reason="v2 missing: Betfair adapter / data provider + OrderBookImbalance strategy",
675-
)
676684
class TestBacktestAcceptanceTestsOrderBookImbalance:
685+
def setup_method(self):
686+
self.engine = _engine()
687+
self.venue = Venue("BETFAIR")
688+
self.gbp = Currency.from_str("GBP")
689+
self.instrument = _betfair_betting_instrument(selection_id=19248890)
690+
691+
self.engine.add_venue(
692+
venue=self.venue,
693+
oms_type=OmsType.NETTING,
694+
account_type=AccountType.BETTING,
695+
base_currency=self.gbp,
696+
starting_balances=[Money(100_000.0, self.gbp)],
697+
book_type=BookType.L2_MBP,
698+
)
699+
self.engine.add_instrument(self.instrument)
700+
self.engine.add_data(_betfair_order_book_deltas(self.instrument))
701+
702+
def teardown_method(self):
703+
self.engine.dispose()
704+
677705
def test_run_order_book_imbalance(self):
678-
pass
706+
self.engine.add_strategy_from_config(
707+
ImportableStrategyConfig(
708+
strategy_path=ORDER_BOOK_IMBALANCE_STRATEGY,
709+
config_path=ORDER_BOOK_IMBALANCE_CONFIG,
710+
config={
711+
"instrument_id": str(self.instrument.id),
712+
"trade_size": "5.00",
713+
},
714+
),
715+
)
716+
717+
self.engine.run()
718+
result = self.engine.get_result()
719+
720+
assert result.iterations == 1
721+
assert result.total_orders == 2
722+
assert result.total_positions == 1
723+
assert result.summary["venues.total"] == "1"
724+
assert result.summary["orders.closed"] == "2"
725+
assert result.summary["positions.closed"] == "1"
726+
727+
728+
def _betfair_betting_instrument(selection_id: int) -> BettingInstrument:
729+
raw_symbol = Symbol(f"1-166811431-{selection_id}-None")
730+
gbp = Currency.from_str("GBP")
731+
return BettingInstrument(
732+
instrument_id=InstrumentId(raw_symbol, Venue("BETFAIR")),
733+
raw_symbol=raw_symbol,
734+
event_type_id=6423,
735+
event_type_name="American Football",
736+
competition_id=12282733,
737+
competition_name="NFL",
738+
event_id=29678534,
739+
event_name="NFL",
740+
event_country_code="GB",
741+
event_open_date=1644276600000000000,
742+
betting_type="ODDS",
743+
market_id="1-166811431",
744+
market_name="AFC Conference Winner",
745+
market_type="SPECIAL",
746+
market_start_time=1644276600000000000,
747+
selection_id=selection_id,
748+
selection_name="Kansas City Chiefs",
749+
selection_handicap=-9999999.0,
750+
currency=gbp,
751+
price_precision=2,
752+
size_precision=2,
753+
price_increment=Price.from_str("0.01"),
754+
size_increment=Quantity.from_str("0.01"),
755+
ts_event=0,
756+
ts_init=0,
757+
)
758+
759+
760+
def _betfair_order_book_deltas(instrument: BettingInstrument) -> list[OrderBookDeltas]:
761+
ts = 1_600_000_000_000_000_000
762+
return [
763+
OrderBookDeltas(
764+
instrument_id=instrument.id,
765+
deltas=[
766+
OrderBookDelta(
767+
instrument.id,
768+
BookAction.ADD,
769+
BookOrder(
770+
OrderSide.BUY,
771+
Price.from_decimal_dp(Decimal("1.99"), instrument.price_precision),
772+
Quantity.from_decimal_dp(Decimal("250.00"), instrument.size_precision),
773+
1,
774+
),
775+
0,
776+
1,
777+
ts,
778+
ts,
779+
),
780+
OrderBookDelta(
781+
instrument.id,
782+
BookAction.ADD,
783+
BookOrder(
784+
OrderSide.SELL,
785+
Price.from_decimal_dp(Decimal("2.00"), instrument.price_precision),
786+
Quantity.from_decimal_dp(Decimal("10.00"), instrument.size_precision),
787+
2,
788+
),
789+
0,
790+
2,
791+
ts,
792+
ts,
793+
),
794+
],
795+
),
796+
]
679797

680798

681799
@pytest.mark.skip(reason="v2 missing: Betfair adapter + MarketMaker example strategy")

python/tests/strategies/acceptance.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@
3131
from nautilus_trader.indicators import MovingAverageConvergenceDivergence
3232
from nautilus_trader.model import Bar
3333
from nautilus_trader.model import BarType
34+
from nautilus_trader.model import BookType
3435
from nautilus_trader.model import ClientOrderId
3536
from nautilus_trader.model import ContingencyType
3637
from nautilus_trader.model import InstrumentId
3738
from nautilus_trader.model import LimitOrder
3839
from nautilus_trader.model import MarketOrder
40+
from nautilus_trader.model import OrderBookDeltas
3941
from nautilus_trader.model import OrderFilled
4042
from nautilus_trader.model import OrderSide
4143
from nautilus_trader.model import Price
@@ -236,6 +238,97 @@ def on_stop(self):
236238
pass
237239

238240

241+
class OrderBookImbalanceConfig(StrategyConfig):
242+
_CUSTOM_FIELDS = ("instrument_id", "trade_size")
243+
244+
def __new__(cls, *args, **kwargs):
245+
for key in cls._CUSTOM_FIELDS:
246+
kwargs.pop(key, None)
247+
return super().__new__(cls, *args, **kwargs)
248+
249+
def __init__(
250+
self,
251+
instrument_id: str,
252+
trade_size: str,
253+
**kwargs,
254+
):
255+
super().__init__()
256+
self.instrument_id = instrument_id
257+
self.trade_size = trade_size
258+
259+
260+
class OrderBookImbalance(Strategy):
261+
_MIN_IMBALANCE_SIZE = Decimal(100)
262+
_MAX_IMBALANCE_RATIO = Decimal("0.20")
263+
264+
def __init__(self, config: OrderBookImbalanceConfig):
265+
super().__init__(config)
266+
self._instrument_id = InstrumentId.from_str(config.instrument_id)
267+
self._trade_size = Quantity.from_str(config.trade_size)
268+
self._has_submitted = False
269+
270+
def on_start(self):
271+
self.subscribe_book_deltas(self._instrument_id, BookType.L2_MBP)
272+
273+
def on_book_deltas(self, deltas: OrderBookDeltas):
274+
if self._has_submitted:
275+
return
276+
277+
bid_size = Decimal(0)
278+
ask_size = Decimal(0)
279+
bid_price = None
280+
ask_price = None
281+
282+
for delta in deltas.deltas:
283+
size = delta.order.size.as_decimal()
284+
if delta.order.side == OrderSide.BUY:
285+
bid_size += size
286+
bid_price = delta.order.price
287+
elif delta.order.side == OrderSide.SELL:
288+
ask_size += size
289+
ask_price = delta.order.price
290+
291+
if bid_size <= 0 or ask_size <= 0:
292+
return
293+
294+
larger = max(bid_size, ask_size)
295+
smaller = min(bid_size, ask_size)
296+
297+
if larger <= self._MIN_IMBALANCE_SIZE:
298+
return
299+
300+
if smaller / larger >= self._MAX_IMBALANCE_RATIO:
301+
return
302+
303+
if bid_size > ask_size and ask_price is not None:
304+
side = OrderSide.BUY
305+
price = ask_price
306+
elif bid_price is not None:
307+
side = OrderSide.SELL
308+
price = bid_price
309+
else:
310+
return
311+
312+
self._has_submitted = True
313+
self.submit_order(
314+
self.order_factory.limit(
315+
instrument_id=self._instrument_id,
316+
order_side=side,
317+
quantity=self._trade_size,
318+
price=price,
319+
time_in_force=TimeInForce.FOK,
320+
post_only=False,
321+
),
322+
)
323+
324+
def on_stop(self):
325+
self.cancel_all_orders(self._instrument_id)
326+
self.close_all_positions(self._instrument_id)
327+
328+
def on_reset(self):
329+
self._has_submitted = False
330+
331+
239332
class MultiInstrumentTickScheduledConfig(StrategyConfig):
240333
"""
241334
Submit market orders from an instrument keyed action schedule.

0 commit comments

Comments
 (0)