Skip to content

Commit

Permalink
Fix TWAP execution algo scheduled size handling
Browse files Browse the repository at this point in the history
  • Loading branch information
cjdsellers committed Jan 11, 2024
1 parent 1f2e632 commit 599b9e2
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 22 deletions.
2 changes: 1 addition & 1 deletion RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ Released on TBD (UTC).
- Implemented global atomic clock in Rust (improves performance and ensures properly monotonic timestamps in real-time)
- Improved Interactive Brokers adapter raising docker `RuntimeError` only when needed (not when using TWS), thanks @rsmb7z
- Upgraded core HTTP client to `hyper` 1.1.0, thanks @ayush-sb
- Optimized core MPSC channels with sync senders
- Optimized Arrow encoding (resulting in ~100x faster writes for the Parquet data catalog)

### Breaking Changes
Expand Down Expand Up @@ -41,6 +40,7 @@ Released on TBD (UTC).
- Fixed handling of configuration objects to work with `StreamingFeatherWriter`
- Fixed `BinanceSpotInstrumentProvider` fee loading key error for partial instruments load, thanks for reporting @doublier1
- Fixed Binance API key configuration parsing for testnet (was falling through to non-testnet env vars)
- Fixed TWAP execution algorithm scheduled size handling when first order should be for the entire size, thanks for reporting @pcgm-team
- Added `BinanceErrorCode.SERVER_BUSY` (-1008). Also added to the retry error codes.
- Added `BinanceOrderStatus.EXPIRED_IN_MATCH` which is when an order was canceled by the exchange due self-trade prevention (STP), thanks for reporting @doublier1

Expand Down
31 changes: 10 additions & 21 deletions nautilus_trader/examples/algorithms/twap.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def on_load(self, state: dict[str, bytes]) -> None:
def round_decimal_down(self, amount: Decimal, precision: int) -> Decimal:
return amount.quantize(Decimal(f"1e-{precision}"), rounding=ROUND_DOWN)

def on_order(self, order: Order) -> None: # noqa (too complex)
def on_order(self, order: Order) -> None:
"""
Actions to be performed when running and receives an order.
Expand Down Expand Up @@ -204,34 +204,23 @@ def on_order(self, order: Order) -> None: # noqa (too complex)
qty_per_interval = instrument.make_qty(qty_quotient)
qty_remainder = order.quantity.as_decimal() - (floored_quotient * num_intervals)

if qty_per_interval < instrument.size_increment:
self.log.error(
f"Cannot execute order: "
f"{qty_per_interval=} less than {instrument.id} {instrument.size_increment}.",
)
return

if instrument.min_quantity and qty_per_interval < instrument.min_quantity:
self.log.error(
f"Cannot execute order: "
f"{qty_per_interval=} less than {instrument.id} {instrument.min_quantity=}.",
)
return
if (
qty_per_interval == order.quantity
or qty_per_interval < instrument.size_increment
or (instrument.min_quantity and qty_per_interval < instrument.min_quantity)
):
# Immediately submit first order for entire size
self.log.warning(f"Submitting for entire size {qty_per_interval=}, {order.quantity=}.")
self.submit_order(order)
return # Done

scheduled_sizes: list[Quantity] = [qty_per_interval] * num_intervals

if qty_remainder:
scheduled_sizes.append(instrument.make_qty(qty_remainder))

assert sum(scheduled_sizes) == order.quantity
self.log.info(f"Order execution size schedule: {scheduled_sizes}.", LogColor.BLUE)

# Immediately submit first order
if qty_per_interval == order.quantity:
self.log.warning(f"Submitting for entire size {qty_per_interval=}, {order.quantity=}.")
self.submit_order(order)
return # Done

self._scheduled_sizes[order.client_order_id] = scheduled_sizes
first_qty: Quantity = scheduled_sizes.pop(0)

Expand Down
47 changes: 47 additions & 0 deletions tests/unit_tests/execution/test_algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@


ETHUSDT_PERP_BINANCE = TestInstrumentProvider.ethusdt_perp_binance()
FAUX_AAPL_BINANCE = TestInstrumentProvider.equity("AAPL", "BINANCE")


class TestExecAlgorithm:
Expand All @@ -82,6 +83,16 @@ def setup(self) -> None:
self.strategy_id = TestIdStubs.strategy_id()
self.account_id = TestIdStubs.account_id()

# Uncomment for logging
# init_logging(
# self.trader_id,
# UUID4(),
# LoggingConfig().spec_string(),
# None,
# None,
# None,
# )

self.msgbus = MessageBus(
trader_id=self.trader_id,
clock=self.clock,
Expand All @@ -97,6 +108,7 @@ def setup(self) -> None:
logger=self.logger,
)
self.cache.add_instrument(ETHUSDT_PERP_BINANCE)
self.cache.add_instrument(FAUX_AAPL_BINANCE)

self.portfolio = Portfolio(
msgbus=self.msgbus,
Expand Down Expand Up @@ -568,6 +580,41 @@ def test_exec_algorithm_on_order(self) -> None:
"O-19700101-0000-000-None-1-E6",
]

def test_exec_algorithm_on_order_with_small_interval_and_size_precision_zero(self) -> None:
# Arrange
exec_algorithm = TWAPExecAlgorithm()
exec_algorithm.register(
trader_id=self.trader_id,
portfolio=self.portfolio,
msgbus=self.msgbus,
cache=self.cache,
clock=self.clock,
logger=self.logger,
)
exec_algorithm.start()

order = self.strategy.order_factory.market(
instrument_id=FAUX_AAPL_BINANCE.id,
order_side=OrderSide.BUY,
quantity=Quantity.from_str("2"),
exec_algorithm_id=ExecAlgorithmId("TWAP"),
exec_algorithm_params={"horizon_secs": 0.5, "interval_secs": 0.1},
)

# Act
self.strategy.submit_order(order)

events: list[TimeEventHandler] = self.clock.advance_time(secs_to_nanos(2.0))
for event in events:
event.handle()

# Assert
spawned_orders = self.cache.orders_for_exec_spawn(order.client_order_id)
assert self.risk_engine.command_count == 1
assert self.exec_engine.command_count == 1
assert len(spawned_orders) == 1
assert [o.client_order_id.value for o in spawned_orders] == ["O-19700101-0000-000-None-1"]

def test_exec_algorithm_on_order_list_emulated_with_entry_exec_algorithm(self) -> None:
# Arrange
exec_algorithm = TWAPExecAlgorithm()
Expand Down

0 comments on commit 599b9e2

Please sign in to comment.