Skip to content

Commit

Permalink
Refine RiskEngine notional checks and add test
Browse files Browse the repository at this point in the history
  • Loading branch information
cjdsellers committed Dec 1, 2023
1 parent 8852aff commit c59cefa
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 16 deletions.
14 changes: 7 additions & 7 deletions nautilus_trader/risk/engine.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ cdef class RiskEngine(Component):
if max_notional and notional._mem.raw > max_notional._mem.raw:
self._deny_order(
order=order,
reason=f"NOTIONAL_EXCEEDS_MAX_PER_ORDER {max_notional.to_str()} @ {notional.to_str()}",
reason=f"NOTIONAL_EXCEEDS_MAX_PER_ORDER: max_notional={max_notional.to_str()}, notional={notional.to_str()}",
)
return False # Denied

Expand All @@ -691,7 +691,7 @@ cdef class RiskEngine(Component):
):
self._deny_order(
order=order,
reason=f"NOTIONAL_LESS_THAN_MIN_FOR_INSTRUMENT {instrument.min_notional.to_str()} @ {notional.to_str()}",
reason=f"NOTIONAL_LESS_THAN_MIN_FOR_INSTRUMENT: min_notional={instrument.min_notional.to_str()} , notional={notional.to_str()}",
)
return False # Denied

Expand All @@ -703,7 +703,7 @@ cdef class RiskEngine(Component):
):
self._deny_order(
order=order,
reason=f"NOTIONAL_GREATER_THAN_MAX_FOR_INSTRUMENT {instrument.max_notional.to_str()} @ {notional.to_str()}",
reason=f"NOTIONAL_GREATER_THAN_MAX_FOR_INSTRUMENT: max_notional={instrument.max_notional.to_str()}, notional={notional.to_str()}",
)
return False # Denied

Expand All @@ -712,7 +712,7 @@ cdef class RiskEngine(Component):
if free is not None and (free._mem.raw + order_balance_impact._mem.raw) < 0:
self._deny_order(
order=order,
reason=f"NOTIONAL_EXCEEDS_FREE_BALANCE {free.to_str()} @ {order_balance_impact.to_str()}",
reason=f"NOTIONAL_EXCEEDS_FREE_BALANCE: free={free.to_str()}, notional={order_balance_impact.to_str()}",
)
return False # Denied

Expand All @@ -727,7 +727,7 @@ cdef class RiskEngine(Component):
if free is not None and cum_notional_buy._mem.raw >= free._mem.raw:
self._deny_order(
order=order,
reason=f"CUM_NOTIONAL_EXCEEDS_FREE_BALANCE {free.to_str()} @ {cum_notional_buy.to_str()}",
reason=f"CUM_NOTIONAL_EXCEEDS_FREE_BALANCE: free={free.to_str()}, cum_notional={cum_notional_buy.to_str()}",
)
return False # Denied
elif order.is_sell_c():
Expand All @@ -739,7 +739,7 @@ cdef class RiskEngine(Component):
if free is not None and cum_notional_sell._mem.raw >= free._mem.raw:
self._deny_order(
order=order,
reason=f"CUM_NOTIONAL_EXCEEDS_FREE_BALANCE {free.to_str()} @ {cum_notional_sell.to_str()}",
reason=f"CUM_NOTIONAL_EXCEEDS_FREE_BALANCE: free={free.to_str()}, cum_notional={cum_notional_sell.to_str()}",
)
return False # Denied
elif base_currency is not None:
Expand All @@ -751,7 +751,7 @@ cdef class RiskEngine(Component):
if free is not None and cum_notional_sell._mem.raw >= free._mem.raw:
self._deny_order(
order=order,
reason=f"CUM_NOTIONAL_EXCEEDS_FREE_BALANCE {free.to_str()} @ {cum_notional_sell.to_str()}",
reason=f"CUM_NOTIONAL_EXCEEDS_FREE_BALANCE: free={free.to_str()}, cum_notional={cum_notional_sell.to_str()}",
)
return False # Denied

