From fbeb07ff93c3f6a25a71793b0a86dd47ae25733e Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 1 Dec 2023 19:43:18 +1100 Subject: [PATCH] Fix RiskEngine cumulative notional check --- RELEASES.md | 1 + nautilus_trader/risk/engine.pyx | 43 ++++++++++++++++++++++++--------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 90605bbea8d7..6e45b53b56ef 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -29,6 +29,7 @@ Released on TBD (UTC). - Fixed conversion of fixed precision integers to floats (should be dividing to avoid rounding errors), thanks for reporting @filipmacek - Fixed daily timestamp parsing for Interactive Brokers, thanks @benjaminsingleton - Fixed live reconciliation trade processing for partially filled then canceled orders +- Fixed `RiskEngine` cumulative notional risk check for `CurrencyPair` SELL orders on multi-currency cash accounts --- diff --git a/nautilus_trader/risk/engine.pyx b/nautilus_trader/risk/engine.pyx index 1119c2cd4f4f..49660cf1b920 100644 --- a/nautilus_trader/risk/engine.pyx +++ b/nautilus_trader/risk/engine.pyx @@ -61,6 +61,7 @@ from nautilus_trader.model.identifiers cimport ComponentId from nautilus_trader.model.identifiers cimport InstrumentId from nautilus_trader.model.instruments.base cimport Instrument from nautilus_trader.model.instruments.currency_pair cimport CurrencyPair +from nautilus_trader.model.objects cimport Currency from nautilus_trader.model.objects cimport Money from nautilus_trader.model.objects cimport Price from nautilus_trader.model.objects cimport Quantity @@ -627,6 +628,7 @@ cdef class RiskEngine(Component): Money cum_notional_buy = None Money cum_notional_sell = None Money order_balance_impact = None + Currency base_currency = None double xrate for order in orders: if order.order_type == OrderType.MARKET or order.order_type == OrderType.MARKET_TO_LIMIT: @@ -666,10 +668,11 @@ cdef class RiskEngine(Component): #################################################################### # CASH account balance risk check #################################################################### - if max_notional and isinstance(instrument, CurrencyPair) and order.side == OrderSide.SELL: + if isinstance(instrument, CurrencyPair) and order.side == OrderSide.SELL: xrate = 1.0 / last_px.as_f64_c() notional = Money(order.quantity.as_f64_c() * xrate, instrument.base_currency) - max_notional = Money(max_notional * Decimal(xrate), instrument.base_currency) + if max_notional: + max_notional = Money(max_notional * Decimal(xrate), instrument.base_currency) else: notional = instrument.notional_value(order.quantity, last_px, use_quote_for_inverse=True) @@ -713,6 +716,9 @@ cdef class RiskEngine(Component): ) return False # Denied + if base_currency is None: + base_currency = instrument.get_base_currency() + if order.is_buy_c(): if cum_notional_buy is None: cum_notional_buy = Money(-order_balance_impact, order_balance_impact.currency) @@ -725,16 +731,29 @@ cdef class RiskEngine(Component): ) return False # Denied elif order.is_sell_c(): - if cum_notional_sell is None: - cum_notional_sell = Money(order_balance_impact, order_balance_impact.currency) - else: - cum_notional_sell._mem.raw += order_balance_impact._mem.raw - 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()}", - ) - return False # Denied + if account.base_currency is not None: + if cum_notional_sell is None: + cum_notional_sell = Money(order_balance_impact, order_balance_impact.currency) + else: + cum_notional_sell._mem.raw += order_balance_impact._mem.raw + 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()}", + ) + return False # Denied + elif base_currency is not None: + free = account.balance_free(base_currency) + if cum_notional_sell is None: + cum_notional_sell = notional + else: + cum_notional_sell._mem.raw += notional._mem.raw + 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()}", + ) + return False # Denied # Finally return True # Passed