Skip to content

Commit

Permalink
Betfair cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
limx0 authored Dec 24, 2023
1 parent 427eeb2 commit 3b2878b
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 30 deletions.
1 change: 1 addition & 0 deletions nautilus_trader/adapters/betfair/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ async def _request(self, method: HttpMethod, request: Request) -> HttpResponse:
body = request.body()
if isinstance(body, str):
body = body.encode()
self._log.debug(f"[REQ] {method} {url} {body.decode()} ")
response: HttpResponse = await self._client.request(
method,
url,
Expand Down
35 changes: 24 additions & 11 deletions nautilus_trader/adapters/betfair/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,37 @@
# N2B = {NAUTILUS: BETFAIR}
# B2N = {BETFAIR: NAUTILUS}

N2B_SIDE = {
OrderSide.BUY: Side.BACK,
OrderSide.SELL: Side.LAY,
}

class OrderSideParser:
BACKS = (Side.BACK, "B")
LAYS = (Side.LAY, "L")

@classmethod
def to_nautilus(cls, side: Side | str) -> OrderSide:
if side in cls.BACKS:
return OrderSide.SELL
elif side in cls.LAYS:
return OrderSide.BUY
else:
raise ValueError(f"Unknown side: {side}")

@staticmethod
def to_betfair(order_side: OrderSide) -> Side:
if order_side == OrderSide.BUY:
return Side.LAY
elif order_side == OrderSide.SELL:
return Side.BACK
else:
raise ValueError(f"Unknown order_side: {order_side}")


N2B_TIME_IN_FORCE = {
TimeInForce.FOK: BetfairTimeInForce.FILL_OR_KILL,
}

N2B_PERSISTENCE = {
TimeInForce.GTC: PersistenceType.PERSIST,
TimeInForce.DAY: PersistenceType.MARKET_ON_CLOSE,
TimeInForce.DAY: PersistenceType.LAPSE,
}

B2N_MARKET_SIDE = {
Expand All @@ -55,12 +74,6 @@
"spl": OrderSide.BUY, # Starting Price LAY
}

B2N_ORDER_SIDE = {
Side.BACK: OrderSide.BUY,
Side.LAY: OrderSide.SELL,
"B": OrderSide.BUY,
"L": OrderSide.BUY,
}

B2N_TIME_IN_FORCE = {
PersistenceType.LAPSE: TimeInForce.DAY,
Expand Down
2 changes: 1 addition & 1 deletion nautilus_trader/adapters/betfair/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ def on_market_update(self, raw: bytes):
"""
Handle an update from the data stream socket.
"""
self._log.debug(f"raw_data: {raw.decode()}")
self._log.debug(f"[RECV]: {raw.decode()}")
update = stream_decode(raw)
if isinstance(update, MCM):
self._on_market_update(mcm=update)
Expand Down
8 changes: 4 additions & 4 deletions nautilus_trader/adapters/betfair/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

from nautilus_trader.accounting.factory import AccountFactory
from nautilus_trader.adapters.betfair.client import BetfairHttpClient
from nautilus_trader.adapters.betfair.common import B2N_ORDER_SIDE
from nautilus_trader.adapters.betfair.common import OrderSideParser
from nautilus_trader.adapters.betfair.constants import BETFAIR_VENUE
from nautilus_trader.adapters.betfair.orderbook import betfair_float_to_price
from nautilus_trader.adapters.betfair.orderbook import betfair_float_to_quantity
Expand Down Expand Up @@ -545,7 +545,7 @@ def handle_order_stream_update(self, raw: bytes) -> None:
"""
Handle an update from the order stream socket.
"""
self._log.debug(f"raw_exec: {raw.decode()}")
self._log.debug(f"[RECV]: {raw.decode()}")
update = stream_decode(raw)

if isinstance(update, OCM):
Expand Down Expand Up @@ -675,7 +675,7 @@ def _handle_stream_executable_order_update(self, unmatched_order: UnmatchedOrder
venue_order_id=venue_order_id,
venue_position_id=None, # Can be None
trade_id=trade_id,
order_side=B2N_ORDER_SIDE[unmatched_order.side],
order_side=OrderSideParser.to_nautilus(unmatched_order.side),
order_type=OrderType.LIMIT,
last_qty=betfair_float_to_quantity(fill_qty),
last_px=betfair_float_to_price(fill_price),
Expand Down Expand Up @@ -745,7 +745,7 @@ def _handle_stream_execution_complete_order_update(
venue_order_id=venue_order_id,
venue_position_id=None, # Can be None
trade_id=trade_id,
order_side=B2N_ORDER_SIDE[unmatched_order.side],
order_side=OrderSideParser.to_nautilus(unmatched_order.side),
order_type=OrderType.LIMIT,
last_qty=betfair_float_to_quantity(fill_qty),
last_px=betfair_float_to_price(fill_price),
Expand Down
23 changes: 14 additions & 9 deletions nautilus_trader/adapters/betfair/parsing/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,14 @@
from betfair_parser.spec.common import OrderStatus as BetfairOrderStatus
from betfair_parser.spec.common import OrderType

from nautilus_trader.adapters.betfair.common import B2N_ORDER_SIDE
from nautilus_trader.adapters.betfair.common import B2N_ORDER_TYPE
from nautilus_trader.adapters.betfair.common import B2N_TIME_IN_FORCE
from nautilus_trader.adapters.betfair.common import BETFAIR_FLOAT_TO_PRICE
from nautilus_trader.adapters.betfair.common import MAX_BET_PRICE
from nautilus_trader.adapters.betfair.common import MIN_BET_PRICE
from nautilus_trader.adapters.betfair.common import N2B_PERSISTENCE
from nautilus_trader.adapters.betfair.common import N2B_SIDE
from nautilus_trader.adapters.betfair.common import N2B_TIME_IN_FORCE
from nautilus_trader.adapters.betfair.common import OrderSideParser
from nautilus_trader.adapters.betfair.constants import BETFAIR_QUANTITY_PRECISION
from nautilus_trader.adapters.betfair.constants import BETFAIR_VENUE
from nautilus_trader.core.datetime import dt_to_unix_nanos
Expand Down Expand Up @@ -99,7 +98,7 @@ def nautilus_limit_to_place_instructions(
handicap=instrument.selection_handicap
if instrument.selection_handicap != null_handicap()
else None,
side=N2B_SIDE[command.order.side],
side=OrderSideParser.to_betfair(command.order.side),
limit_order=LimitOrder(
price=command.order.price.as_double(),
size=command.order.quantity.as_double(),
Expand Down Expand Up @@ -128,7 +127,7 @@ def nautilus_limit_on_close_to_place_instructions(
handicap=instrument.selection_handicap
if instrument.selection_handicap != null_handicap()
else None,
side=N2B_SIDE[command.order.side],
side=OrderSideParser.to_betfair(command.order.side),
limit_on_close_order=LimitOnCloseOrder(
price=command.order.price.as_double(),
liability=command.order.quantity.as_double(),
Expand All @@ -153,7 +152,7 @@ def nautilus_market_to_place_instructions(
handicap=instrument.selection_handicap
if instrument.selection_handicap != null_handicap()
else None,
side=N2B_SIDE[command.order.side],
side=OrderSideParser.to_betfair(command.order.side),
limit_order=LimitOrder(
price=price.as_double(),
size=command.order.quantity.as_double(),
Expand Down Expand Up @@ -182,7 +181,7 @@ def nautilus_market_on_close_to_place_instructions(
handicap=instrument.selection_handicap
if instrument.selection_handicap != null_handicap()
else None,
side=N2B_SIDE[command.order.side],
side=OrderSideParser.to_betfair(command.order.side),
market_on_close_order=MarketOnCloseOrder(
liability=command.order.quantity.as_double(),
),
Expand All @@ -199,15 +198,21 @@ def nautilus_order_to_place_instructions(
instrument: BettingInstrument,
) -> PlaceInstruction:
if isinstance(command.order, NautilusLimitOrder):
if command.order.time_in_force == NautilusTimeInForce.AT_THE_OPEN:
if command.order.time_in_force in (
NautilusTimeInForce.AT_THE_OPEN,
NautilusTimeInForce.AT_THE_CLOSE,
):
return nautilus_limit_on_close_to_place_instructions(
command=command,
instrument=instrument,
)
else:
return nautilus_limit_to_place_instructions(command=command, instrument=instrument)
elif isinstance(command.order, NautilusMarketOrder):
if command.order.time_in_force == NautilusTimeInForce.AT_THE_OPEN:
if command.order.time_in_force in (
NautilusTimeInForce.AT_THE_OPEN,
NautilusTimeInForce.AT_THE_CLOSE,
):
return nautilus_market_on_close_to_place_instructions(
command=command,
instrument=instrument,
Expand Down Expand Up @@ -359,7 +364,7 @@ def bet_to_order_status_report(
instrument_id=instrument_id,
venue_order_id=venue_order_id,
client_order_id=client_order_id,
order_side=B2N_ORDER_SIDE[order.side],
order_side=OrderSideParser.to_nautilus(order.side),
order_type=B2N_ORDER_TYPE[order.order_type],
contingency_type=ContingencyType.NO_CONTINGENCY,
time_in_force=B2N_TIME_IN_FORCE[order.persistence_type],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ async def test_place_orders(betfair_client):
instrument = betting_instrument()
limit_order = TestExecStubs.limit_order(
instrument_id=instrument.id,
order_side=OrderSide.BUY,
order_side=OrderSide.SELL,
price=betfair_float_to_price(2.0),
quantity=betfair_float_to_quantity(10),
)
Expand Down Expand Up @@ -235,7 +235,7 @@ async def test_place_orders_handicap(betfair_client):
order_type=OrderType.LIMIT,
selection_id=5304641,
handicap=-5.5,
side=Side.BACK,
side=Side.LAY,
limit_order=LimitOrder(
price=2.0,
size=10.0,
Expand Down Expand Up @@ -290,7 +290,7 @@ async def test_place_orders_market_on_close(betfair_client):
order_type=OrderType.MARKET_ON_CLOSE,
selection_id=50214,
handicap=None,
side=Side.BACK,
side=Side.LAY,
limit_order=None,
limit_on_close_order=None,
market_on_close_order=MarketOnCloseOrder(liability=10.0),
Expand Down
66 changes: 64 additions & 2 deletions tests/integration_tests/adapters/betfair/test_betfair_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from betfair_parser.spec.betting.type_definitions import PlaceInstruction
from betfair_parser.spec.betting.type_definitions import PriceSize
from betfair_parser.spec.betting.type_definitions import ReplaceInstruction
from betfair_parser.spec.betting.type_definitions import TimeInForce as BP_TimeInForce
from betfair_parser.spec.common import OrderStatus
from betfair_parser.spec.common import OrderType
from betfair_parser.spec.common import decode
Expand All @@ -47,6 +48,7 @@

# fmt: off
from nautilus_trader.adapters.betfair.common import BETFAIR_TICK_SCHEME
from nautilus_trader.adapters.betfair.common import OrderSideParser
from nautilus_trader.adapters.betfair.data_types import BetfairStartingPrice
from nautilus_trader.adapters.betfair.data_types import BetfairTicker
from nautilus_trader.adapters.betfair.data_types import BSPOrderBookDelta
Expand All @@ -61,6 +63,7 @@
from nautilus_trader.adapters.betfair.parsing.requests import nautilus_limit_to_place_instructions
from nautilus_trader.adapters.betfair.parsing.requests import nautilus_market_on_close_to_place_instructions
from nautilus_trader.adapters.betfair.parsing.requests import nautilus_market_to_place_instructions
from nautilus_trader.adapters.betfair.parsing.requests import nautilus_order_to_place_instructions
from nautilus_trader.adapters.betfair.parsing.requests import order_cancel_to_cancel_order_params
from nautilus_trader.adapters.betfair.parsing.requests import order_submit_to_place_order_params
from nautilus_trader.adapters.betfair.parsing.requests import order_update_to_replace_order_params
Expand Down Expand Up @@ -336,11 +339,25 @@ def setup(self):
self.uuid = UUID4()
self.parser = BetfairParser(currency="GBP")

def test_order_side_parser_to_betfair(self):
assert OrderSideParser.to_betfair(OrderSide.BUY) == Side.LAY
assert OrderSideParser.to_betfair(OrderSide.SELL) == Side.BACK

def test_order_side_parser_round_trip(self):
assert (
OrderSideParser.to_nautilus(OrderSideParser.to_betfair(OrderSide.BUY)) == OrderSide.BUY
)
assert (
OrderSideParser.to_nautilus(OrderSideParser.to_betfair(OrderSide.SELL))
== OrderSide.SELL
)

def test_order_submit_to_betfair(self):
command = TestCommandStubs.submit_order_command(
order=TestExecStubs.limit_order(
price=betfair_float_to_price(2.5),
quantity=betfair_float_to_quantity(10),
order_side=OrderSide.SELL,
),
)
result = order_submit_to_place_order_params(command=command, instrument=self.instrument)
Expand Down Expand Up @@ -481,6 +498,7 @@ def test_make_order_limit(self):
order = TestExecStubs.limit_order(
price=betfair_float_to_price(3.05),
quantity=betfair_float_to_quantity(10),
order_side=OrderSide.SELL,
)
command = TestCommandStubs.submit_order_command(order)

Expand Down Expand Up @@ -515,6 +533,7 @@ def test_make_order_limit_on_close(self):
quantity=betfair_float_to_quantity(10),
instrument_id=TestIdStubs.betting_instrument_id(),
time_in_force=TimeInForce.AT_THE_OPEN,
order_side=OrderSide.SELL,
)
command = TestCommandStubs.submit_order_command(order)
result = nautilus_limit_on_close_to_place_instructions(command, instrument=self.instrument)
Expand All @@ -539,7 +558,7 @@ def test_make_order_market_buy(self):
order_type=OrderType.LIMIT,
selection_id=50214,
handicap=None,
side=Side.BACK,
side=Side.LAY,
limit_order=LimitOrder(
size=100.0,
price=1.01,
Expand All @@ -564,7 +583,7 @@ def test_make_order_market_sell(self):
order_type=OrderType.LIMIT,
selection_id=50214,
handicap=None,
side=Side.LAY,
side=Side.BACK,
limit_order=LimitOrder(
size=100.0,
price=1000,
Expand Down Expand Up @@ -681,6 +700,49 @@ def test_mcm_bsp_example2(self):
]
assert len(single_instrument_bsp_updates) == 1

@pytest.mark.parametrize(
("time_in_force", "expected_time_in_force", "expected_persistence_type"),
[
(TimeInForce.GTC, None, PersistenceType.PERSIST),
(TimeInForce.DAY, None, PersistenceType.LAPSE),
(TimeInForce.FOK, BP_TimeInForce.FILL_OR_KILL, PersistenceType.LAPSE),
],
)
def test_persistence_types(
self,
time_in_force,
expected_time_in_force,
expected_persistence_type,
):
# Arrange
order = TestExecStubs.limit_order(time_in_force=time_in_force)
command = TestCommandStubs.submit_order_command(order)

# Act
place_instruction = nautilus_order_to_place_instructions(command, self.instrument)
place_time_in_force = place_instruction.limit_order.time_in_force
place_persistence_type = place_instruction.limit_order.persistence_type

# Assert
assert place_time_in_force == expected_time_in_force
assert place_persistence_type == expected_persistence_type

def test_persistence_encoding(self):
# Arrange
order = TestExecStubs.limit_order(time_in_force=TimeInForce.DAY)
command = TestCommandStubs.submit_order_command(order)

# Act
place_instruction = nautilus_order_to_place_instructions(command, self.instrument)
result = msgspec.json.decode(msgspec.json.encode(place_instruction))["limitOrder"]

expected = {
"size": 100.0,
"price": 55.0,
"persistenceType": "LAPSE",
}
assert result == expected


def request_id() -> int:
"""
Expand Down

0 comments on commit 3b2878b

Please sign in to comment.