Expand Down
33 changes: 24 additions & 9 deletions nautilus_trader/test_kit/stubs/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from nautilus_trader.common.messages import ComponentStateChanged
from nautilus_trader.common.messages import TradingStateChanged
from nautilus_trader.core.uuid import UUID4
from nautilus_trader.model.currencies import AUD
from nautilus_trader.model.currencies import GBP
from nautilus_trader.model.currencies import USD
from nautilus_trader.model.enums import AccountType
Expand Down Expand Up @@ -84,19 +85,33 @@ def trading_state_changed() -> TradingStateChanged:
)

@staticmethod
def cash_account_state(account_id: AccountId | None = None) -> AccountState:
def cash_account_state(
account_id: AccountId | None = None,
base_currency: Currency | None = USD,
) -> AccountState:
balances = [
AccountBalance(
Money(1_000_000, USD),
Money(0, USD),
Money(1_000_000, USD),
),
]

if base_currency is None:
balances.append(
AccountBalance(
Money(10_000, AUD),
Money(0, AUD),
Money(10_000, AUD),
),
)

return AccountState(
account_id=account_id or TestIdStubs.account_id(),
account_type=AccountType.CASH,
base_currency=USD,
base_currency=base_currency,
reported=True, # reported
balances=[
AccountBalance(
Money(1_000_000, USD),
Money(0, USD),
Money(1_000_000, USD),
),
],
balances=balances,
margins=[],
info={},
event_id=UUID4(),
Expand Down
70 changes: 70 additions & 0 deletions tests/unit_tests/risk/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,76 @@ def test_submit_order_list_sells_when_over_free_balance_then_denies(self):
assert order2.status == OrderStatus.DENIED
assert self.exec_engine.command_count == 0 # <-- Command never reaches engine

def test_submit_order_list_sells_when_multi_currency_cash_account_over_cumulative_notional(
self,
):
# Arrange - change account
exec_client = MockExecutionClient(
client_id=ClientId(self.venue.value),
venue=self.venue,
account_type=AccountType.CASH,
base_currency=None, # <-- Multi-currency
msgbus=self.msgbus,
cache=self.cache,
clock=self.clock,
logger=self.logger,
)

self.exec_engine.deregister_client(self.exec_client)
self.exec_engine.register_client(exec_client)
self.cache.reset() # Clear accounts
self.cache.add_instrument(AUDUSD_SIM) # Re-add instrument
self.portfolio.update_account(TestEventStubs.cash_account_state(base_currency=None))

# Prepare market
quote = TestDataStubs.quote_tick(AUDUSD_SIM)
self.cache.add_quote_tick(quote)

self.exec_engine.start()

strategy = Strategy()
strategy.register(
trader_id=self.trader_id,
portfolio=self.portfolio,
msgbus=self.msgbus,
cache=self.cache,
clock=self.clock,
logger=self.logger,
)

order1 = strategy.order_factory.market(
AUDUSD_SIM.id,
OrderSide.SELL,
Quantity.from_int(5_000),
)

order2 = strategy.order_factory.market(
AUDUSD_SIM.id,
OrderSide.SELL,
Quantity.from_int(5_000),
)

order_list = OrderList(
order_list_id=OrderListId("1"),
orders=[order1, order2],
)

submit_order = SubmitOrderList(
self.trader_id,
strategy.id,
order_list,
UUID4(),
self.clock.timestamp_ns(),
)

# Act
self.risk_engine.execute(submit_order)

# Assert
assert order1.status == OrderStatus.DENIED
assert order2.status == OrderStatus.DENIED
assert self.exec_engine.command_count == 0 # <-- Command never reaches engine

def test_submit_order_when_reducing_and_buy_order_adds_then_denies(self):
# Arrange
self.risk_engine.set_max_notional_per_order(AUDUSD_SIM.id, 1_000_000)
Expand Down

0 comments on commit c59cefa

Please sign in to comment.