diff --git a/README.md b/README.md index 7702e83bca98..b9761395f04b 100644 --- a/README.md +++ b/README.md @@ -232,8 +232,8 @@ value_bars = BarSpecification(1_000_000, BarAggregation.VALUE, PriceType.MID) Bars can be either internally or externally aggregated (alternative bar types are only available by internal aggregation). External aggregation is normally for -standard bar periods as available from the provider through the adapter -integration. +standard bar periods as available from the provider through an integrations +adapter. Custom data types can also be requested through a users custom handler, and fed back to the strategies `on_data` method. diff --git a/RELEASES.md b/RELEASES.md index 605c41e15100..12022637fdd0 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,27 @@ +# NautilusTrader 1.111.0 Beta - Release Notes + +This release adds further enhancements to the platform. + +## Breaking Changes +None + +## Enhancements +- `RiskEngine` built out including configuration options hook and + `LiveRiskEngine` implementation. +- Add generic `Throttler`. +- Add details `dict` to `instrument_id` related requests to cover IB futures + contracts. +- Add missing Fiat currencies. +- Add additional Crypto currencies. +- Add ISO 4217 codes. +- Add currency names. + +## Fixes +- Queue `put` coroutines in live engines when blocking at `maxlen` was not + creating a task on the event loop. + +--- + # NautilusTrader 1.110.0 Beta - Release Notes This release applies one more major change to the identifier API. `Security` has diff --git a/docs/source/api_reference/common.rst b/docs/source/api_reference/common.rst index df54eaa7facf..922d28e209c0 100644 --- a/docs/source/api_reference/common.rst +++ b/docs/source/api_reference/common.rst @@ -67,6 +67,15 @@ Logging :members: :member-order: bysource +Providers +--------- + +.. automodule:: nautilus_trader.common.providers + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource + Queue ----- diff --git a/docs/source/api_reference/live.rst b/docs/source/api_reference/live.rst index d3cc39f3aca2..98270186c563 100644 --- a/docs/source/api_reference/live.rst +++ b/docs/source/api_reference/live.rst @@ -13,7 +13,6 @@ Data Client :members: :member-order: bysource - Data Engine ----------- @@ -23,7 +22,6 @@ Data Engine :members: :member-order: bysource - Execution Client ---------------- @@ -33,7 +31,6 @@ Execution Client :members: :member-order: bysource - Execution Engine ---------------- @@ -43,21 +40,19 @@ Execution Engine :members: :member-order: bysource +Risk Engine +----------- -Node ----- - -.. automodule:: nautilus_trader.live.node +.. automodule:: nautilus_trader.live.risk_engine :show-inheritance: :inherited-members: :members: :member-order: bysource +Node +---- -Providers ---------- - -.. automodule:: nautilus_trader.live.providers +.. automodule:: nautilus_trader.live.node :show-inheritance: :inherited-members: :members: diff --git a/docs/source/api_reference/risk.rst b/docs/source/api_reference/risk.rst index 3165e65a569a..8dd1a6044e73 100644 --- a/docs/source/api_reference/risk.rst +++ b/docs/source/api_reference/risk.rst @@ -4,6 +4,15 @@ Risk .. automodule:: nautilus_trader.risk +Engine +------ + +.. automodule:: nautilus_trader.risk.engine + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource + Sizing ------ diff --git a/examples/backtest/crypto_ema_cross_ethusdt_trade_ticks.py b/examples/backtest/crypto_ema_cross_ethusdt_trade_ticks.py index 3d1743b9af44..faae6d3884de 100644 --- a/examples/backtest/crypto_ema_cross_ethusdt_trade_ticks.py +++ b/examples/backtest/crypto_ema_cross_ethusdt_trade_ticks.py @@ -49,7 +49,7 @@ BINANCE = Venue("BINANCE") instrument_id = InstrumentId(symbol=Symbol("ETH/USDT"), venue=BINANCE) - ETHUSDT_BINANCE = instruments.get(instrument_id) + ETHUSDT_BINANCE = instruments.find(instrument_id) # Setup data container data = BacktestDataContainer() @@ -70,7 +70,7 @@ engine = BacktestEngine( data=data, strategies=[strategy], # List of 'any' number of strategies - use_tick_cache=True, # Pre-cache ticks for increased performance on repeated runs + use_data_cache=True, # Pre-cache data for increased performance on repeated runs # exec_db_type="redis", # bypass_logging=True ) diff --git a/examples/backtest/fx_ema_cross_audusd_bars_from_ticks.py b/examples/backtest/fx_ema_cross_audusd_bars_from_ticks.py index 8c3956a04367..656484c2f6a3 100644 --- a/examples/backtest/fx_ema_cross_audusd_bars_from_ticks.py +++ b/examples/backtest/fx_ema_cross_audusd_bars_from_ticks.py @@ -74,7 +74,7 @@ engine = BacktestEngine( data=data, strategies=[strategy], # List of 'any' number of strategies - use_tick_cache=True, # Pre-cache ticks for increased performance on repeated runs + use_data_cache=True, # Pre-cache data for increased performance on repeated runs # exec_db_type="redis", # bypass_logging=True ) diff --git a/examples/backtest/fx_ema_cross_audusd_ticks.py b/examples/backtest/fx_ema_cross_audusd_ticks.py index 525743e42961..7bfea5962dc3 100644 --- a/examples/backtest/fx_ema_cross_audusd_ticks.py +++ b/examples/backtest/fx_ema_cross_audusd_ticks.py @@ -74,7 +74,7 @@ engine = BacktestEngine( data=data, strategies=[strategy], # List of 'any' number of strategies - use_tick_cache=True, # Pre-cache ticks for increased performance on repeated runs + use_data_cache=True, # Pre-cache data for increased performance on repeated runs # exec_db_type="redis", # bypass_logging=True ) diff --git a/examples/backtest/fx_ema_cross_gbpusd_bars.py b/examples/backtest/fx_ema_cross_gbpusd_bars.py index 4796c7a295d0..e2c4b8a94363 100644 --- a/examples/backtest/fx_ema_cross_gbpusd_bars.py +++ b/examples/backtest/fx_ema_cross_gbpusd_bars.py @@ -74,7 +74,7 @@ engine = BacktestEngine( data=data, strategies=[strategy], # List of 'any' number of strategies - use_tick_cache=True, # Pre-cache ticks for increased performance on repeated runs + use_data_cache=True, # Pre-cache data for increased performance on repeated runs # exec_db_type="redis", # exec_db_flush=False, # bypass_logging=True diff --git a/examples/backtest/fx_market_maker_gbpusd_bars.py b/examples/backtest/fx_market_maker_gbpusd_bars.py index 62731d4e514e..6f13f662e8a8 100644 --- a/examples/backtest/fx_market_maker_gbpusd_bars.py +++ b/examples/backtest/fx_market_maker_gbpusd_bars.py @@ -75,7 +75,7 @@ engine = BacktestEngine( data=data, strategies=[strategy], # List of 'any' number of strategies - use_tick_cache=True, + use_data_cache=True, # exec_db_type="redis", # bypass_logging=True ) diff --git a/examples/live/binance_ema_cross.py b/examples/live/binance_ema_cross.py index 8f4904aa60ef..eaf89f15a159 100644 --- a/examples/live/binance_ema_cross.py +++ b/examples/live/binance_ema_cross.py @@ -58,6 +58,8 @@ "port": 6379, }, + "risk": {}, + "strategy": { "load_state": True, # Strategy state is loaded from the database on start "save_state": True, # Strategy state is saved to the database on shutdown diff --git a/examples/live/binance_market_maker.py b/examples/live/binance_market_maker.py index 1ce1b072bb3a..3586c66cafc2 100644 --- a/examples/live/binance_market_maker.py +++ b/examples/live/binance_market_maker.py @@ -58,6 +58,8 @@ "port": 6379, }, + "risk": {}, + "strategy": { "load_state": True, # Strategy state is loaded from the database on start "save_state": True, # Strategy state is saved to the database on shutdown diff --git a/examples/live/bitmex_ema_cross.py b/examples/live/bitmex_ema_cross.py index bffa97d33a94..4452bcd0d9ca 100644 --- a/examples/live/bitmex_ema_cross.py +++ b/examples/live/bitmex_ema_cross.py @@ -58,6 +58,8 @@ "port": 6379, }, + "risk": {}, + "strategy": { "load_state": True, # Strategy state is loaded from the database on start "save_state": True, # Strategy state is saved to the database on shutdown diff --git a/examples/live/bitmex_ema_cross_stop_entry_with_trail.py b/examples/live/bitmex_ema_cross_stop_entry_with_trail.py index 2cf93fd9b211..e7b8f628fbbb 100644 --- a/examples/live/bitmex_ema_cross_stop_entry_with_trail.py +++ b/examples/live/bitmex_ema_cross_stop_entry_with_trail.py @@ -58,6 +58,8 @@ "port": 6379, }, + "risk": {}, + "strategy": { "load_state": True, # Strategy state is loaded from the database on start "save_state": True, # Strategy state is saved to the database on shutdown diff --git a/examples/live/bitmex_market_maker.py b/examples/live/bitmex_market_maker.py index 73de24d842df..875919dd86b6 100644 --- a/examples/live/bitmex_market_maker.py +++ b/examples/live/bitmex_market_maker.py @@ -58,6 +58,8 @@ "port": 6379, }, + "risk": {}, + "strategy": { "load_state": True, # Strategy state is loaded from the database on start "save_state": True, # Strategy state is saved to the database on shutdown diff --git a/examples/live/bitmex_testnet_market_maker.py b/examples/live/bitmex_testnet_market_maker.py index e921e27a3605..be61098d1456 100644 --- a/examples/live/bitmex_testnet_market_maker.py +++ b/examples/live/bitmex_testnet_market_maker.py @@ -58,6 +58,8 @@ "port": 6379, }, + "risk": {}, + "strategy": { "load_state": True, # Strategy state is loaded from the database on start "save_state": True, # Strategy state is saved to the database on shutdown diff --git a/examples/live/multi_venue_ema_cross_stop_entry_with_trail.py b/examples/live/multi_venue_ema_cross_stop_entry_with_trail.py index cd2657eaddb8..0e29b09d3c0b 100644 --- a/examples/live/multi_venue_ema_cross_stop_entry_with_trail.py +++ b/examples/live/multi_venue_ema_cross_stop_entry_with_trail.py @@ -59,6 +59,8 @@ "port": 6379, }, + "risk": {}, + "strategy": { "load_state": True, # Strategy state is loaded from the database on start "save_state": True, # Strategy state is saved to the database on shutdown diff --git a/examples/live/oanda_ema_cross.py b/examples/live/oanda_ema_cross.py index c6eba263566a..4d29f2e26f97 100644 --- a/examples/live/oanda_ema_cross.py +++ b/examples/live/oanda_ema_cross.py @@ -58,6 +58,8 @@ "port": 6379, }, + "risk": {}, + "strategy": { "load_state": True, # Strategy state is loaded from the database on start "save_state": True, # Strategy state is saved to the database on shutdown diff --git a/nautilus_trader/adapters/ccxt/data.pyx b/nautilus_trader/adapters/ccxt/data.pyx index 163bad67d813..b89d6faf7a94 100644 --- a/nautilus_trader/adapters/ccxt/data.pyx +++ b/nautilus_trader/adapters/ccxt/data.pyx @@ -651,7 +651,7 @@ cdef class CCXTDataClient(LiveMarketDataClient): # TODO: Possibly combine this with _watch_quotes async def _watch_order_book(self, InstrumentId instrument_id, int level, int depth, dict kwargs): - cdef Instrument instrument = self._instrument_provider.get(instrument_id) + cdef Instrument instrument = self._instrument_provider.find_c(instrument_id) if instrument is None: self._log.error(f"Cannot subscribe to order book (no instrument for {instrument_id.symbol}).") return @@ -705,7 +705,7 @@ cdef class CCXTDataClient(LiveMarketDataClient): self._log.exception(ex) async def _watch_quotes(self, InstrumentId instrument_id): - cdef Instrument instrument = self._instrument_provider.get(instrument_id) + cdef Instrument instrument = self._instrument_provider.find_c(instrument_id) if instrument is None: self._log.error(f"Cannot subscribe to quote ticks (no instrument for {instrument_id.symbol}).") return @@ -806,7 +806,7 @@ cdef class CCXTDataClient(LiveMarketDataClient): self._handle_quote_tick(tick) async def _watch_trades(self, InstrumentId instrument_id): - cdef Instrument instrument = self._instrument_provider.get(instrument_id) + cdef Instrument instrument = self._instrument_provider.find_c(instrument_id) if instrument is None: self._log.error(f"Cannot subscribe to trade ticks (no instrument for {instrument_id.symbol}).") return @@ -877,7 +877,7 @@ cdef class CCXTDataClient(LiveMarketDataClient): self._handle_trade_tick(tick) async def _watch_ohlcv(self, BarType bar_type): - cdef Instrument instrument = self._instrument_provider.get(bar_type.instrument_id) + cdef Instrument instrument = self._instrument_provider.find_c(bar_type.instrument_id) if instrument is None: self._log.error(f"Cannot subscribe to bars (no instrument for {bar_type.instrument_id}).") return @@ -975,7 +975,7 @@ cdef class CCXTDataClient(LiveMarketDataClient): async def _request_instrument(self, InstrumentId instrument_id, UUID correlation_id): await self._load_instruments() - cdef Instrument instrument = self._instrument_provider.get(instrument_id) + cdef Instrument instrument = self._instrument_provider.find_c(instrument_id) if instrument is not None: self._handle_instruments([instrument], correlation_id) else: @@ -992,7 +992,7 @@ cdef class CCXTDataClient(LiveMarketDataClient): cdef InstrumentId instrument_id cdef Instrument instrument for instrument_id in self._subscribed_instruments: - instrument = self._instrument_provider.get(instrument_id) + instrument = self._instrument_provider.find_c(instrument_id) if instrument is not None: self._handle_instrument(instrument) else: @@ -1010,7 +1010,7 @@ cdef class CCXTDataClient(LiveMarketDataClient): int limit, UUID correlation_id, ): - cdef Instrument instrument = self._instrument_provider.get(instrument_id) + cdef Instrument instrument = self._instrument_provider.find_c(instrument_id) if instrument is None: self._log.error(f"Cannot request trade ticks (no instrument for {instrument_id}).") return @@ -1061,7 +1061,7 @@ cdef class CCXTDataClient(LiveMarketDataClient): int limit, UUID correlation_id, ): - cdef Instrument instrument = self._instrument_provider.get(bar_type.instrument_id) + cdef Instrument instrument = self._instrument_provider.find_c(bar_type.instrument_id) if instrument is None: self._log.error(f"Cannot request bars (no instrument for {bar_type.instrument_id}).") return diff --git a/nautilus_trader/adapters/ccxt/execution.pyx b/nautilus_trader/adapters/ccxt/execution.pyx index 65ebea81851a..2de558431623 100644 --- a/nautilus_trader/adapters/ccxt/execution.pyx +++ b/nautilus_trader/adapters/ccxt/execution.pyx @@ -25,13 +25,13 @@ from nautilus_trader.adapters.ccxt.providers cimport CCXTInstrumentProvider from nautilus_trader.common.clock cimport LiveClock from nautilus_trader.common.logging cimport LogColor from nautilus_trader.common.logging cimport Logger +from nautilus_trader.common.providers cimport InstrumentProvider from nautilus_trader.core.correctness cimport Condition from nautilus_trader.core.datetime cimport from_unix_time_ms from nautilus_trader.core.datetime cimport to_unix_time_ms from nautilus_trader.execution.reports cimport ExecutionStateReport from nautilus_trader.live.execution_client cimport LiveExecutionClient from nautilus_trader.live.execution_engine cimport LiveExecutionEngine -from nautilus_trader.live.providers cimport InstrumentProvider from nautilus_trader.model.c_enums.liquidity_side cimport LiquiditySide from nautilus_trader.model.c_enums.order_side cimport OrderSideParser from nautilus_trader.model.c_enums.order_state cimport OrderState @@ -206,7 +206,7 @@ cdef class CCXTExecutionClient(LiveExecutionClient): self._log.error(f"Cannot resolve state for {repr(order.cl_ord_id)}, " f"OrderId was 'NULL'.") continue # Cannot resolve order - instrument = self._instrument_provider.get(order.symbol) + instrument = self._instrument_provider.find_c(order.symbol) if instrument is None: self._log.error(f"Cannot resolve state for {repr(order.cl_ord_id)}, " f"instrument for {order.instrument_id} not found.") diff --git a/nautilus_trader/adapters/ccxt/providers.pxd b/nautilus_trader/adapters/ccxt/providers.pxd index 78b1053370fe..647582e57adb 100644 --- a/nautilus_trader/adapters/ccxt/providers.pxd +++ b/nautilus_trader/adapters/ccxt/providers.pxd @@ -13,15 +13,20 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from nautilus_trader.live.providers cimport InstrumentProvider +from nautilus_trader.common.providers cimport InstrumentProvider from nautilus_trader.model.c_enums.currency_type cimport CurrencyType +from nautilus_trader.model.currency cimport Currency from nautilus_trader.model.identifiers cimport InstrumentId from nautilus_trader.model.instrument cimport Instrument cdef class CCXTInstrumentProvider(InstrumentProvider): cdef object _client + cdef object _currencies + cdef readonly venue + + cpdef Currency currency(self, str code) cdef void _load_instruments(self) except * cdef void _load_currencies(self) except * cdef inline int _tick_size_to_precision(self, double tick_size) except * diff --git a/nautilus_trader/adapters/ccxt/providers.pyx b/nautilus_trader/adapters/ccxt/providers.pyx index f0d0264ec89a..d64ac3193bf0 100644 --- a/nautilus_trader/adapters/ccxt/providers.pyx +++ b/nautilus_trader/adapters/ccxt/providers.pyx @@ -18,7 +18,7 @@ from decimal import Decimal import ccxt -from nautilus_trader.live.providers cimport InstrumentProvider +from nautilus_trader.common.providers cimport InstrumentProvider from nautilus_trader.model.c_enums.asset_class cimport AssetClass from nautilus_trader.model.c_enums.asset_type cimport AssetType from nautilus_trader.model.c_enums.asset_type cimport AssetTypeParser @@ -50,8 +50,15 @@ cdef class CCXTInstrumentProvider(InstrumentProvider): If all instruments should be loaded at instantiation. """ - self._client = client # Assign first as `load_all` will call it - super().__init__(venue=Venue(client.name.upper()), load_all=load_all) + super().__init__() + + self._client = client + self._currencies = {} # type: dict[str, Currency] + + self.venue = Venue(client.name.upper()) + + if load_all: + self.load_all() async def load_all_async(self): """ @@ -69,19 +76,6 @@ cdef class CCXTInstrumentProvider(InstrumentProvider): self._load_currencies() self._load_instruments() - cpdef dict get_all(self): - """ - Return all loaded instruments. - - If no instruments loaded, will return an empty dict. - - Returns - ------- - dict[InstrumentId, Instrument] - - """ - return self._instruments.copy() - cpdef Currency currency(self, str code): """ Return the currency with the given code (if found). @@ -98,10 +92,6 @@ cdef class CCXTInstrumentProvider(InstrumentProvider): """ return self._currencies.get(code) - cdef Instrument get_c(self, Symbol symbol): - # Provides fast C level access assuming the venue is correct - return self._instruments.get(symbol) - cdef void _load_instruments(self) except *: cdef str k cdef dict v @@ -113,9 +103,7 @@ cdef class CCXTInstrumentProvider(InstrumentProvider): if instrument is None: continue # Something went wrong in parsing - self._instruments[instrument_id.symbol] = instrument - - self.count = len(self._instruments) + self._instruments[instrument_id] = instrument cdef void _load_currencies(self) except *: cdef int precision_mode = self._client.precisionMode @@ -133,6 +121,8 @@ cdef class CCXTInstrumentProvider(InstrumentProvider): currency = Currency( code=code, precision=self._get_precision(precision, precision_mode), + iso4217=0, + name=code, currency_type=currency_type, ) diff --git a/nautilus_trader/adapters/ib/providers.pxd b/nautilus_trader/adapters/ib/providers.pxd index abc37651b472..8b1428e6b64c 100644 --- a/nautilus_trader/adapters/ib/providers.pxd +++ b/nautilus_trader/adapters/ib/providers.pxd @@ -13,22 +13,18 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +from nautilus_trader.common.providers cimport InstrumentProvider +from nautilus_trader.model.c_enums.asset_class cimport AssetClass from nautilus_trader.model.identifiers cimport InstrumentId from nautilus_trader.model.instrument cimport Future -from nautilus_trader.model.instrument cimport Instrument -# cdef class IBInstrumentProvider: -# cdef dict _instruments -# cdef object _client -# cdef str _host -# cdef str _port -# cdef int _client_id -# -# cdef readonly int count -# """The count of instruments held by the provider.\n\n:returns: `int`""" -# -# cpdef void connect(self) -# cpdef Future load_future(self, InstrumentId instrument_id) -# cpdef Instrument get(self, InstrumentId instrument_id) -# cdef inline int _tick_size_to_precision(self, double tick_size) except * -# cdef Future _parse_futures_contract(self, InstrumentId instrument_id, list details_list) + +cdef class IBInstrumentProvider(InstrumentProvider): + cdef object _client + cdef str _host + cdef str _port + cdef int _client_id + + cpdef void connect(self) + cdef inline int _tick_size_to_precision(self, double tick_size) except * + cdef Future _parse_futures_contract(self, InstrumentId instrument_id, AssetClass asset_class, list details_list) diff --git a/nautilus_trader/adapters/ib/providers.pyx b/nautilus_trader/adapters/ib/providers.pyx index 8403f68484fe..0e5b1bc8a434 100644 --- a/nautilus_trader/adapters/ib/providers.pyx +++ b/nautilus_trader/adapters/ib/providers.pyx @@ -21,128 +21,130 @@ from cpython.datetime cimport datetime from ib_insync.contract import ContractDetails +from nautilus_trader.common.providers cimport InstrumentProvider from nautilus_trader.core.correctness cimport Condition +from nautilus_trader.model.c_enums.asset_class cimport AssetClass +from nautilus_trader.model.c_enums.asset_class cimport AssetClassParser +from nautilus_trader.model.currency cimport Currency from nautilus_trader.model.identifiers cimport InstrumentId from nautilus_trader.model.instrument cimport Future -from nautilus_trader.model.instrument cimport Instrument from nautilus_trader.model.objects cimport Quantity -# cdef class IBInstrumentProvider: -# """ -# Provides a means of loading `Instrument` objects through Interactive Brokers. -# """ -# -# def __init__( -# self, -# client not None: ib_insync.IB, -# str host="127.0.0.1", -# str port="7497", -# int client_id=1, -# ): -# """ -# Initialize a new instance of the `IBInstrumentProvider` class. -# -# Parameters -# ---------- -# client : ib_insync.IB -# The Interactive Brokers client. -# host : str -# The client host name or IP address. -# port : str -# The client port number. -# client_id : int -# The unique client identifier number for the connection. -# -# """ -# self.count = 0 -# self._instruments = {} # type: dict[InstrumentId, Instrument] -# self._client = client -# self._host = host -# self._port = port -# self._client_id = client_id -# -# cpdef void connect(self): -# self._client.connect( -# host=self._host, -# port=self._port, -# clientId=self._client_id, -# ) -# -# cpdef Future load_future(self, InstrumentId instrument_id): -# """ -# Return the future contract instrument for the given instrument identifier. -# -# Parameters -# ---------- -# instrument_id : InstrumentId -# The futures contract instrument identifier. -# -# Returns -# ------- -# Future or None -# -# """ -# Condition.not_none(instrument_id, "instrument_id") -# -# if not self._client.client.CONNECTED: -# self.connect() -# -# contract = ib_insync.contract.Future( -# symbol=instrument_id.symbol.value, -# lastTradeDateOrContractMonth=instrument_id.expiry, -# exchange=instrument_id.venue.value, -# multiplier=str(instrument_id.multiplier), -# currency=str(instrument_id.currency), -# ) -# -# cdef list details = self._client.reqContractDetails(contract=contract) -# cdef Future future = self._parse_futures_contract(instrument_id, details) -# -# self._instruments[future.instrument_id] = future -# -# return future -# -# cpdef Instrument get(self, InstrumentId instrument_id): -# """ -# Return the instrument for the given instrument_id (if found). -# -# Returns -# ------- -# Instrument or None -# -# """ -# return self._instruments.get(instrument_id) -# -# cdef inline int _tick_size_to_precision(self, double tick_size) except *: -# cdef tick_size_str = f"{tick_size:f}" -# return len(tick_size_str.partition('.')[2].rstrip('0')) -# -# cdef Future _parse_futures_contract(self, InstrumentId instrument_id, list details_list): -# if len(details_list) == 0: -# raise ValueError(f"No contract details found for the given instrument identifier {instrument_id}") -# elif len(details_list) > 1: -# raise ValueError(f"Multiple contract details found for the given instrument identifier {instrument_id}") -# -# details: ContractDetails = details_list[0] -# -# cdef int price_precision = self._tick_size_to_precision(details.minTick) -# -# cdef Future future = Future( -# instrument_id=instrument_id, -# contract_id=details.contract.conId, -# local_symbol=details.contract.localSymbol, -# trading_class=details.contract.tradingClass, -# market_name=details.marketName, -# long_name=details.longName, -# contract_month=details.contractMonth, -# time_zone_id=details.timeZoneId, -# trading_hours=details.tradingHours, -# liquid_hours=details.liquidHours, -# last_trade_time=details.lastTradeTime, -# price_precision=price_precision, -# tick_size=Decimal(f"{details.minTick:.{price_precision}f}"), -# lot_size=Quantity(1), -# timestamp=datetime.utcnow(), -# ) -# -# return future + +cdef class IBInstrumentProvider(InstrumentProvider): + """ + Provides a means of loading `Instrument` objects through Interactive Brokers. + """ + + def __init__( + self, + client not None: ib_insync.IB, + str host="127.0.0.1", + str port="7497", + int client_id=1, + ): + """ + Initialize a new instance of the `IBInstrumentProvider` class. + + Parameters + ---------- + client : ib_insync.IB + The Interactive Brokers client. + host : str + The client host name or IP address. + port : str + The client port number. + client_id : int + The unique client identifier number for the connection. + + """ + super().__init__() + + self._client = client + self._host = host + self._port = port + self._client_id = client_id + + cpdef void connect(self): + self._client.connect( + host=self._host, + port=self._port, + clientId=self._client_id, + ) + + cpdef void load(self, InstrumentId instrument_id, dict details) except *: + """ + Load the instrument for the given identifier and details. + + Parameters + ---------- + instrument_id : InstrumentId + The instrument identifier. + details : dict + The instrument details. + + """ + Condition.not_none(instrument_id, "instrument_id") + Condition.not_none(details, "details") + + if not self._client.client.CONNECTED: + self.connect() + + contract = ib_insync.contract.Future( + symbol=instrument_id.symbol.value, + lastTradeDateOrContractMonth=details.get("expiry"), + exchange=instrument_id.venue.value, + multiplier=details.get("multiplier"), + currency=details.get("currency"), + ) + + cdef list contract_details = self._client.reqContractDetails(contract=contract) + cdef Future future = self._parse_futures_contract( + instrument_id=instrument_id, + asset_class=AssetClassParser.from_str(details.get("asset_class")), + details_list=contract_details, + ) + + self._instruments[instrument_id] = future + + cdef inline int _tick_size_to_precision(self, double tick_size) except *: + cdef tick_size_str = f"{tick_size:f}" + return len(tick_size_str.partition('.')[2].rstrip('0')) + + cdef Future _parse_futures_contract( + self, InstrumentId instrument_id, + AssetClass asset_class, + list details_list, + ): + if len(details_list) == 0: + raise ValueError(f"No contract details found for the given instrument identifier {instrument_id}") + elif len(details_list) > 1: + raise ValueError(f"Multiple contract details found for the given instrument identifier {instrument_id}") + + details: ContractDetails = details_list[0] + + cdef int price_precision = self._tick_size_to_precision(details.minTick) + + cdef Future future = Future( + instrument_id=instrument_id, + asset_class=asset_class, + currency=Currency.from_str_c(details.contract.currency), + expiry=details.contract.lastTradeDateOrContractMonth, + contract_id=details.contract.conId, + local_symbol=details.contract.localSymbol, + trading_class=details.contract.tradingClass, + market_name=details.marketName, + long_name=details.longName, + contract_month=details.contractMonth, + time_zone_id=details.timeZoneId, + trading_hours=details.tradingHours, + liquid_hours=details.liquidHours, + last_trade_time=details.lastTradeTime, + multiplier=int(details.contract.multiplier), + price_precision=price_precision, + tick_size=Decimal(f"{details.minTick:.{price_precision}f}"), + lot_size=Quantity(1), + timestamp=datetime.utcnow(), + ) + + return future diff --git a/nautilus_trader/adapters/oanda/data.pyx b/nautilus_trader/adapters/oanda/data.pyx index 1bc7a6b82656..ba91f09feda3 100644 --- a/nautilus_trader/adapters/oanda/data.pyx +++ b/nautilus_trader/adapters/oanda/data.pyx @@ -486,7 +486,7 @@ cdef class OandaDataClient(LiveMarketDataClient): cpdef void _request_instrument(self, InstrumentId instrument_id, UUID correlation_id) except *: self._load_instruments() - cdef Instrument instrument = self._instrument_provider.get(instrument_id) + cdef Instrument instrument = self._instrument_provider.find_c(instrument_id) if instrument is not None: self._loop.call_soon_threadsafe(self._handle_instruments_py, [instrument], correlation_id) else: @@ -506,7 +506,7 @@ cdef class OandaDataClient(LiveMarketDataClient): cdef InstrumentId instrument_id cdef Instrument instrument for instrument_id in self._subscribed_instruments: - instrument = self._instrument_provider.get(instrument_id) + instrument = self._instrument_provider.find_c(instrument_id) if instrument is not None: self._loop.call_soon_threadsafe(self._handle_instrument_py, instrument) else: @@ -523,7 +523,7 @@ cdef class OandaDataClient(LiveMarketDataClient): int limit, UUID correlation_id, ) except *: - cdef Instrument instrument = self._instrument_provider.get(bar_type.instrument_id) + cdef Instrument instrument = self._instrument_provider.find_c(bar_type.instrument_id) if instrument is None: self._log.error(f"Cannot request bars (no instrument for {bar_type.instrument_id}).") return diff --git a/nautilus_trader/adapters/oanda/factory.pyx b/nautilus_trader/adapters/oanda/factory.pyx index 726cafd3ffec..c2a7d76895d8 100644 --- a/nautilus_trader/adapters/oanda/factory.pyx +++ b/nautilus_trader/adapters/oanda/factory.pyx @@ -56,7 +56,7 @@ cdef class OandaDataClientFactory: """ # Get credentials oanda_api_token = os.getenv(config.get("api_token", ""), "") - oanda_account_id = os.getenv(config.get("account_id", ""), "") + oanda_account_id = os.getenv(config.get("account_id", ""), "001") # Create client client = oandapyV20.API(access_token=oanda_api_token) diff --git a/nautilus_trader/adapters/oanda/providers.pxd b/nautilus_trader/adapters/oanda/providers.pxd index ddc16e3d9dfb..fc7513c3ed0a 100644 --- a/nautilus_trader/adapters/oanda/providers.pxd +++ b/nautilus_trader/adapters/oanda/providers.pxd @@ -13,22 +13,17 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from nautilus_trader.model.identifiers cimport InstrumentId +from nautilus_trader.common.providers cimport InstrumentProvider from nautilus_trader.model.identifiers cimport Venue from nautilus_trader.model.instrument cimport Instrument -cdef class OandaInstrumentProvider: - cdef dict _instruments +cdef class OandaInstrumentProvider(InstrumentProvider): cdef object _client cdef str _account_id cdef readonly Venue venue """The venue of the provider.\n\n:returns: `Venue`""" - cdef readonly int count - """The count of instruments held by the provider.\n\n:returns: `int`""" cpdef void load_all(self) except * - cpdef dict get_all(self) - cpdef Instrument get(self, InstrumentId instrument_id) cdef Instrument _parse_instrument(self, dict values) diff --git a/nautilus_trader/adapters/oanda/providers.pyx b/nautilus_trader/adapters/oanda/providers.pyx index c1aa5fc951f5..04d5d01aabe3 100644 --- a/nautilus_trader/adapters/oanda/providers.pyx +++ b/nautilus_trader/adapters/oanda/providers.pyx @@ -19,6 +19,8 @@ from decimal import Decimal import oandapyV20 from oandapyV20.endpoints.accounts import AccountInstruments +from nautilus_trader.common.providers cimport InstrumentProvider +from nautilus_trader.core.correctness cimport Condition from nautilus_trader.model.c_enums.asset_class cimport AssetClass from nautilus_trader.model.c_enums.asset_class cimport AssetClassParser from nautilus_trader.model.c_enums.asset_type cimport AssetType @@ -31,7 +33,7 @@ from nautilus_trader.model.instrument cimport Instrument from nautilus_trader.model.objects cimport Quantity -cdef class OandaInstrumentProvider: +cdef class OandaInstrumentProvider(InstrumentProvider): """ Provides a means of loading `Instrument` objects through Oanda. """ @@ -54,13 +56,20 @@ cdef class OandaInstrumentProvider: load_all : bool, optional If all instruments should be loaded at instantiation. + Raises + ------ + ValueError + If account_id is not a valid string. + """ - self.venue = Venue("OANDA") - self.count = 0 - self._instruments = {} # type: dict[InstrumentId, Instrument] + Condition.valid_string(account_id, "account_id") + super().__init__() + self._client = client self._account_id = account_id + self.venue = Venue("OANDA") + if load_all: self.load_all() @@ -78,49 +87,18 @@ cdef class OandaInstrumentProvider: instrument = self._parse_instrument(values) self._instruments[instrument.id] = instrument - self.count = len(self._instruments) - - cpdef dict get_all(self): - """ - Return all loaded instruments. - - If no instruments loaded, will return an empty dict. - - Returns - ------- - dict[InstrumentId, Instrument] - - """ - return self._instruments.copy() - - cpdef Instrument get(self, InstrumentId instrument_id): - """ - Return the instrument for the given instrument identifier (if found). - - Parameters - ---------- - instrument_id : InstrumentId - The instrument identifier for the instrument. - - Returns - ------- - Instrument or None - - """ - return self._instruments.get(instrument_id) - cdef Instrument _parse_instrument(self, dict values): cdef str oanda_name = values["name"] cdef str oanda_type = values["type"] cdef list instrument_id_pieces = values["name"].split('_', maxsplit=1) cdef Currency base_currency = None - cdef Currency quote_currency = Currency(instrument_id_pieces[1], 2, CurrencyType.FIAT) + cdef Currency quote_currency = Currency.from_str_c(instrument_id_pieces[1]) if oanda_type == "CURRENCY": asset_class = AssetClass.FX asset_type = AssetType.SPOT - base_currency = Currency(instrument_id_pieces[0], 2, CurrencyType.FIAT) + base_currency = Currency.from_str_c(instrument_id_pieces[0]) elif oanda_type == "METAL": asset_class = AssetClass.COMMODITY asset_type = AssetType.SPOT diff --git a/nautilus_trader/backtest/data_producer.pxd b/nautilus_trader/backtest/data_producer.pxd index 296eb1f7663d..b16c8ed75d17 100644 --- a/nautilus_trader/backtest/data_producer.pxd +++ b/nautilus_trader/backtest/data_producer.pxd @@ -19,15 +19,15 @@ from nautilus_trader.backtest.data_container cimport BacktestDataContainer from nautilus_trader.common.clock cimport Clock from nautilus_trader.common.logging cimport LoggerAdapter from nautilus_trader.data.engine cimport DataEngine +from nautilus_trader.model.data cimport Data from nautilus_trader.model.tick cimport QuoteTick -from nautilus_trader.model.tick cimport Tick from nautilus_trader.model.tick cimport TradeTick cdef class DataProducerFacade: cpdef void setup(self, datetime start, datetime stop) except * cpdef void reset(self) except * - cpdef Tick next_tick(self) + cpdef Data next(self) cdef class BacktestDataProducer(DataProducerFacade): @@ -63,13 +63,13 @@ cdef class BacktestDataProducer(DataProducerFacade): cdef readonly list execution_resolutions cdef readonly datetime min_timestamp cdef readonly datetime max_timestamp - cdef readonly bint has_tick_data + cdef readonly bint has_data cpdef LoggerAdapter get_logger(self) cpdef void setup(self, datetime start, datetime stop) except * cpdef void reset(self) except * cpdef void clear(self) except * - cpdef Tick next_tick(self) + cpdef Data next(self) cdef inline QuoteTick _generate_quote_tick(self, int index) cdef inline TradeTick _generate_trade_tick(self, int index) @@ -80,7 +80,7 @@ cdef class BacktestDataProducer(DataProducerFacade): cdef class CachedProducer(DataProducerFacade): cdef BacktestDataProducer _producer cdef LoggerAdapter _log - cdef list _tick_cache + cdef list _data_cache cdef list _ts_cache cdef int _tick_index cdef int _tick_index_last @@ -90,9 +90,9 @@ cdef class CachedProducer(DataProducerFacade): cdef readonly list execution_resolutions cdef readonly datetime min_timestamp cdef readonly datetime max_timestamp - cdef readonly bint has_tick_data + cdef readonly bint has_data cpdef void setup(self, datetime start, datetime stop) except * cpdef void reset(self) except * - cpdef Tick next_tick(self) - cdef void _create_tick_cache(self) except * + cpdef Data next(self) + cdef void _create_data_cache(self) except * diff --git a/nautilus_trader/backtest/data_producer.pyx b/nautilus_trader/backtest/data_producer.pyx index 82b93485c263..76d8bc0bde79 100644 --- a/nautilus_trader/backtest/data_producer.pyx +++ b/nautilus_trader/backtest/data_producer.pyx @@ -39,6 +39,7 @@ from nautilus_trader.data.wrangling cimport TradeTickDataWrangler from nautilus_trader.model.c_enums.bar_aggregation cimport BarAggregation from nautilus_trader.model.c_enums.bar_aggregation cimport BarAggregationParser from nautilus_trader.model.c_enums.order_side cimport OrderSideParser +from nautilus_trader.model.data cimport Data from nautilus_trader.model.identifiers cimport TradeMatchId from nautilus_trader.model.objects cimport Price from nautilus_trader.model.objects cimport Quantity @@ -58,7 +59,7 @@ cdef class DataProducerFacade: """Abstract method (implement in subclass).""" raise NotImplementedError("method must be implemented in the subclass") - cpdef Tick next_tick(self): + cpdef Data next(self): """Abstract method (implement in subclass).""" raise NotImplementedError("method must be implemented in the subclass") @@ -163,7 +164,7 @@ cdef class BacktestDataProducer(DataProducerFacade): if execution_resolution is None: self._log.warning(f"No execution level data for {instrument_id}.") - # Increment counter for indexing the next instrument_id + # Increment counter for indexing the next instrument instrument_counter += 1 self.execution_resolutions.append(f"{instrument_id}={execution_resolution}") @@ -218,7 +219,7 @@ cdef class BacktestDataProducer(DataProducerFacade): self._trade_index_last = 0 self._next_trade_tick = None - self.has_tick_data = False + self.has_data = False self._log.info(f"Prepared {len(self._quote_tick_data) + len(self._trade_tick_data):,} " f"total tick rows in {self._clock.unix_time() - ts_total:.3f}s.") @@ -315,7 +316,7 @@ cdef class BacktestDataProducer(DataProducerFacade): # Prepare initial tick self._iterate_trade_ticks() - self.has_tick_data = True + self.has_data = True self._log.info(f"Data stream size: {format_bytes(total_size)}") @@ -345,7 +346,7 @@ cdef class BacktestDataProducer(DataProducerFacade): self._trade_index = 0 self._trade_index_last = len(self._quote_tick_data) - 1 - self.has_tick_data = False + self.has_data = False self._log.info("Reset.") @@ -360,38 +361,40 @@ cdef class BacktestDataProducer(DataProducerFacade): self._log.info("Cleared.") - cpdef Tick next_tick(self): + cpdef Data next(self): """ - Return the next tick in the stream (if one exists). + Return the next data item in the stream (if one exists). - Checking `has_tick_data` is `True` will ensure there is a next tick. + Checking `has_data` is `True` will ensure there is data. Returns ------- - Tick or None + Data or None """ - cdef Tick next_tick + # TODO: Refactor below logic + + cdef Data next_data # Quote ticks only if self._next_trade_tick is None: - next_tick = self._next_quote_tick + next_data = self._next_quote_tick self._iterate_quote_ticks() - return next_tick + return next_data # Trade ticks only if self._next_quote_tick is None: - next_tick = self._next_trade_tick + next_data = self._next_trade_tick self._iterate_trade_ticks() - return next_tick + return next_data # Mixture of quote and trade ticks if self._next_quote_tick.timestamp <= self._next_trade_tick.timestamp: - next_tick = self._next_quote_tick + next_data = self._next_quote_tick self._iterate_quote_ticks() - return next_tick + return next_data else: - next_tick = self._next_trade_tick + next_data = self._next_trade_tick self._iterate_trade_ticks() - return next_tick + return next_data cdef inline QuoteTick _generate_quote_tick(self, int index): return QuoteTick( @@ -420,7 +423,7 @@ cdef class BacktestDataProducer(DataProducerFacade): else: self._next_quote_tick = None if self._next_trade_tick is None: - self.has_tick_data = False + self.has_data = False cdef inline void _iterate_trade_ticks(self) except *: if self._trade_index <= self._trade_index_last: @@ -429,7 +432,7 @@ cdef class BacktestDataProducer(DataProducerFacade): else: self._next_trade_tick = None if self._next_quote_tick is None: - self.has_tick_data = False + self.has_data = False cdef class CachedProducer(DataProducerFacade): @@ -449,7 +452,7 @@ cdef class CachedProducer(DataProducerFacade): """ self._producer = producer self._log = producer.get_logger() - self._tick_cache = [] + self._data_cache = [] self._ts_cache = [] self._tick_index = 0 self._tick_index_last = 0 @@ -459,9 +462,9 @@ cdef class CachedProducer(DataProducerFacade): self.execution_resolutions = self._producer.execution_resolutions self.min_timestamp = self._producer.min_timestamp self.max_timestamp = self._producer.max_timestamp - self.has_tick_data = False + self.has_data = False - self._create_tick_cache() + self._create_data_cache() cpdef void setup(self, datetime start, datetime stop) except *: """ @@ -485,7 +488,7 @@ cdef class CachedProducer(DataProducerFacade): self._tick_index_last = bisect_left(self._ts_cache, stop.timestamp()) self._init_start_tick_index = self._tick_index self._init_stop_tick_index = self._tick_index_last - self.has_tick_data = True + self.has_data = True cpdef void reset(self) except *: """ @@ -495,44 +498,46 @@ cdef class CachedProducer(DataProducerFacade): """ self._tick_index = self._init_start_tick_index self._tick_index_last = self._init_stop_tick_index - self.has_tick_data = True + self.has_data = True - cpdef Tick next_tick(self): + cpdef Data next(self): """ - Return the next tick in the stream (if one exists). + Return the next data item in the stream (if one exists). - Checking `has_tick_data` is `True` will ensure there is a next tick. + Checking `has_data` is `True` will ensure there is data. Returns ------- - Tick or None + Data or None """ - cdef Tick tick + # TODO: Refactor for generic data + + cdef Data data if self._tick_index <= self._tick_index_last: - tick = self._tick_cache[self._tick_index] + data = self._data_cache[self._tick_index] self._tick_index += 1 # Check if last tick if self._tick_index > self._tick_index_last: - self.has_tick_data = False + self.has_data = False - return tick + return data - cdef void _create_tick_cache(self) except *: - self._log.info(f"Pre-caching ticks...") + cdef void _create_data_cache(self) except *: + self._log.info(f"Pre-caching data...") self._producer.setup(self.min_timestamp, self.max_timestamp) cdef double ts = time.time() - cdef Tick tick - while self._producer.has_tick_data: - tick = self._producer.next_tick() - self._tick_cache.append(tick) - self._ts_cache.append(tick.timestamp.timestamp()) + cdef Data data + while self._producer.has_data: + data = self._producer.next() + self._data_cache.append(data) + self._ts_cache.append(data.unix_timestamp) - self._log.info(f"Pre-cached {len(self._tick_cache):,} " - f"total tick rows in {time.time() - ts:.3f}s.") + self._log.info(f"Pre-cached {len(self._data_cache):,} " + f"total data items in {time.time() - ts:.3f}s.") self._producer.reset() self._producer.clear() diff --git a/nautilus_trader/backtest/engine.pxd b/nautilus_trader/backtest/engine.pxd index 499da91fc00d..189f17b7f912 100644 --- a/nautilus_trader/backtest/engine.pxd +++ b/nautilus_trader/backtest/engine.pxd @@ -27,6 +27,7 @@ from nautilus_trader.data.engine cimport DataEngine from nautilus_trader.execution.engine cimport ExecutionEngine from nautilus_trader.model.c_enums.oms_type cimport OMSType from nautilus_trader.model.identifiers cimport Venue +from nautilus_trader.risk.engine cimport RiskEngine from nautilus_trader.trading.portfolio cimport Portfolio from nautilus_trader.trading.trader cimport Trader @@ -37,6 +38,7 @@ cdef class BacktestEngine: cdef UUIDFactory _uuid_factory cdef DataEngine _data_engine cdef ExecutionEngine _exec_engine + cdef RiskEngine _risk_engine cdef DataProducerFacade _data_producer cdef LoggerAdapter _log cdef Logger _logger diff --git a/nautilus_trader/backtest/engine.pyx b/nautilus_trader/backtest/engine.pyx index 4ef582add0fe..8cfdef23c99f 100644 --- a/nautilus_trader/backtest/engine.pyx +++ b/nautilus_trader/backtest/engine.pyx @@ -45,11 +45,13 @@ from nautilus_trader.core.functions cimport pad_string from nautilus_trader.execution.database cimport BypassExecutionDatabase from nautilus_trader.execution.engine cimport ExecutionEngine from nautilus_trader.model.c_enums.oms_type cimport OMSType +from nautilus_trader.model.data cimport Data from nautilus_trader.model.identifiers cimport AccountId from nautilus_trader.model.identifiers cimport TraderId from nautilus_trader.model.identifiers cimport Venue from nautilus_trader.model.tick cimport Tick from nautilus_trader.redis.execution cimport RedisExecutionDatabase +from nautilus_trader.risk.engine cimport RiskEngine from nautilus_trader.serialization.serializers cimport MsgPackCommandSerializer from nautilus_trader.serialization.serializers cimport MsgPackEventSerializer from nautilus_trader.trading.portfolio cimport Portfolio @@ -69,9 +71,10 @@ cdef class BacktestEngine: list strategies=None, int tick_capacity=1000, int bar_capacity=1000, - bint use_tick_cache=False, + bint use_data_cache=False, str exec_db_type not None="in-memory", bint exec_db_flush=True, + dict risk_config=None, bint bypass_logging=False, int level_console=LogLevel.INFO, int level_file=LogLevel.DEBUG, @@ -96,12 +99,14 @@ cdef class BacktestEngine: The length for the data engines internal ticks deque (> 0). bar_capacity : int, optional The length for the data engines internal bars deque (> 0). - use_tick_cache : bool, optional + use_data_cache : bool, optional If use cache for DataProducer (increased performance with repeated backtests on same data). exec_db_type : str, optional The type for the execution cache (can be the default 'in-memory' or redis). exec_db_flush : bool, optional If the execution cache should be flushed on each run. + risk_config : dict[str, object] + The configuration for the risk engine. bypass_logging : bool, optional If logging should be bypassed. level_console : int, optional @@ -138,6 +143,8 @@ cdef class BacktestEngine: trader_id = TraderId("BACKTESTER", "000") if strategies is None: strategies = [] + if risk_config is None: + risk_config = {} Condition.list_type(strategies, TradingStrategy, "strategies") self._clock = LiveClock() @@ -228,7 +235,7 @@ cdef class BacktestEngine: logger=self._test_logger, ) - if use_tick_cache: + if use_data_cache: self._data_producer = CachedProducer(self._data_producer) # Create data client per venue @@ -255,7 +262,16 @@ cdef class BacktestEngine: logger=self._test_logger, ) + self._risk_engine = RiskEngine( + exec_engine=self._exec_engine, + portfolio=self.portfolio, + clock=self._test_clock, + logger=self._test_logger, + config=risk_config, + ) + self._exec_engine.load_cache() + self._exec_engine.register_risk_engine(self._risk_engine) self.trader = Trader( trader_id=trader_id, @@ -263,6 +279,7 @@ cdef class BacktestEngine: portfolio=self.portfolio, data_engine=self._data_engine, exec_engine=self._exec_engine, + risk_engine=self._risk_engine, clock=self._test_clock, logger=self._test_logger, ) @@ -402,16 +419,23 @@ cdef class BacktestEngine: """ self._log.debug(f"Resetting...") + # Reset DataEngine if self._data_engine.state_c() == ComponentState.RUNNING: self._data_engine.stop() self._data_engine.reset() + # Reset ExecEngine if self._exec_engine.state_c() == ComponentState.RUNNING: self._exec_engine.stop() if self._exec_db_flush: self._exec_engine.flush_db() self._exec_engine.reset() + # Reset RiskEngine + if self._risk_engine.state_c() == ComponentState.RUNNING: + self._risk_engine.stop() + self._risk_engine.reset() + self.trader.reset() for exchange in self._exchanges.values(): @@ -440,6 +464,7 @@ cdef class BacktestEngine: self._data_engine.dispose() self._exec_engine.dispose() + self._risk_engine.dispose() cpdef void change_fill_model(self, Venue venue, FillModel model) except *: """ @@ -546,14 +571,15 @@ cdef class BacktestEngine: self._exec_engine.start() self.trader.start() - cdef Tick tick + cdef Data data # -- MAIN BACKTEST LOOP -----------------------------------------------# - while self._data_producer.has_tick_data: - tick = self._data_producer.next_tick() - self._advance_time(tick.timestamp) - self._exchanges[tick.venue].process_tick(tick) - self._data_engine.process(tick) - self._process_modules(tick.timestamp) + while self._data_producer.has_data: + data = self._data_producer.next() + self._advance_time(data.timestamp) + if isinstance(data, Tick): + self._exchanges[data.venue].process_tick(data) + self._data_engine.process(data) + self._process_modules(data.timestamp) self.iteration += 1 # ---------------------------------------------------------------------# @@ -575,9 +601,8 @@ cdef class BacktestEngine: self._test_clock.set_time(now) cdef inline void _process_modules(self, datetime now) except *: - cdef Venue venue cdef SimulatedExchange exchange - for venue, exchange in self._exchanges.items(): + for exchange in self._exchanges.values(): exchange.process_modules(now) cdef inline void _log_header( diff --git a/nautilus_trader/backtest/exchange.pyx b/nautilus_trader/backtest/exchange.pyx index 3752337fb3e0..05180e030b82 100644 --- a/nautilus_trader/backtest/exchange.pyx +++ b/nautilus_trader/backtest/exchange.pyx @@ -61,6 +61,7 @@ from nautilus_trader.model.order.stop_market cimport StopMarketOrder from nautilus_trader.model.position cimport Position from nautilus_trader.model.tick cimport QuoteTick from nautilus_trader.model.tick cimport Tick +from nautilus_trader.model.tick cimport TradeTick from nautilus_trader.trading.calculators cimport ExchangeRateCalculator @@ -232,7 +233,7 @@ cdef class SimulatedExchange: Parameters ---------- tick : Tick - The tick data to process with (`QuoteTick` or `TradeTick`). + The tick to process. """ Condition.not_none(tick, "tick") @@ -249,7 +250,7 @@ cdef class SimulatedExchange: ask = tick.ask self._market_bids[instrument_id] = bid self._market_asks[instrument_id] = ask - else: # TradeTick + elif isinstance(tick, TradeTick): if tick.side == OrderSide.SELL: # TAKER hit the bid bid = tick.price ask = self._market_asks.get(instrument_id) @@ -263,6 +264,8 @@ cdef class SimulatedExchange: bid = ask # Initialize bid self._market_asks[instrument_id] = ask # tick.side must be BUY or SELL (condition checked in TradeTick) + else: + raise RuntimeError("not market data") # Design-time error cdef PassiveOrder order for order in self._working_orders.copy().values(): # Copy dict for safe loop @@ -289,6 +292,10 @@ cdef class SimulatedExchange: The time to advance to. """ + Condition.not_none(now, "now") + + self._clock.set_time(now) + # Iterate through modules cdef SimulationModule module for module in self.modules: diff --git a/nautilus_trader/backtest/modules.pyx b/nautilus_trader/backtest/modules.pyx index 69cc293763e8..db0daf38fe3a 100644 --- a/nautilus_trader/backtest/modules.pyx +++ b/nautilus_trader/backtest/modules.pyx @@ -95,6 +95,7 @@ cdef class FXRolloverInterestModule(SimulationModule): """ super().__init__() + self._calculator = RolloverInterestCalculator(data=rate_data) self._rollover_time = None # Initialized at first rollover self._rollover_applied = False diff --git a/nautilus_trader/common/cache.pyx b/nautilus_trader/common/cache.pyx index 42a5e99ea9a2..2f6a02097819 100644 --- a/nautilus_trader/common/cache.pyx +++ b/nautilus_trader/common/cache.pyx @@ -28,7 +28,7 @@ cdef class IdentifierCache: self._cached_trader_ids = ObjectCache(TraderId, TraderId.from_str_c) self._cached_account_ids = ObjectCache(AccountId, AccountId.from_str_c) self._cached_strategy_ids = ObjectCache(StrategyId, StrategyId.from_str_c) - self._cached_instrument_ids = ObjectCache(InstrumentId, InstrumentId.from_serializable_str_c) + self._cached_instrument_ids = ObjectCache(InstrumentId, InstrumentId.from_str_c) cpdef TraderId get_trader_id(self, str value): """ diff --git a/nautilus_trader/common/clock.pyx b/nautilus_trader/common/clock.pyx index 2fc26d478843..d08c91423c45 100644 --- a/nautilus_trader/common/clock.pyx +++ b/nautilus_trader/common/clock.pyx @@ -512,6 +512,7 @@ cdef class LiveClock(Clock): """ super().__init__() + self._loop = loop self._utc = pytz.utc diff --git a/nautilus_trader/live/providers.pxd b/nautilus_trader/common/providers.pxd similarity index 67% rename from nautilus_trader/live/providers.pxd rename to nautilus_trader/common/providers.pxd index 88a0f549a746..26676cd2b131 100644 --- a/nautilus_trader/live/providers.pxd +++ b/nautilus_trader/common/providers.pxd @@ -13,24 +13,15 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from nautilus_trader.model.currency cimport Currency from nautilus_trader.model.identifiers cimport InstrumentId -from nautilus_trader.model.identifiers cimport Symbol -from nautilus_trader.model.identifiers cimport Venue from nautilus_trader.model.instrument cimport Instrument cdef class InstrumentProvider: - cdef dict _currencies cdef dict _instruments - cdef readonly Venue venue - """The venue of the provider.\n\n:returns: `Venue`""" - cdef readonly int count - """The count of instruments held by the provider.\n\n:returns: `int`""" - cpdef void load_all(self) except * + cpdef void load(self, InstrumentId instrument_id, dict details) except * cpdef dict get_all(self) - cpdef Currency currency(self, str code) - cpdef Instrument get(self, InstrumentId instrument_id) - cdef Instrument get_c(self, Symbol symbol) + cpdef Instrument find(self, InstrumentId instrument_id) + cdef Instrument find_c(self, InstrumentId instrument_id) diff --git a/nautilus_trader/live/providers.pyx b/nautilus_trader/common/providers.pyx similarity index 60% rename from nautilus_trader/live/providers.pyx rename to nautilus_trader/common/providers.pyx index 21a5ef2220aa..bcd94953d62d 100644 --- a/nautilus_trader/live/providers.pyx +++ b/nautilus_trader/common/providers.pyx @@ -13,10 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from nautilus_trader.model.currency cimport Currency from nautilus_trader.model.identifiers cimport InstrumentId -from nautilus_trader.model.identifiers cimport Symbol -from nautilus_trader.model.identifiers cimport Venue from nautilus_trader.model.instrument cimport Instrument @@ -27,26 +24,24 @@ cdef class InstrumentProvider: This class should not be used directly, but through its concrete subclasses. """ - def __init__(self, Venue venue not None, bint load_all=False): + def __init__(self): """ Initialize a new instance of the `InstrumentProvider` class. - Parameters - ---------- - venue : Venue - The venue for the provider. - load_all : bool, optional - If all instruments should be loaded at instantiation. + """ + self._instruments = {} # type: dict[InstrumentId, Instrument] + @property + def count(self) -> int: """ - self.venue = venue - self.count = 0 + The count of instruments held by the provider. - self._currencies = {} # type: dict[str, Currency] - self._instruments = {} # type: dict[Symbol, Instrument] + Returns + ------- + int - if load_all: - self.load_all() + """ + return len(self._instruments) async def load_all_async(self): """Abstract method (implement in subclass).""" @@ -56,30 +51,39 @@ cdef class InstrumentProvider: """Abstract method (implement in subclass).""" raise NotImplementedError("method must be implemented in the subclass") - cpdef dict get_all(self): + cpdef void load(self, InstrumentId instrument_id, dict details) except *: """Abstract method (implement in subclass).""" raise NotImplementedError("method must be implemented in the subclass") - cpdef Instrument get(self, InstrumentId instrument_id): + cpdef dict get_all(self): """ - Get the instrument for the given instrument identifier (if found). + Return all loaded instruments. + + If no instruments loaded, will return an empty dict. + + Returns + ------- + dict[InstrumentId, Instrument] + + """ + return self._instruments.copy() + + cpdef Instrument find(self, InstrumentId instrument_id): + """ + Return the instrument for the given instrument identifier (if found). Parameters ---------- instrument_id : InstrumentId - The instrument identifier for the instrument + The identifier for the instrument Returns ------- Instrument or None """ - return self.get_c(instrument_id.symbol) + return self.find_c(instrument_id) - cpdef Currency currency(self, str code): - """Abstract method (implement in subclass).""" - raise NotImplementedError("method must be implemented in the subclass") - - cdef Instrument get_c(self, Symbol symbol): - """Abstract method (implement in subclass).""" - raise NotImplementedError("method must be implemented in the subclass") + cdef Instrument find_c(self, InstrumentId instrument_id): + # Provides faster C level access + return self._instruments.get(instrument_id) diff --git a/nautilus_trader/common/queue.pxd b/nautilus_trader/common/queue.pxd index e31786d4b9e6..cfddd4ebb915 100644 --- a/nautilus_trader/common/queue.pxd +++ b/nautilus_trader/common/queue.pxd @@ -18,7 +18,9 @@ cdef class Queue: cdef object _queue cdef readonly int maxsize + """The maximum capacity of the queue before blocking.\n\n:returns: `int`""" cdef readonly int count + """The current count of items on the queue.\n\n:returns: `int`""" cpdef int qsize(self) except * cpdef bint empty(self) except * diff --git a/nautilus_trader/common/queue.pyx b/nautilus_trader/common/queue.pyx index da03011f69f2..de97fd2672b5 100644 --- a/nautilus_trader/common/queue.pyx +++ b/nautilus_trader/common/queue.pyx @@ -23,9 +23,6 @@ cdef class Queue: Provides a high-performance stripped back queue for use with coroutines and an event loop. - This queue is not thread-safe and must be called from the same thread as the - event loop. - If maxsize is less than or equal to zero, the queue size is infinite. If it is an integer greater than 0, then "await put()" will block when the queue reaches maxsize, until an item is removed by get(). @@ -33,9 +30,25 @@ cdef class Queue: Unlike the standard library Queue, you can reliably know this Queue's size with qsize(), since your single-threaded asyncio application won't be interrupted between calling qsize() and doing an operation on the Queue. + + Warnings + -------- + This queue is not thread-safe and must be called from the same thread as the + event loop. + """ - def __init__(self, maxsize=0): + def __init__(self, int maxsize=0): + """ + Initialize a new instance of the `Queue` class. + + Parameters + ---------- + maxsize : int + The maximum capacity of the queue before blocking. + + """ + self.maxsize = maxsize self.count = 0 diff --git a/nautilus_trader/common/throttler.pxd b/nautilus_trader/common/throttler.pxd new file mode 100644 index 000000000000..18e06ce25963 --- /dev/null +++ b/nautilus_trader/common/throttler.pxd @@ -0,0 +1,44 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +from cpython.datetime cimport timedelta + +from nautilus_trader.common.clock cimport Clock +from nautilus_trader.common.logging cimport LoggerAdapter +from nautilus_trader.common.queue cimport Queue +from nautilus_trader.common.timer cimport TimeEvent + + +cdef class Throttler: + cdef Clock _clock + cdef LoggerAdapter _log + cdef Queue _queue + cdef int _limit + cdef int _vouchers + cdef str _token + cdef timedelta _interval + cdef object _output + + cdef readonly str name + """The name of the throttler.\n\n:returns: `str`""" + cdef readonly bint is_active + """If the throttler is actively timing.\n\n:returns: `bool`""" + cdef readonly bint is_throttling + """If the throttler is currently throttling items.\n\n:returns: `bool`""" + + cpdef void send(self, item) except * + cpdef void _process_queue(self) except * + cpdef void _refresh_vouchers(self, TimeEvent event) except * + cdef inline void _run_timer(self) except * diff --git a/nautilus_trader/common/throttler.pyx b/nautilus_trader/common/throttler.pyx new file mode 100644 index 000000000000..ff0440ab372e --- /dev/null +++ b/nautilus_trader/common/throttler.pyx @@ -0,0 +1,160 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +from cpython.datetime cimport timedelta + +from nautilus_trader.common.clock cimport Clock +from nautilus_trader.common.logging cimport Logger +from nautilus_trader.common.queue cimport Queue +from nautilus_trader.common.timer cimport TimeEvent +from nautilus_trader.core.correctness cimport Condition + + +cdef class Throttler: + """ + Provides a generic throttler with an internal queue. + + Will throttle messages to the given limit-interval combination. + + Warnings + -------- + This throttler is not thread-safe and must be called from the same thread as + the event loop. + + The internal queue is unbounded and so a bounded queue should be upstream. + + """ + + def __init__( + self, + str name, + int limit, + timedelta interval not None, + output not None: callable, + Clock clock not None, + Logger logger not None, + ): + """ + Initialize a new instance of the `Throttler` class. + + Parameters + ---------- + name : str + The unique name of the throttler. + limit : int + The limit setting for the throttling. + interval : timedelta + The interval setting for the throttling. + output : callable + The output handler from the throttler. + clock : Clock + The clock for the throttler. + logger : Logger + The logger for the throttler. + + Raises + ------ + ValueError + If name is not a valid string. + ValueError + If limit is not positive (> 0). + ValueError + If output is not of type callable. + + """ + Condition.valid_string(name, "name") + Condition.positive_int(limit, "limit") + Condition.callable(output, "output") + + self._clock = clock + self._log = LoggerAdapter(name, logger) + self._queue = Queue() + self._limit = limit + self._vouchers = limit + self._token = name + "-REFRESH-TOKEN" + self._interval = interval + self._output = output + + self.name = name + self.is_active = False + self.is_throttling = False + + @property + def qsize(self): + """ + The qsize of the internal queue. + + Returns + ------- + int + + """ + return self._queue.qsize() + + cpdef void send(self, item) except *: + """ + Send the given item on the throttler. + + If currently idle then internal refresh token timer will start running. + If currently throttling then item will be placed on the internal queue + to be sent when vouchers are refreshed. + + Parameters + ---------- + item : object + The item to send on the throttler. + + Notes + ----- + Test system specs: x86_64 @ 4000 MHz Linux-4.15.0-136-lowlatency-x86_64-with-glibc2.27 + Performance overhead ~0.3μs. + + """ + if not self.is_active: + self._run_timer() + self._queue.put_nowait(item) + self._process_queue() + + cpdef void _process_queue(self) except *: + while self._vouchers > 0 and not self._queue.empty(): + item = self._queue.get_nowait() + self._output(item) + self._vouchers -= 1 + + if self._vouchers == 0 and not self._queue.empty(): + self.is_throttling = True + self._log.debug("At limit.") + else: + self.is_throttling = False + + cpdef void _refresh_vouchers(self, TimeEvent event) except *: + self._vouchers = self._limit + + if self._queue.empty(): + self.is_active = False + self.is_throttling = False + self._log.debug("Idle.") + else: + self._run_timer() + self._process_queue() + + cdef inline void _run_timer(self) except *: + self.is_active = True + self._log.debug("Active.") + self._clock.set_time_alert( + name=self._token, + alert_time=self._clock.utc_now() + self._interval, + handler=self._refresh_vouchers, + ) diff --git a/nautilus_trader/common/timer.pyx b/nautilus_trader/common/timer.pyx index 22f93322a659..37d1c7315986 100644 --- a/nautilus_trader/common/timer.pyx +++ b/nautilus_trader/common/timer.pyx @@ -76,6 +76,12 @@ cdef class TimeEventHandler: self.event = event self._handler = handler + def handle_py(self) -> None: + """ + Python wrapper for testing. + """ + self.handle() + cdef void handle(self) except *: self._handler(self.event) @@ -462,7 +468,7 @@ cdef class LoopTimer(LiveTimer): """ Condition.valid_string(name, "name") - self._loop = loop + self._loop = loop # Assign here as `super().__init__` will call it super().__init__(name, callback, interval, now, start_time, stop_time) cdef object _start_timer(self, datetime now): diff --git a/nautilus_trader/core/message.pxd b/nautilus_trader/core/message.pxd index e38b90e2a086..858038b10860 100644 --- a/nautilus_trader/core/message.pxd +++ b/nautilus_trader/core/message.pxd @@ -25,7 +25,7 @@ cpdef enum MessageType: DOCUMENT = 3, EVENT = 4, REQUEST = 5, - RESPONSE = 6 + RESPONSE = 6, cpdef str message_type_to_str(int value) diff --git a/nautilus_trader/data/client.pxd b/nautilus_trader/data/client.pxd index 9411c08b489f..be84a8887ee7 100644 --- a/nautilus_trader/data/client.pxd +++ b/nautilus_trader/data/client.pxd @@ -23,6 +23,7 @@ from nautilus_trader.data.engine cimport DataEngine from nautilus_trader.model.bar cimport Bar from nautilus_trader.model.bar cimport BarType from nautilus_trader.model.data cimport DataType +from nautilus_trader.model.data cimport GenericData from nautilus_trader.model.identifiers cimport InstrumentId from nautilus_trader.model.instrument cimport Instrument from nautilus_trader.model.order_book cimport OrderBook @@ -58,8 +59,8 @@ cdef class DataClient: # -- DATA HANDLERS --------------------------------------------------------------------------------- - cdef void _handle_data(self, DataType data_type, data, datetime timestamp=*) except * - cdef void _handle_data_response(self, DataType data_type, data, UUID correlation_id) except * + cdef void _handle_data(self, GenericData data) except * + cdef void _handle_data_response(self, GenericData data, UUID correlation_id) except * cdef class MarketDataClient(DataClient): diff --git a/nautilus_trader/data/client.pyx b/nautilus_trader/data/client.pyx index 221e86efa07a..6d2481b13d19 100644 --- a/nautilus_trader/data/client.pyx +++ b/nautilus_trader/data/client.pyx @@ -126,23 +126,21 @@ cdef class DataClient: # -- PYTHON WRAPPERS ------------------------------------------------------------------------------- - def _handle_data_py(self, DataType data_type, data, datetime timestamp=None): - self._handle_data(data_type, data, timestamp) + def _handle_data_py(self, GenericData data): + self._handle_data(data) - def _handle_data_response_py(self, DataType data_type, data, UUID correlation_id): - self._handle_data_response(data_type, data, correlation_id) + def _handle_data_response_py(self, GenericData data, UUID correlation_id): + self._handle_data_response(data, correlation_id) # -- DATA HANDLERS --------------------------------------------------------------------------------- - cdef void _handle_data(self, DataType data_type, data, datetime timestamp=None) except *: - if timestamp is None: - timestamp = self._clock.utc_now() - self._engine.process(GenericData(data_type, data, timestamp)) + cdef void _handle_data(self, GenericData data) except *: + self._engine.process(data) - cdef void _handle_data_response(self, DataType data_type, data, UUID correlation_id) except *: + cdef void _handle_data_response(self, GenericData data, UUID correlation_id) except *: cdef DataResponse response = DataResponse( provider=self.name, - data_type=data_type, + data_type=data.data_type, data=data, correlation_id=correlation_id, response_id=self._uuid_factory.generate(), diff --git a/nautilus_trader/data/engine.pxd b/nautilus_trader/data/engine.pxd index da258b1da733..250fac0f1c6d 100644 --- a/nautilus_trader/data/engine.pxd +++ b/nautilus_trader/data/engine.pxd @@ -30,8 +30,8 @@ from nautilus_trader.data.messages cimport Subscribe from nautilus_trader.data.messages cimport Unsubscribe from nautilus_trader.model.bar cimport Bar from nautilus_trader.model.bar cimport BarType -from nautilus_trader.model.data cimport Data from nautilus_trader.model.data cimport DataType +from nautilus_trader.model.data cimport GenericData from nautilus_trader.model.identifiers cimport InstrumentId from nautilus_trader.model.instrument cimport Instrument from nautilus_trader.model.order_book cimport OrderBook @@ -115,7 +115,7 @@ cdef class DataEngine(Component): cdef inline void _handle_quote_tick(self, QuoteTick tick) except * cdef inline void _handle_trade_tick(self, TradeTick tick) except * cdef inline void _handle_bar(self, BarType bar_type, Bar bar) except * - cdef inline void _handle_custom_data(self, Data data) except * + cdef inline void _handle_custom_data(self, GenericData data) except * # -- RESPONSE HANDLERS ----------------------------------------------------------------------------- diff --git a/nautilus_trader/data/engine.pyx b/nautilus_trader/data/engine.pyx index bd6fd2c05263..a0e436815095 100644 --- a/nautilus_trader/data/engine.pyx +++ b/nautilus_trader/data/engine.pyx @@ -64,8 +64,8 @@ from nautilus_trader.model.bar cimport BarType from nautilus_trader.model.c_enums.bar_aggregation cimport BarAggregation from nautilus_trader.model.c_enums.bar_aggregation cimport BarAggregationParser from nautilus_trader.model.c_enums.price_type cimport PriceType -from nautilus_trader.model.data cimport Data from nautilus_trader.model.data cimport DataType +from nautilus_trader.model.data cimport GenericData from nautilus_trader.model.identifiers cimport InstrumentId from nautilus_trader.model.instrument cimport Instrument from nautilus_trader.model.order_book cimport OrderBook @@ -964,7 +964,7 @@ cdef class DataEngine(Component): self._handle_bar(data.bar_type, data.bar) elif isinstance(data, Instrument): self._handle_instrument(data) - elif isinstance(data, Data): + elif isinstance(data, GenericData): self._handle_custom_data(data) else: self._log.error(f"Cannot handle data: unrecognized type {type(data)} {data}.") @@ -1011,7 +1011,7 @@ cdef class DataEngine(Component): for handler in bar_handlers: handler(bar_type, bar) - cdef inline void _handle_custom_data(self, Data data) except *: + cdef inline void _handle_custom_data(self, GenericData data) except *: # Send to all registered data handlers for that data type cdef list handlers = self._data_handlers.get(data.data_type, []) for handler in handlers: @@ -1114,7 +1114,7 @@ cdef class DataEngine(Component): cpdef void _snapshot_order_book(self, TimeEvent snap_event) except *: cdef tuple pieces = snap_event.name.partition('-')[2].partition('-') - cdef InstrumentId instrument_id = InstrumentId.from_serializable_str_c(pieces[0]) + cdef InstrumentId instrument_id = InstrumentId.from_str_c(pieces[0]) cdef int interval = int(pieces[2]) cdef list handlers = self._order_book_intervals.get((instrument_id, interval)) if handlers is None: diff --git a/nautilus_trader/execution/engine.pxd b/nautilus_trader/execution/engine.pxd index dcc412a34dd9..fef1fbd49108 100644 --- a/nautilus_trader/execution/engine.pxd +++ b/nautilus_trader/execution/engine.pxd @@ -77,6 +77,10 @@ cdef class ExecutionEngine(Component): cpdef void _on_start(self) except * cpdef void _on_stop(self) except * +# -- INTERNAL -------------------------------------------------------------------------------------- + + cdef inline void _set_position_id_counts(self) except * + # -- COMMANDS -------------------------------------------------------------------------------------- cpdef void load_cache(self) except * @@ -111,7 +115,3 @@ cdef class ExecutionEngine(Component): cdef inline PositionChanged _pos_changed_event(self, Position position, OrderFilled fill) cdef inline PositionClosed _pos_closed_event(self, Position position, OrderFilled fill) cdef inline void _send_to_strategy(self, Event event, StrategyId strategy_id) except * - -# -- INTERNAL -------------------------------------------------------------------------------------- - - cdef inline void _set_position_id_counts(self) except * diff --git a/nautilus_trader/execution/engine.pyx b/nautilus_trader/execution/engine.pyx index 5e90c8c5fe35..fef347d2f2dc 100644 --- a/nautilus_trader/execution/engine.pyx +++ b/nautilus_trader/execution/engine.pyx @@ -124,9 +124,9 @@ cdef class ExecutionEngine(Component): self.event_count = 0 @property - def registered_venues(self): + def registered_clients(self): """ - The trading venues registered with the execution engine. + The execution clients registered with the engine. Returns ------- @@ -138,7 +138,7 @@ cdef class ExecutionEngine(Component): @property def registered_strategies(self): """ - The strategy identifiers registered with the execution engine. + The strategy identifiers registered with the engine. Returns ------- @@ -261,6 +261,9 @@ cdef class ExecutionEngine(Component): self._clients[client.venue] = client self._log.info(f"Registered {client}.") + if self._risk_engine is not None and client not in self._risk_engine.registered_clients: + self._risk_engine.register_client(client) + cpdef void register_strategy(self, TradingStrategy strategy) except *: """ Register the given strategy with the execution engine. @@ -299,6 +302,14 @@ cdef class ExecutionEngine(Component): self._risk_engine = engine self._log.info(f"Registered {engine}.") + cdef list risk_registered = self._risk_engine.registered_clients + + cdef Venue venue + cdef ExecutionClient client + for venue, client in self._clients.items(): + if venue not in risk_registered: + self._risk_engine.register_client(client) + cpdef void deregister_client(self, ExecutionClient client) except *: """ Deregister the given execution client from the execution engine. @@ -445,6 +456,31 @@ cdef class ExecutionEngine(Component): """ self.cache.flush_db() +# -- INTERNAL -------------------------------------------------------------------------------------- + + cdef inline void _set_position_id_counts(self) except *: + # For the internal position identifier generator + cdef list positions = self.cache.positions() + + # Count positions per instrument_id + cdef dict counts = {} # type: dict[StrategyId, int] + cdef int count + cdef Position position + for position in positions: + count = counts.get(position.strategy_id, 0) + count += 1 + # noinspection PyUnresolvedReferences + counts[position.strategy_id] = count + + # Reset position identifier generator + self._pos_id_generator.reset() + + # Set counts + cdef StrategyId strategy_id + for strategy_id, count in counts.items(): + self._pos_id_generator.set_count(strategy_id, count) + self._log.info(f"Set PositionId count for {repr(strategy_id)} to {count}.") + # -- COMMAND HANDLERS ------------------------------------------------------------------------------ cdef inline void _execute_command(self, TradingCommand command) except *: @@ -486,7 +522,10 @@ cdef class ExecutionEngine(Component): return # Invalid command # Submit order - client.submit_order(command) + if self._risk_engine is not None: + self._risk_engine.execute(command) + else: + client.submit_order(command) cdef inline void _handle_submit_bracket_order(self, ExecutionClient client, SubmitBracketOrder command) except *: # Validate command @@ -507,8 +546,11 @@ cdef class ExecutionEngine(Component): if command.bracket_order.take_profit is not None: self.cache.add_order(command.bracket_order.take_profit, PositionId.null_c()) - # Submit bracket order - client.submit_bracket_order(command) + # Submit order + if self._risk_engine is not None: + self._risk_engine.execute(command) + else: + client.submit_bracket_order(command) cdef inline void _handle_amend_order(self, ExecutionClient client, AmendOrder command) except *: # Validate command @@ -517,7 +559,11 @@ cdef class ExecutionEngine(Component): f"{repr(command.cl_ord_id)} already completed.") return # Invalid command - client.amend_order(command) + # Amend order + if self._risk_engine is not None: + self._risk_engine.execute(command) + else: + client.amend_order(command) cdef inline void _handle_cancel_order(self, ExecutionClient client, CancelOrder command) except *: # Validate command @@ -526,7 +572,11 @@ cdef class ExecutionEngine(Component): f"{repr(command.cl_ord_id)} already completed.") return # Invalid command - client.cancel_order(command) + # Cancel order + if self._risk_engine is not None: + self._risk_engine.execute(command) + else: + client.cancel_order(command) cdef inline void _invalidate_order(self, ClientOrderId cl_ord_id, str reason) except *: # Generate event @@ -858,28 +908,3 @@ cdef class ExecutionEngine(Component): return # Cannot send to strategy strategy.handle_event_c(event) - -# -- INTERNAL -------------------------------------------------------------------------------------- - - cdef inline void _set_position_id_counts(self) except *: - # For the internal position identifier generator - cdef list positions = self.cache.positions() - - # Count positions per instrument_id - cdef dict counts = {} # type: dict[StrategyId, int] - cdef int count - cdef Position position - for position in positions: - count = counts.get(position.strategy_id, 0) - count += 1 - # noinspection PyUnresolvedReferences - counts[position.strategy_id] = count - - # Reset position identifier generator - self._pos_id_generator.reset() - - # Set counts - cdef StrategyId strategy_id - for strategy_id, count in counts.items(): - self._pos_id_generator.set_count(strategy_id, count) - self._log.info(f"Set PositionId count for {repr(strategy_id)} to {count}.") diff --git a/nautilus_trader/indicators/fuzzy_enums/candle_body.pxd b/nautilus_trader/indicators/fuzzy_enums/candle_body.pxd index 79a60dd10cbf..7fd26df2a4f2 100644 --- a/nautilus_trader/indicators/fuzzy_enums/candle_body.pxd +++ b/nautilus_trader/indicators/fuzzy_enums/candle_body.pxd @@ -15,8 +15,8 @@ cpdef enum CandleBodySize: - NONE = 0 # Doji - SMALL = 1 - MEDIUM = 2 - LARGE = 3 - TREND = 4 + NONE = 0, # Doji + SMALL = 1, + MEDIUM = 2, + LARGE = 3, + TREND = 4, diff --git a/nautilus_trader/indicators/fuzzy_enums/candle_direction.pxd b/nautilus_trader/indicators/fuzzy_enums/candle_direction.pxd index 52fe684406e8..45df08b8133e 100644 --- a/nautilus_trader/indicators/fuzzy_enums/candle_direction.pxd +++ b/nautilus_trader/indicators/fuzzy_enums/candle_direction.pxd @@ -15,6 +15,6 @@ cpdef enum CandleDirection: - BULL = 1 - NONE = 0 # Doji - BEAR = -1 + BULL = 1, + NONE = 0, # Doji + BEAR = -1, diff --git a/nautilus_trader/indicators/fuzzy_enums/candle_size.pxd b/nautilus_trader/indicators/fuzzy_enums/candle_size.pxd index 682f7ef8e7f1..3d2a130a3bb0 100644 --- a/nautilus_trader/indicators/fuzzy_enums/candle_size.pxd +++ b/nautilus_trader/indicators/fuzzy_enums/candle_size.pxd @@ -15,10 +15,10 @@ cpdef enum CandleSize: - NONE = 0 # Doji - VERY_SMALL = 1 - SMALL = 2 - MEDIUM = 3 - LARGE = 4 - VERY_LARGE = 5 - EXTREMELY_LARGE = 6 + NONE = 0, # Doji + VERY_SMALL = 1, + SMALL = 2, + MEDIUM = 3, + LARGE = 4, + VERY_LARGE = 5, + EXTREMELY_LARGE = 6, diff --git a/nautilus_trader/indicators/fuzzy_enums/candle_wick.pxd b/nautilus_trader/indicators/fuzzy_enums/candle_wick.pxd index d19eae7aa6b0..b1b330503f68 100644 --- a/nautilus_trader/indicators/fuzzy_enums/candle_wick.pxd +++ b/nautilus_trader/indicators/fuzzy_enums/candle_wick.pxd @@ -15,7 +15,7 @@ cpdef enum CandleWickSize: - NONE = 0 # No candle wick - SMALL = 1 - MEDIUM = 2 - LARGE = 3 + NONE = 0, # No candle wick + SMALL = 1, + MEDIUM = 2, + LARGE = 3, diff --git a/nautilus_trader/indicators/roc.pyx b/nautilus_trader/indicators/roc.pyx index 74ded21f7904..75140626078f 100644 --- a/nautilus_trader/indicators/roc.pyx +++ b/nautilus_trader/indicators/roc.pyx @@ -45,8 +45,8 @@ cdef class RateOfChange(Indicator): """ Condition.true(period > 1, "period was <= 1") - super().__init__(params=[period]) + self.period = period self._use_log = use_log self._prices = deque(maxlen=period) diff --git a/nautilus_trader/live/data_engine.pxd b/nautilus_trader/live/data_engine.pxd index 1736bd364033..1758f666b875 100644 --- a/nautilus_trader/live/data_engine.pxd +++ b/nautilus_trader/live/data_engine.pxd @@ -25,8 +25,9 @@ cdef class LiveDataEngine(DataEngine): cdef readonly bint is_running - cpdef void kill(self) except * cpdef object get_event_loop(self) cpdef object get_run_queue_task(self) cpdef int data_qsize(self) except * cpdef int message_qsize(self) except * + + cpdef void kill(self) except * diff --git a/nautilus_trader/live/data_engine.pyx b/nautilus_trader/live/data_engine.pyx index b2dc2638f322..bd86bbaebfdb 100644 --- a/nautilus_trader/live/data_engine.pyx +++ b/nautilus_trader/live/data_engine.pyx @@ -160,7 +160,7 @@ cdef class LiveDataEngine(DataEngine): except QueueFull: self._log.warning(f"Blocking on `_message_queue.put` as message_queue full at " f"{self._message_queue.qsize()} items.") - self._message_queue.put(command) # Block until qsize reduces below maxsize + self._loop.create_task(self._message_queue.put(command)) cpdef void process(self, data) except *: """ @@ -188,7 +188,7 @@ cdef class LiveDataEngine(DataEngine): except asyncio.QueueFull: self._log.warning(f"Blocking on `_data_queue.put` as data_queue full at " f"{self._data_queue.qsize()} items.") - self._data_queue.put(data) # Block until qsize reduces below maxsize + self._loop.create_task(self._data_queue.put(data)) cpdef void send(self, DataRequest request) except *: """ @@ -216,7 +216,7 @@ cdef class LiveDataEngine(DataEngine): except asyncio.QueueFull: self._log.warning(f"Blocking on `_message_queue.put` as message_queue full at " f"{self._message_queue.qsize()} items.") - self._message_queue.put(request) # Block until qsize reduces below maxsize + self._loop.create_task(self._message_queue.put(request)) cpdef void receive(self, DataResponse response) except *: """ diff --git a/nautilus_trader/live/execution_client.pxd b/nautilus_trader/live/execution_client.pxd index 0a79707af075..06f21a6bf0f2 100644 --- a/nautilus_trader/live/execution_client.pxd +++ b/nautilus_trader/live/execution_client.pxd @@ -17,8 +17,8 @@ from decimal import Decimal from cpython.datetime cimport datetime +from nautilus_trader.common.providers cimport InstrumentProvider from nautilus_trader.execution.client cimport ExecutionClient -from nautilus_trader.live.providers cimport InstrumentProvider from nautilus_trader.model.c_enums.liquidity_side cimport LiquiditySide from nautilus_trader.model.c_enums.order_side cimport OrderSide from nautilus_trader.model.identifiers cimport ClientOrderId diff --git a/nautilus_trader/live/execution_client.pyx b/nautilus_trader/live/execution_client.pyx index dfb1cbb2aee0..801a5eb34037 100644 --- a/nautilus_trader/live/execution_client.pyx +++ b/nautilus_trader/live/execution_client.pyx @@ -18,9 +18,9 @@ from decimal import Decimal from nautilus_trader.common.clock cimport LiveClock from nautilus_trader.common.logging cimport Logger +from nautilus_trader.common.providers cimport InstrumentProvider from nautilus_trader.execution.client cimport ExecutionClient from nautilus_trader.live.execution_engine cimport LiveExecutionEngine -from nautilus_trader.live.providers cimport InstrumentProvider from nautilus_trader.model.c_enums.liquidity_side cimport LiquiditySide from nautilus_trader.model.c_enums.order_side cimport OrderSide from nautilus_trader.model.currency cimport Currency @@ -214,7 +214,7 @@ cdef class LiveExecutionClient(ExecutionClient): LiquiditySide liquidity_side, datetime timestamp ) except *: - cdef Instrument instrument = self._instrument_provider.get(instrument_id) + cdef Instrument instrument = self._instrument_provider.find_c(instrument_id) if instrument is None: self._log.error(f"Cannot fill order with {repr(order_id)}, " f"instrument for {instrument_id} not found.") diff --git a/nautilus_trader/live/execution_engine.pxd b/nautilus_trader/live/execution_engine.pxd index 06f4da87afce..4240a8f60bb0 100644 --- a/nautilus_trader/live/execution_engine.pxd +++ b/nautilus_trader/live/execution_engine.pxd @@ -23,7 +23,8 @@ cdef class LiveExecutionEngine(ExecutionEngine): cdef readonly bint is_running - cpdef void kill(self) except * cpdef object get_event_loop(self) cpdef object get_run_queue_task(self) cpdef int qsize(self) except * + + cpdef void kill(self) except * diff --git a/nautilus_trader/live/execution_engine.pyx b/nautilus_trader/live/execution_engine.pyx index 649ba2ceea88..0dbd21fbe8bc 100644 --- a/nautilus_trader/live/execution_engine.pyx +++ b/nautilus_trader/live/execution_engine.pyx @@ -230,7 +230,7 @@ cdef class LiveExecutionEngine(ExecutionEngine): self._queue.put_nowait(command) except asyncio.QueueFull: self._log.warning(f"Blocking on `_queue.put` as queue full at {self._queue.qsize()} items.") - self._queue.put(command) # Block until qsize reduces below maxsize + self._loop.create_task(self._queue.put(command)) cpdef void process(self, Event event) except *: """ @@ -257,7 +257,7 @@ cdef class LiveExecutionEngine(ExecutionEngine): self._queue.put_nowait(event) except asyncio.QueueFull: self._log.warning(f"Blocking on `_queue.put` as queue full at {self._queue.qsize()} items.") - self._queue.put(event) # Block until qsize reduces below maxsize + self._loop.create_task(self._queue.put(event)) cpdef void _on_start(self) except *: if not self._loop.is_running(): diff --git a/nautilus_trader/live/node.py b/nautilus_trader/live/node.py index 7233e0af73b6..c32c281e22fd 100644 --- a/nautilus_trader/live/node.py +++ b/nautilus_trader/live/node.py @@ -40,6 +40,7 @@ from nautilus_trader.execution.database import BypassExecutionDatabase from nautilus_trader.live.data_engine import LiveDataEngine from nautilus_trader.live.execution_engine import LiveExecutionEngine +from nautilus_trader.live.risk_engine import LiveRiskEngine from nautilus_trader.model.identifiers import TraderId from nautilus_trader.redis.execution import RedisExecutionDatabase from nautilus_trader.serialization.serializers import MsgPackCommandSerializer @@ -95,6 +96,7 @@ def __init__( config_system = config.get("system", {}) config_log = config.get("logging", {}) config_exec_db = config.get("exec_database", {}) + config_risk = config.get("risk", {}) config_strategy = config.get("strategy", {}) config_adapters = config.get("adapters", {}) @@ -177,7 +179,17 @@ def __init__( config={"qsize": 10000}, ) + self._risk_engine = LiveRiskEngine( + loop=self._loop, + exec_engine=self._exec_engine, + portfolio=self.portfolio, + clock=self._clock, + logger=self._logger, + config=config_risk, + ) + self._exec_engine.load_cache() + self._exec_engine.register_risk_engine(self._risk_engine) self._setup_adapters(config_adapters, self._logger) self.trader = Trader( @@ -186,6 +198,7 @@ def __init__( portfolio=self.portfolio, data_engine=self._data_engine, exec_engine=self._exec_engine, + risk_engine=self._risk_engine, clock=self._clock, logger=self._logger, ) @@ -288,10 +301,12 @@ def dispose(self) -> None: self._log.debug(f"{self._data_engine.get_run_queue_task()}") self._log.debug(f"{self._exec_engine.get_run_queue_task()}") + self._log.debug(f"{self._risk_engine.get_run_queue_task()}") self.trader.dispose() self._data_engine.dispose() self._exec_engine.dispose() + self._risk_engine.dispose() self._log.info("Shutting down executor...") if sys.version_info >= (3, 9): @@ -412,6 +427,7 @@ def _setup_adapters(self, config: Dict[str, object], logger: LiveLogger) -> None if exec_client is not None: self._exec_engine.register_client(exec_client) + # Automatically registers with the risk engine async def _run(self) -> None: try: @@ -420,6 +436,7 @@ async def _run(self) -> None: self._data_engine.start() self._exec_engine.start() + self._risk_engine.start() result: bool = await self._await_engines_connected() if not result: @@ -439,6 +456,7 @@ async def _run(self) -> None: # Continue to run while engines are running... await self._data_engine.get_run_queue_task() await self._exec_engine.get_run_queue_task() + await self._risk_engine.get_run_queue_task() except asyncio.CancelledError as ex: self._log.error(str(ex)) @@ -483,6 +501,8 @@ async def _stop(self) -> None: self._data_engine.stop() if self._exec_engine.state == ComponentState.RUNNING: self._exec_engine.stop() + if self._risk_engine.state == ComponentState.RUNNING: + self._risk_engine.stop() await self._await_engines_disconnected() diff --git a/nautilus_trader/live/risk_engine.pxd b/nautilus_trader/live/risk_engine.pxd new file mode 100644 index 000000000000..87ab832c18aa --- /dev/null +++ b/nautilus_trader/live/risk_engine.pxd @@ -0,0 +1,30 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +from nautilus_trader.risk.engine cimport RiskEngine + + +cdef class LiveRiskEngine(RiskEngine): + cdef object _loop + cdef object _queue + cdef object _run_queue_task + + cdef readonly bint is_running + + cpdef object get_event_loop(self) + cpdef object get_run_queue_task(self) + cpdef int qsize(self) except * + + cpdef void kill(self) except * diff --git a/nautilus_trader/live/risk_engine.pyx b/nautilus_trader/live/risk_engine.pyx new file mode 100644 index 000000000000..31532f0ef7b9 --- /dev/null +++ b/nautilus_trader/live/risk_engine.pyx @@ -0,0 +1,216 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +import asyncio +from asyncio import AbstractEventLoop +from asyncio import CancelledError + +from nautilus_trader.common.clock cimport LiveClock +from nautilus_trader.common.logging cimport Logger +from nautilus_trader.common.queue cimport Queue +from nautilus_trader.core.correctness cimport Condition +from nautilus_trader.core.message cimport Command +from nautilus_trader.core.message cimport Message +from nautilus_trader.core.message cimport MessageType +from nautilus_trader.execution.engine cimport ExecutionEngine +from nautilus_trader.model.events cimport Event +from nautilus_trader.trading.portfolio cimport Portfolio + + +cdef class LiveRiskEngine(RiskEngine): + """ + Provides a high-performance asynchronous live risk engine. + """ + + def __init__( + self, + loop not None: AbstractEventLoop, + ExecutionEngine exec_engine not None, + Portfolio portfolio not None, + LiveClock clock not None, + Logger logger not None, + dict config=None, + ): + """ + Initialize a new instance of the `LiveRiskEngine` class. + + Parameters + ---------- + loop : asyncio.AbstractEventLoop + The event loop for the engine. + portfolio : Portfolio + The portfolio for the engine. + clock : Clock + The clock for the engine. + logger : Logger + The logger for the engine. + config : dict[str, object], optional + The configuration options. + + """ + if config is None: + config = {} + super().__init__( + exec_engine, + portfolio, + clock, + logger, + config, + ) + + self._loop = loop + self._queue = Queue(maxsize=config.get("qsize", 10000)) + + self._run_queue_task = None + self.is_running = False + + cpdef object get_event_loop(self): + """ + Return the internal event loop for the engine. + + Returns + ------- + asyncio.AbstractEventLoop + + """ + return self._loop + + cpdef object get_run_queue_task(self): + """ + Return the internal run queue task for the engine. + + Returns + ------- + asyncio.Task + + """ + return self._run_queue_task + + cpdef int qsize(self) except *: + """ + Return the number of messages buffered on the internal queue. + + Returns + ------- + int + + """ + return self._queue.qsize() + +# -- COMMANDS -------------------------------------------------------------------------------------- + + cpdef void kill(self) except *: + """ + Kill the engine by abruptly cancelling the queue task and calling stop. + """ + self._log.warning("Killing engine...") + if self._run_queue_task: + self._log.debug("Cancelling run_queue_task...") + self._run_queue_task.cancel() + if self.is_running: + self.is_running = False # Avoids sentinel messages for queues + self.stop() + + cpdef void execute(self, Command command) except *: + """ + Execute the given command. + + If the internal queue is already full then will log a warning and block + until queue size reduces. + + Parameters + ---------- + command : Command + The command to execute. + + Warnings + -------- + This method should only be called from the same thread the event loop is + running on. + + """ + Condition.not_none(command, "command") + # Do not allow None through (None is a sentinel value which stops the queue) + + try: + self._queue.put_nowait(command) + except asyncio.QueueFull: + self._log.warning(f"Blocking on `_queue.put` as queue full at {self._queue.qsize()} items.") + self._loop.create_task(self._queue.put(command)) + + cpdef void process(self, Event event) except *: + """ + Process the given event. + + If the internal queue is already full then will log a warning and block + until queue size reduces. + + Parameters + ---------- + event : Event + The event to process. + + Warnings + -------- + This method should only be called from the same thread the event loop is + running on. + + """ + Condition.not_none(event, "event") + # Do not allow None through (None is a sentinel value which stops the queue) + + try: + self._queue.put_nowait(event) + except asyncio.QueueFull: + self._log.warning(f"Blocking on `_queue.put` as queue full at {self._queue.qsize()} items.") + self._queue.put(event) # Block until qsize reduces below maxsize + +# -- INTERNAL -------------------------------------------------------------------------------------- + + cpdef void _on_start(self) except *: + if not self._loop.is_running(): + self._log.warning("Started when loop is not running.") + + self.is_running = True # Queue will continue to process + self._run_queue_task = self._loop.create_task(self._run()) + + self._log.debug(f"Scheduled {self._run_queue_task}") + + cpdef void _on_stop(self) except *: + if self.is_running: + self.is_running = False + self._queue.put_nowait(None) # Sentinel message pattern + self._log.debug(f"Sentinel message placed on message queue.") + + async def _run(self): + self._log.debug(f"Message queue processing starting (qsize={self.qsize()})...") + cdef Message message + try: + while self.is_running: + message = await self._queue.get() + if message is None: # Sentinel message (fast C-level check) + continue # Returns to the top to check `self.is_running` + if message.type == MessageType.EVENT: + self._handle_event(message) + elif message.type == MessageType.COMMAND: + self._execute_command(message) + else: + self._log.error(f"Cannot handle message: unrecognized {message}.") + except CancelledError: + if self.qsize() > 0: + self._log.warning(f"Running cancelled " + f"with {self.qsize()} message(s) on queue.") + else: + self._log.debug(f"Message queue processing stopped (qsize={self.qsize()}).") diff --git a/nautilus_trader/model/bar.pxd b/nautilus_trader/model/bar.pxd index 7ad82cdbe8f0..5ecadc829245 100644 --- a/nautilus_trader/model/bar.pxd +++ b/nautilus_trader/model/bar.pxd @@ -17,6 +17,7 @@ from cpython.datetime cimport datetime from nautilus_trader.model.c_enums.bar_aggregation cimport BarAggregation from nautilus_trader.model.c_enums.price_type cimport PriceType +from nautilus_trader.model.data cimport Data from nautilus_trader.model.identifiers cimport InstrumentId from nautilus_trader.model.identifiers cimport Symbol from nautilus_trader.model.identifiers cimport Venue @@ -56,7 +57,7 @@ cdef class BarType: cpdef str to_serializable_str(self) -cdef class Bar: +cdef class Bar(Data): cdef readonly Price open """The open price of the bar.\n\n:returns: `Price`""" cdef readonly Price high @@ -67,8 +68,6 @@ cdef class Bar: """The close price of the bar.\n\n:returns: `Price`""" cdef readonly Quantity volume """The volume of the bar.\n\n:returns: `Quantity`""" - cdef readonly datetime timestamp - """The timestamp the bar closed at (UTC).\n\n:returns: `datetime`""" cdef readonly bint checked """If the input values were integrity checked.\n\n:returns: `bool`""" diff --git a/nautilus_trader/model/bar.pyx b/nautilus_trader/model/bar.pyx index a9bb239c978c..fb096d2a6621 100644 --- a/nautilus_trader/model/bar.pyx +++ b/nautilus_trader/model/bar.pyx @@ -23,6 +23,7 @@ from nautilus_trader.model.c_enums.bar_aggregation cimport BarAggregation from nautilus_trader.model.c_enums.bar_aggregation cimport BarAggregationParser from nautilus_trader.model.c_enums.price_type cimport PriceType from nautilus_trader.model.c_enums.price_type cimport PriceTypeParser +from nautilus_trader.model.data cimport Data from nautilus_trader.model.identifiers cimport InstrumentId from nautilus_trader.model.objects cimport Price from nautilus_trader.model.objects cimport Quantity @@ -252,7 +253,7 @@ cdef class BarType: if len(pieces) != 4: raise ValueError(f"The BarType string value was malformed, was {value}") - cdef InstrumentId instrument_id = InstrumentId.from_serializable_str_c(pieces[0]) + cdef InstrumentId instrument_id = InstrumentId.from_str_c(pieces[0]) cdef BarSpecification bar_spec = BarSpecification( int(pieces[1]), BarAggregationParser.from_str(pieces[2]), @@ -294,10 +295,10 @@ cdef class BarType: str """ - return f"{self.instrument_id.to_serializable_str()}-{self.spec}" + return f"{self.instrument_id}-{self.spec}" -cdef class Bar: +cdef class Bar(Data): """ Represents an aggregated bar. """ @@ -328,7 +329,7 @@ cdef class Bar: volume : Quantity The bars volume. timestamp : datetime - The bars timestamp (UTC). + The timestamp the bar closed at (UTC). check : bool If bar parameters should be checked valid. @@ -346,13 +347,13 @@ cdef class Bar: Condition.true(high_price >= low_price, 'high_price was < low_price') Condition.true(high_price >= close_price, 'high_price was < close_price') Condition.true(low_price <= close_price, 'low_price was > close_price') + super().__init__(timestamp) self.open = open_price self.high = high_price self.low = low_price self.close = close_price self.volume = volume - self.timestamp = timestamp self.checked = check def __eq__(self, Bar other) -> bool: diff --git a/nautilus_trader/model/c_enums/asset_class.pxd b/nautilus_trader/model/c_enums/asset_class.pxd index cf6d1349559b..03bcb611a452 100644 --- a/nautilus_trader/model/c_enums/asset_class.pxd +++ b/nautilus_trader/model/c_enums/asset_class.pxd @@ -22,7 +22,7 @@ cpdef enum AssetClass: BOND = 4, INDEX = 5, CRYPTO = 6, - BETTING = 7 + BETTING = 7, cdef class AssetClassParser: diff --git a/nautilus_trader/model/c_enums/asset_type.pxd b/nautilus_trader/model/c_enums/asset_type.pxd index 0749e5d937c4..62cecbce712b 100644 --- a/nautilus_trader/model/c_enums/asset_type.pxd +++ b/nautilus_trader/model/c_enums/asset_type.pxd @@ -22,7 +22,7 @@ cpdef enum AssetType: FORWARD = 4, CFD = 5, OPTION = 6, - WARRANT = 7 + WARRANT = 7, cdef class AssetTypeParser: diff --git a/nautilus_trader/model/c_enums/oms_type.pxd b/nautilus_trader/model/c_enums/oms_type.pxd index 751cc8409248..7469244983c3 100644 --- a/nautilus_trader/model/c_enums/oms_type.pxd +++ b/nautilus_trader/model/c_enums/oms_type.pxd @@ -17,7 +17,7 @@ cpdef enum OMSType: UNDEFINED = 0, # Invalid value NETTING = 1, - HEDGING = 2 + HEDGING = 2, cdef class OMSTypeParser: diff --git a/nautilus_trader/model/c_enums/order_side.pxd b/nautilus_trader/model/c_enums/order_side.pxd index cb53521038be..fc4977731629 100644 --- a/nautilus_trader/model/c_enums/order_side.pxd +++ b/nautilus_trader/model/c_enums/order_side.pxd @@ -17,7 +17,7 @@ cpdef enum OrderSide: UNDEFINED = 0, # Invalid value BUY = 1, - SELL = 2 + SELL = 2, cdef class OrderSideParser: diff --git a/nautilus_trader/model/c_enums/order_state.pxd b/nautilus_trader/model/c_enums/order_state.pxd index 8d6ea8904487..80c06392c216 100644 --- a/nautilus_trader/model/c_enums/order_state.pxd +++ b/nautilus_trader/model/c_enums/order_state.pxd @@ -26,7 +26,7 @@ cpdef enum OrderState: EXPIRED = 8, TRIGGERED = 9, PARTIALLY_FILLED = 10, - FILLED = 11 + FILLED = 11, cdef class OrderStateParser: diff --git a/nautilus_trader/model/c_enums/position_side.pxd b/nautilus_trader/model/c_enums/position_side.pxd index ab7b5795035a..d14e4cb11b6c 100644 --- a/nautilus_trader/model/c_enums/position_side.pxd +++ b/nautilus_trader/model/c_enums/position_side.pxd @@ -18,7 +18,7 @@ cpdef enum PositionSide: UNDEFINED = 0, # Invalid value FLAT = 1, LONG = 2, - SHORT = 3 + SHORT = 3, cdef class PositionSideParser: diff --git a/nautilus_trader/model/c_enums/price_type.pxd b/nautilus_trader/model/c_enums/price_type.pxd index 3ab79de48501..4d25b8636563 100644 --- a/nautilus_trader/model/c_enums/price_type.pxd +++ b/nautilus_trader/model/c_enums/price_type.pxd @@ -19,7 +19,7 @@ cpdef enum PriceType: BID = 1, ASK = 2, MID = 3, - LAST = 4 + LAST = 4, cdef class PriceTypeParser: diff --git a/nautilus_trader/model/c_enums/time_in_force.pxd b/nautilus_trader/model/c_enums/time_in_force.pxd index f43dd7be1a13..a770393688c0 100644 --- a/nautilus_trader/model/c_enums/time_in_force.pxd +++ b/nautilus_trader/model/c_enums/time_in_force.pxd @@ -20,7 +20,7 @@ cpdef enum TimeInForce: GTC = 2, IOC = 3, FOK = 4, - GTD = 5 + GTD = 5, cdef class TimeInForceParser: diff --git a/nautilus_trader/model/commands.pxd b/nautilus_trader/model/commands.pxd index db99ba3971ce..dc50632f2299 100644 --- a/nautilus_trader/model/commands.pxd +++ b/nautilus_trader/model/commands.pxd @@ -43,10 +43,6 @@ cdef class SubmitOrder(TradingCommand): """The position identifier associated with the command.\n\n:returns: `PositionId`""" cdef readonly Order order """The order for the command.\n\n:returns: `Order`""" - cdef readonly bint approved - """If the order has been risk approved.\n\n:returns: `bool`""" - - cdef void approve(self) except * cdef class SubmitBracketOrder(TradingCommand): @@ -58,10 +54,6 @@ cdef class SubmitBracketOrder(TradingCommand): """The strategy identifier associated with the command.\n\n:returns: `StrategyId`""" cdef readonly BracketOrder bracket_order """The bracket order to submit.\n\n:returns: `BracketOrder`""" - cdef readonly bint approved - """If the bracket order has been risk approved.\n\n:returns: `bool`""" - - cdef void approve(self) except * cdef class CancelOrder(TradingCommand): diff --git a/nautilus_trader/model/commands.pyx b/nautilus_trader/model/commands.pyx index f4302c40fdaa..5c8253c2179a 100644 --- a/nautilus_trader/model/commands.pyx +++ b/nautilus_trader/model/commands.pyx @@ -109,11 +109,6 @@ cdef class SubmitOrder(TradingCommand): self.strategy_id = strategy_id self.position_id = position_id self.order = order - self.approved = False - - cdef void approve(self) except *: - # C-only access for approving the sending of the order. - self.approved = True def __repr__(self) -> str: cdef str position_id_str = '' if self.position_id.is_null() else f"position_id={self.position_id.value}, " @@ -175,11 +170,6 @@ cdef class SubmitBracketOrder(TradingCommand): self.account_id = account_id self.strategy_id = strategy_id self.bracket_order = bracket_order - self.approved = False - - cdef void approve(self) except *: - # C-only access for approving the sending of the order. - self.approved = True def __repr__(self) -> str: return (f"{type(self).__name__}(" diff --git a/nautilus_trader/model/currencies.pxd b/nautilus_trader/model/currencies.pxd new file mode 100644 index 000000000000..0f60c4ea7572 --- /dev/null +++ b/nautilus_trader/model/currencies.pxd @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +cdef dict _CURRENCY_MAP diff --git a/nautilus_trader/model/currencies.pyx b/nautilus_trader/model/currencies.pyx index d9233774a428..e1b933764184 100644 --- a/nautilus_trader/model/currencies.pyx +++ b/nautilus_trader/model/currencies.pyx @@ -16,17 +16,110 @@ from nautilus_trader.model.c_enums.currency_type cimport CurrencyType from nautilus_trader.model.currency cimport Currency -BTC = Currency("BTC", precision=8, currency_type=CurrencyType.CRYPTO) -ETH = Currency("ETH", precision=8, currency_type=CurrencyType.CRYPTO) -XRP = Currency("XRP", precision=8, currency_type=CurrencyType.CRYPTO) -USDT = Currency("USDT", precision=8, currency_type=CurrencyType.CRYPTO) -AUD = Currency("AUD", precision=2, currency_type=CurrencyType.FIAT) -USD = Currency("USD", precision=2, currency_type=CurrencyType.FIAT) -CAD = Currency("CAD", precision=2, currency_type=CurrencyType.FIAT) -EUR = Currency("EUR", precision=2, currency_type=CurrencyType.FIAT) -GBP = Currency("GBP", precision=2, currency_type=CurrencyType.FIAT) -CHF = Currency("CHF", precision=2, currency_type=CurrencyType.FIAT) -HKD = Currency("HKD", precision=2, currency_type=CurrencyType.FIAT) -NZD = Currency("NZD", precision=2, currency_type=CurrencyType.FIAT) -SGD = Currency("SGD", precision=2, currency_type=CurrencyType.FIAT) -JPY = Currency("JPY", precision=2, currency_type=CurrencyType.FIAT) +# Fiat currencies +AUD = Currency("AUD", precision=2, iso4217=036, name="Australian dollar", currency_type=CurrencyType.FIAT) +BRL = Currency("BRL", precision=2, iso4217=986, name="Brazilian real", currency_type=CurrencyType.FIAT) +CAD = Currency("CAD", precision=2, iso4217=124, name="Canadian dollar", currency_type=CurrencyType.FIAT) +CHF = Currency("CHF", precision=2, iso4217=756, name="Swiss franc", currency_type=CurrencyType.FIAT) +CNY = Currency("CNY", precision=2, iso4217=156, name="Chinese yuan", currency_type=CurrencyType.FIAT) +CNH = Currency("CNH", precision=2, iso4217=0, name="Chinese yuan (offshore)", currency_type=CurrencyType.FIAT) +CZK = Currency("CZK", precision=2, iso4217=203, name="Czech koruna", currency_type=CurrencyType.FIAT) +DKK = Currency("DKK", precision=2, iso4217=208, name="Danish krone", currency_type=CurrencyType.FIAT) +EUR = Currency("EUR", precision=2, iso4217=978, name="Euro", currency_type=CurrencyType.FIAT) +GBP = Currency("GBP", precision=2, iso4217=826, name="British Pound", currency_type=CurrencyType.FIAT) +HKD = Currency("HKD", precision=2, iso4217=344, name="Hong Kong dollar", currency_type=CurrencyType.FIAT) +HUF = Currency("HUF", precision=2, iso4217=348, name="Hungarian forint", currency_type=CurrencyType.FIAT) +ILS = Currency("ILS", precision=2, iso4217=376, name="Israeli new shekel", currency_type=CurrencyType.FIAT) +INR = Currency("INR", precision=2, iso4217=356, name="Indian rupee", currency_type=CurrencyType.FIAT) +JPY = Currency("JPY", precision=0, iso4217=392, name="Japanese yen", currency_type=CurrencyType.FIAT) +KRW = Currency("KRW", precision=0, iso4217=410, name="South Korean won", currency_type=CurrencyType.FIAT) +MXN = Currency("MXN", precision=2, iso4217=484, name="Mexican peso", currency_type=CurrencyType.FIAT) +NOK = Currency("NOK", precision=2, iso4217=578, name="Norwegian krone", currency_type=CurrencyType.FIAT) +NZD = Currency("NZD", precision=2, iso4217=554, name="New Zealand dollar", currency_type=CurrencyType.FIAT) +PLN = Currency("PLN", precision=2, iso4217=985, name="Polish złoty", currency_type=CurrencyType.FIAT) +RUB = Currency("RUB", precision=2, iso4217=643, name="Russian ruble", currency_type=CurrencyType.FIAT) +SAR = Currency("SAR", precision=2, iso4217=682, name="Saudi riyal", currency_type=CurrencyType.FIAT) +SEK = Currency("SEK", precision=2, iso4217=752, name="Swedish krona/kronor", currency_type=CurrencyType.FIAT) +SGD = Currency("SGD", precision=2, iso4217=702, name="Singapore dollar", currency_type=CurrencyType.FIAT) +THB = Currency("THB", precision=2, iso4217=764, name="Thai baht", currency_type=CurrencyType.FIAT) +TRY = Currency("TRY", precision=2, iso4217=949, name="Turkish lira", currency_type=CurrencyType.FIAT) +USD = Currency("USD", precision=2, iso4217=840, name="United States dollar", currency_type=CurrencyType.FIAT) +XAG = Currency("XAG", precision=0, iso4217=961, name="Silver (one troy ounce)", currency_type=CurrencyType.FIAT) +XAU = Currency("XAU", precision=0, iso4217=959, name="Gold (one troy ounce)", currency_type=CurrencyType.FIAT) +ZAR = Currency("ZAR", precision=2, iso4217=710, name="South African rand", currency_type=CurrencyType.FIAT) + +# Crypto currencies +ADA = Currency("ADA", precision=6, iso4217=0, name="Cardano", currency_type=CurrencyType.CRYPTO) +BCH = Currency("BCH", precision=8, iso4217=0, name="Bitcoin Cash", currency_type=CurrencyType.CRYPTO) +BNB = Currency("BNB", precision=8, iso4217=0, name="Binance Coin", currency_type=CurrencyType.CRYPTO) +BSV = Currency("BSV", precision=8, iso4217=0, name="Bitcoin SV", currency_type=CurrencyType.CRYPTO) +BTC = Currency("BTC", precision=8, iso4217=0, name="Bitcoin", currency_type=CurrencyType.CRYPTO) +XBT = Currency("XBT", precision=8, iso4217=0, name="Bitcoin", currency_type=CurrencyType.CRYPTO) +DASH = Currency("DASH", precision=8, iso4217=0, name="Dash", currency_type=CurrencyType.CRYPTO) +DOT = Currency("DOT", precision=8, iso4217=0, name="Polkadot", currency_type=CurrencyType.CRYPTO) +EOS = Currency("EOS", precision=8, iso4217=0, name="EOS", currency_type=CurrencyType.CRYPTO) +ETH = Currency("ETH", precision=8, iso4217=0, name="Ether", currency_type=CurrencyType.CRYPTO) # Precision 18 +LINK = Currency("LINK", precision=8, iso4217=0, name="Chainlink", currency_type=CurrencyType.CRYPTO) +LTC = Currency("LTC", precision=8, iso4217=0, name="Litecoin", currency_type=CurrencyType.CRYPTO) +VTC = Currency("VTC", precision=8, iso4217=0, name="Vertcoin", currency_type=CurrencyType.CRYPTO) +XLM = Currency("XLM", precision=8, iso4217=0, name="Stellar Lumen", currency_type=CurrencyType.CRYPTO) +XMR = Currency("XMR", precision=12, iso4217=0, name="Monero", currency_type=CurrencyType.CRYPTO) +XRP = Currency("XRP", precision=6, iso4217=0, name="Ripple", currency_type=CurrencyType.CRYPTO) +XTZ = Currency("XTZ", precision=6, iso4217=0, name="Tezos", currency_type=CurrencyType.CRYPTO) +USDT = Currency("USDT", precision=8, iso4217=0, name="Tether", currency_type=CurrencyType.CRYPTO) +ZEC = Currency("ZEC", precision=8, iso4217=0, name="Zcash", currency_type=CurrencyType.CRYPTO) + + +_CURRENCY_MAP = { + # Fiat currencies + "AUD": AUD, + "BRL": BRL, + "CAD": CAD, + "CHF": CHF, + "CNY": CNY, + "CNH": CNH, + "CZK": CZK, + "DKK": DKK, + "EUR": EUR, + "GBP": GBP, + "HKD": HKD, + "HUF": HUF, + "ILS": ILS, + "INR": INR, + "JPY": JPY, + "KRW": KRW, + "MXN": MXN, + "NOK": NOK, + "NZD": NZD, + "PLN": PLN, + "RUB": RUB, + "SAR": SAR, + "SEK": SEK, + "SGD": SGD, + "THB": THB, + "TRY": TRY, + "USD": USD, + "XAG": XAG, + "XAU": XAU, + "ZAR": ZAR, + # Crypto currencies + "ADA": ADA, + "BCH": BCH, + "BNB": BNB, + "BSV": BSV, + "BTC": BTC, + "XBT": XBT, + "DASH": DASH, + "DOT": DOT, + "EOS": EOS, + "ETH": ETH, + "LINK": LINK, + "LTC": LTC, + "VTC": VTC, + "XLM": XLM, + "XMR": XMR, + "XRP": XRP, + "XTZ": XTZ, + "USDT": USDT, + "ZEC": ZEC, +} diff --git a/nautilus_trader/model/currency.pxd b/nautilus_trader/model/currency.pxd index ebc7455428f1..f08489810f76 100644 --- a/nautilus_trader/model/currency.pxd +++ b/nautilus_trader/model/currency.pxd @@ -16,49 +16,23 @@ from nautilus_trader.model.c_enums.currency_type cimport CurrencyType -# Crypto currencies -cdef Currency BTC -cdef Currency ETH -cdef Currency USDT -cdef Currency XRP -cdef Currency BCH -cdef Currency BNB -cdef Currency DOT -cdef Currency LINK -cdef Currency LTC - -# Fiat currencies -cdef Currency AUD -cdef Currency CAD -cdef Currency CHF -cdef Currency CNY -cdef Currency CNH -cdef Currency CZK -cdef Currency EUR -cdef Currency GBP -cdef Currency HKD -cdef Currency JPY -cdef Currency MXN -cdef Currency NOK -cdef Currency NZD -cdef Currency RUB -cdef Currency SEK -cdef Currency TRY -cdef Currency SGD -cdef Currency USD -cdef Currency ZAR - - cdef class Currency: cdef readonly str code - """The identifier code of the currency.\n\n:returns: `str`""" + """The currency identifier code.\n\n:returns: `str`""" cdef readonly int precision - """The specified precision of the currency.\n\n:returns: `int`""" + """The currency decimal precision.\n\n:returns: `int`""" + cdef readonly int iso4217 + """The currency ISO 4217 code.\n\n:returns: `int`""" + cdef readonly str name + """The currency name.\n\n:returns: `str`""" cdef readonly CurrencyType currency_type - """The general type of the currency.\n\n:returns: `CurrencyType` (Enum)""" + """The currency type (FIAT or CRYPTO).\n\n:returns: `CurrencyType` (Enum)""" @staticmethod cdef Currency from_str_c(str code) @staticmethod cdef bint is_fiat_c(str code) + + @staticmethod + cdef bint is_crypto_c(str code) diff --git a/nautilus_trader/model/currency.pyx b/nautilus_trader/model/currency.pyx index 512274f22205..5c102093edec 100644 --- a/nautilus_trader/model/currency.pyx +++ b/nautilus_trader/model/currency.pyx @@ -16,92 +16,7 @@ from nautilus_trader.core.correctness cimport Condition from nautilus_trader.model.c_enums.currency_type cimport CurrencyType from nautilus_trader.model.c_enums.currency_type cimport CurrencyTypeParser - -# Crypto currencies -BTC = Currency("BTC", precision=8, currency_type=CurrencyType.CRYPTO) -ETH = Currency("ETH", precision=8, currency_type=CurrencyType.CRYPTO) -USDT = Currency("USDT", precision=8, currency_type=CurrencyType.CRYPTO) -XRP = Currency("XRP", precision=8, currency_type=CurrencyType.CRYPTO) -BCH = Currency("BCH", precision=8, currency_type=CurrencyType.CRYPTO) -BNB = Currency("BNB", precision=8, currency_type=CurrencyType.CRYPTO) -DOT = Currency("DOT", precision=8, currency_type=CurrencyType.CRYPTO) -LINK = Currency("LINK", precision=8, currency_type=CurrencyType.CRYPTO) -LTC = Currency("LTC", precision=8, currency_type=CurrencyType.CRYPTO) - -# Fiat currencies -AUD = Currency("AUD", precision=2, currency_type=CurrencyType.FIAT) -CAD = Currency("CAD", precision=2, currency_type=CurrencyType.FIAT) -CHF = Currency("CHF", precision=2, currency_type=CurrencyType.FIAT) -CNY = Currency("CNY", precision=2, currency_type=CurrencyType.FIAT) -CNH = Currency("CNH", precision=2, currency_type=CurrencyType.FIAT) -CZK = Currency("CZK", precision=2, currency_type=CurrencyType.FIAT) -EUR = Currency("EUR", precision=2, currency_type=CurrencyType.FIAT) -GBP = Currency("GBP", precision=2, currency_type=CurrencyType.FIAT) -HKD = Currency("HKD", precision=2, currency_type=CurrencyType.FIAT) -JPY = Currency("JPY", precision=2, currency_type=CurrencyType.FIAT) -MXN = Currency("MXN", precision=2, currency_type=CurrencyType.FIAT) -NOK = Currency("NOK", precision=2, currency_type=CurrencyType.FIAT) -NZD = Currency("NZD", precision=2, currency_type=CurrencyType.FIAT) -RUB = Currency("RUB", precision=2, currency_type=CurrencyType.FIAT) -SEK = Currency("SEK", precision=2, currency_type=CurrencyType.FIAT) -TRY = Currency("TRY", precision=2, currency_type=CurrencyType.FIAT) -SGD = Currency("SGD", precision=2, currency_type=CurrencyType.FIAT) -USD = Currency("USD", precision=2, currency_type=CurrencyType.FIAT) -ZAR = Currency("ZAR", precision=2, currency_type=CurrencyType.FIAT) - - -cdef dict _CURRENCY_TABLE = { - "BTC": BTC, - "ETH": ETH, - "XRP": XRP, - "BCH": BCH, - "BNB": BNB, - "DOT": DOT, - "LINK": LINK, - "LTC": LTC, - "USDT": USDT, - "AUD": AUD, - "CAD": CAD, - "CHF": CHF, - "CNY": CNY, - "CNH": CNH, - "CZK": CZK, - "EUR": EUR, - "GBP": GBP, - "HKD": HKD, - "JPY": JPY, - "MXN": MXN, - "NOK": NOK, - "NZD": NZD, - "RUB": RUB, - "SEK": SEK, - "TRY": TRY, - "SGD": SGD, - "USD": USD, - "ZAR": ZAR, -} - -cdef set _FIAT_CURRENCIES = { - "AUD", - "CAD", - "CHF", - "CNY", - "CNH", - "CZK", - "EUR", - "GBP", - "HKD", - "JPY", - "MXN", - "NOK", - "NZD", - "RUB", - "SEK", - "TRY", - "SGD", - "USD", - "ZAR", -} +from nautilus_trader.model.currencies cimport _CURRENCY_MAP cdef class Currency: @@ -114,6 +29,8 @@ cdef class Currency: self, str code, int precision, + int iso4217, + str name, CurrencyType currency_type, ): """ @@ -125,6 +42,10 @@ cdef class Currency: The currency code. precision : int The currency decimal precision. + iso4217 : int + The currency ISO 4217 code. + name : str + The currency name. currency_type : CurrencyType (Enum) The currency type. @@ -134,16 +55,21 @@ cdef class Currency: If code is not a valid string. ValueError If precision is negative (< 0). + ValueError + If name is not a valid string. ValueError If currency_type is UNDEFINED. """ Condition.valid_string(code, "code") + Condition.valid_string(name, "name") Condition.not_negative_int(precision, "precision") Condition.not_equal(currency_type, CurrencyType.UNDEFINED, "currency_type", "UNDEFINED") self.code = code + self.name = name self.precision = precision + self.iso4217 = iso4217 self.currency_type = currency_type def __eq__(self, Currency other) -> bool: @@ -161,12 +87,14 @@ cdef class Currency: def __repr__(self) -> str: return (f"{type(self).__name__}(" f"code={self.code}, " + f"name={self.name}, " f"precision={self.precision}, " + f"iso4217={self.iso4217}, " f"type={CurrencyTypeParser.to_str(self.currency_type)})") @staticmethod cdef Currency from_str_c(str code): - return _CURRENCY_TABLE.get(code) + return _CURRENCY_MAP.get(code) @staticmethod def from_str(str code): @@ -183,11 +111,23 @@ cdef class Currency: Currency or None """ - return _CURRENCY_TABLE.get(code) + return _CURRENCY_MAP.get(code) @staticmethod cdef bint is_fiat_c(str code): - return code in _FIAT_CURRENCIES + cdef Currency currency = _CURRENCY_MAP.get(code) + if currency is None: + return False + + return currency.currency_type == CurrencyType.FIAT + + @staticmethod + cdef bint is_crypto_c(str code): + cdef Currency currency = _CURRENCY_MAP.get(code) + if currency is None: + return False + + return currency.currency_type == CurrencyType.CRYPTO @staticmethod def is_fiat(str code): @@ -204,5 +144,37 @@ cdef class Currency: bool True if Fiat, else False. + Raises + ------ + ValueError + If code is not a valid string. + + """ + Condition.valid_string(code, "code") + + return Currency.is_fiat_c(code) + + @staticmethod + def is_crypto(str code): + """ + Return a value indicating whether a currency with the given code is Crypto. + + Parameters + ---------- + code : str + The code of the currency. + + Returns + ------- + bool + True if Crypto, else False. + + Raises + ------ + ValueError + If code is not a valid string. + """ - return code in _FIAT_CURRENCIES + Condition.valid_string(code, "code") + + return Currency.is_crypto_c(code) diff --git a/nautilus_trader/model/data.pxd b/nautilus_trader/model/data.pxd index e6acea3b5ce9..b46c2af98062 100644 --- a/nautilus_trader/model/data.pxd +++ b/nautilus_trader/model/data.pxd @@ -18,23 +18,23 @@ from cpython.datetime cimport datetime cdef class Data: cdef readonly datetime timestamp - """The timestamp (UTC).\n\n:returns: `datetime`""" + """The data timestamp (UTC).\n\n:returns: `datetime`""" cdef readonly double unix_timestamp - """The Unix timestamp (seconds).\n\n:returns: `double`""" - - -cdef class GenericData(Data): - cdef readonly DataType data_type - """The data type for the data.\n\n:returns: `DataType`""" - cdef readonly object data - """The data.\n\n:returns: `object`""" + """The data Unix timestamp (seconds).\n\n:returns: `double`""" cdef class DataType: - cdef frozenset _metadata_key + cdef frozenset _key cdef int _hash cdef readonly type type - """The type of the data.\n\n:returns: `type`""" + """The PyObject type of the data.\n\n:returns: `type`""" cdef readonly dict metadata """The data types metadata.\n\n:returns: `set[str, object]`""" + + +cdef class GenericData(Data): + cdef readonly DataType data_type + """The data type for the data.\n\n:returns: `DataType`""" + cdef readonly object data + """The data.\n\n:returns: `object`""" diff --git a/nautilus_trader/model/data.pyx b/nautilus_trader/model/data.pyx index cb7bb1795db3..e3f79466c293 100644 --- a/nautilus_trader/model/data.pyx +++ b/nautilus_trader/model/data.pyx @@ -51,45 +51,6 @@ cdef class Data: self.unix_timestamp = unix_timestamp -cdef class GenericData(Data): - """ - Provides a generic data wrapper which includes data type information. - """ - - def __init__( - self, - DataType data_type not None, - data not None, - datetime timestamp not None, - double unix_timestamp=0, - ): - """ - Initialize a new instance of the `GenericData` class. - - Parameters - ---------- - data_type : DataType - The data type. - data : object - The data object to wrap. - timestamp : datetime - The data timestamp (UTC). - unix_timestamp : double, optional - The data Unix timestamp (seconds). - - Raises - ------ - ValueError - If type(data) is not of type data_type.type. - - """ - Condition.type(data, data_type.type, "data") - super().__init__(timestamp, unix_timestamp) - - self.data_type = data_type - self.data = data - - cdef class DataType: """ Represents a data type including its metadata. @@ -106,22 +67,22 @@ cdef class DataType: metadata : dict The data types metadata. - Warnings - -------- - This class may be used as a key in hash maps throughout the system, - thus the key and value contents of metadata must themselves be hashable. - Raises ------ TypeError If metadata contains a key or value which is not hashable. + Warnings + -------- + This class may be used as a key in hash maps throughout the system, thus + the key and value contents of metadata must themselves be hashable. + """ if metadata is None: metadata = {} - self._metadata_key = frozenset(metadata.items()) - self._hash = hash(self._metadata_key) # Assign hash for improved time complexity + self._key = frozenset(metadata.items()) + self._hash = hash(self._key) # Assign hash for improved time complexity self.type = data_type self.metadata = metadata @@ -139,3 +100,42 @@ cdef class DataType: def __repr__(self) -> str: return f"{type(self).__name__}(type={self.type.__name__}, metadata={self.metadata})" + + +cdef class GenericData(Data): + """ + Provides a generic data wrapper which includes data type information. + """ + + def __init__( + self, + DataType data_type not None, + data not None, + datetime timestamp not None, + double unix_timestamp=0, + ): + """ + Initialize a new instance of the `GenericData` class. + + Parameters + ---------- + data_type : DataType + The data type. + data : object + The data object to wrap. + timestamp : datetime + The data timestamp (UTC). + unix_timestamp : double, optional + The data Unix timestamp (seconds). + + Raises + ------ + ValueError + If type(data) is not of type data_type.type. + + """ + Condition.type(data, data_type.type, "data") + super().__init__(timestamp, unix_timestamp) + + self.data_type = data_type + self.data = data diff --git a/nautilus_trader/model/events.pyx b/nautilus_trader/model/events.pyx index ec386b096491..bb73b7105df5 100644 --- a/nautilus_trader/model/events.pyx +++ b/nautilus_trader/model/events.pyx @@ -917,6 +917,7 @@ cdef class PositionEvent(Event): """ super().__init__(event_id, event_timestamp) + self.position = position self.order_fill = order_fill @@ -948,7 +949,7 @@ cdef class PositionOpened(PositionEvent): The event timestamp. """ - assert position.is_open_c() + assert position.is_open_c() # Design-time check super().__init__( position, order_fill, @@ -999,7 +1000,7 @@ cdef class PositionChanged(PositionEvent): If position is not open. """ - assert position.is_open_c() + assert position.is_open_c() # Design-time check super().__init__( position, order_fill, @@ -1053,7 +1054,7 @@ cdef class PositionClosed(PositionEvent): If position is not closed. """ - assert position.is_closed_c() + assert position.is_closed_c() # Design-time check super().__init__( position, order_fill, diff --git a/nautilus_trader/model/identifiers.pxd b/nautilus_trader/model/identifiers.pxd index 08bacd811f36..3724add318e1 100644 --- a/nautilus_trader/model/identifiers.pxd +++ b/nautilus_trader/model/identifiers.pxd @@ -34,8 +34,7 @@ cdef class InstrumentId(Identifier): """The instrument trading venue.\n\n:returns: `Venue`""" @staticmethod - cdef InstrumentId from_serializable_str_c(str value) - cpdef str to_serializable_str(self) + cdef InstrumentId from_str_c(str value) cdef class IdTag(Identifier): diff --git a/nautilus_trader/model/identifiers.pyx b/nautilus_trader/model/identifiers.pyx index b6d2c31e7687..2e6848db5a42 100644 --- a/nautilus_trader/model/identifiers.pyx +++ b/nautilus_trader/model/identifiers.pyx @@ -149,7 +149,7 @@ cdef class InstrumentId(Identifier): self.venue = venue @staticmethod - cdef InstrumentId from_serializable_str_c(str value): + cdef InstrumentId from_str_c(str value): Condition.valid_string(value, "value") cdef tuple pieces = value.partition('.') @@ -160,10 +160,10 @@ cdef class InstrumentId(Identifier): return InstrumentId(symbol=Symbol(pieces[0]), venue=Venue(pieces[2])) @staticmethod - def from_serializable_str(value: str) -> InstrumentId: + def from_str(value: str) -> InstrumentId: """ Return an instrument identifier parsed from the given string value. - Must be correctly formatted including a single period and two commas. + Must be correctly formatted including a single period. Example: "AUD/USD.IDEALPRO". @@ -177,18 +177,7 @@ cdef class InstrumentId(Identifier): InstrumentId """ - return InstrumentId.from_serializable_str_c(value) - - cpdef str to_serializable_str(self): - """ - Return a serializable string representation of this object. - - Returns - ------- - str - - """ - return self.value + return InstrumentId.from_str_c(value) cdef class IdTag(Identifier): @@ -269,7 +258,7 @@ cdef class TraderId(Identifier): Return a trader identifier parsed from the given string value. Must be correctly formatted with two valid strings either side of a hyphen. - Its is expected a trader identifier is the abbreviated name of the + It is expected a trader identifier is the abbreviated name of the trader with an order identifier tag number separated by a hyphen. Example: "TESTER-001". @@ -351,7 +340,7 @@ cdef class StrategyId(Identifier): Must be correctly formatted with two valid strings either side of a hyphen. Is is expected a strategy identifier is the class name of the strategy with - an order_id tag number separated by a hyphen. + an order identifier tag number separated by a hyphen. Example: "EMACross-001". diff --git a/nautilus_trader/model/instrument.pxd b/nautilus_trader/model/instrument.pxd index 8cdbdefd817e..3a72abad8c94 100644 --- a/nautilus_trader/model/instrument.pxd +++ b/nautilus_trader/model/instrument.pxd @@ -15,13 +15,12 @@ from decimal import Decimal -from cpython.datetime cimport datetime - from nautilus_trader.model.c_enums.asset_class cimport AssetClass from nautilus_trader.model.c_enums.asset_type cimport AssetType from nautilus_trader.model.c_enums.liquidity_side cimport LiquiditySide from nautilus_trader.model.c_enums.position_side cimport PositionSide from nautilus_trader.model.currency cimport Currency +from nautilus_trader.model.data cimport Data from nautilus_trader.model.identifiers cimport InstrumentId from nautilus_trader.model.identifiers cimport Symbol from nautilus_trader.model.identifiers cimport Venue @@ -30,7 +29,7 @@ from nautilus_trader.model.objects cimport Price from nautilus_trader.model.objects cimport Quantity -cdef class Instrument: +cdef class Instrument(Data): cdef readonly InstrumentId id """The instrument identifier.\n\n:returns: `InstrumentId`""" cdef readonly Symbol symbol @@ -87,8 +86,6 @@ cdef class Instrument: """The taker fee rate for the instrument.\n\n:returns: `Decimal`""" cdef readonly dict financing """The financing information for the instrument.\n\n:returns: `dict[str, object]`""" - cdef readonly datetime timestamp - """The initialization timestamp of the instrument.\n\n:returns: `datetime`""" cdef readonly dict info """The additional instrument information.\n\n:returns: `dict[str, object]`""" diff --git a/nautilus_trader/model/instrument.pyx b/nautilus_trader/model/instrument.pyx index 417e79ecb228..f9528af29f28 100644 --- a/nautilus_trader/model/instrument.pyx +++ b/nautilus_trader/model/instrument.pyx @@ -183,6 +183,7 @@ cdef class Instrument: Condition.type(taker_fee, Decimal, "taker_fee") if info is None: info = {} + super().__init__(timestamp) self.id = instrument_id self.symbol = instrument_id.symbol @@ -212,7 +213,6 @@ cdef class Instrument: self.maker_fee = maker_fee self.taker_fee = taker_fee self.financing = financing - self.timestamp = timestamp self.info = info cdef bint _is_quanto( diff --git a/nautilus_trader/model/order/limit.pyx b/nautilus_trader/model/order/limit.pyx index a02292299a2e..642140422e96 100644 --- a/nautilus_trader/model/order/limit.pyx +++ b/nautilus_trader/model/order/limit.pyx @@ -106,7 +106,6 @@ cdef class LimitOrder(PassiveOrder): Condition.false(hidden, "A post-only order is not hidden") if hidden: Condition.false(post_only, "A hidden order is not post-only") - super().__init__( cl_ord_id, strategy_id, diff --git a/nautilus_trader/model/order/stop_limit.pyx b/nautilus_trader/model/order/stop_limit.pyx index 9b981cec3b10..0be7ad5bf4d1 100644 --- a/nautilus_trader/model/order/stop_limit.pyx +++ b/nautilus_trader/model/order/stop_limit.pyx @@ -116,7 +116,6 @@ cdef class StopLimitOrder(PassiveOrder): Condition.false(hidden, "A post-only order is not hidden") if hidden: Condition.false(post_only, "A hidden order is not post-only") - super().__init__( cl_ord_id, strategy_id, diff --git a/nautilus_trader/risk/engine.pxd b/nautilus_trader/risk/engine.pxd index 8d7042a8af55..1cafc1e9dbc3 100644 --- a/nautilus_trader/risk/engine.pxd +++ b/nautilus_trader/risk/engine.pxd @@ -14,22 +14,64 @@ # ------------------------------------------------------------------------------------------------- from nautilus_trader.common.component cimport Component +from nautilus_trader.core.message cimport Command +from nautilus_trader.core.message cimport Event +from nautilus_trader.execution.client cimport ExecutionClient from nautilus_trader.execution.engine cimport ExecutionEngine +from nautilus_trader.model.commands cimport AmendOrder +from nautilus_trader.model.commands cimport CancelOrder from nautilus_trader.model.commands cimport SubmitBracketOrder from nautilus_trader.model.commands cimport SubmitOrder +from nautilus_trader.model.commands cimport TradingCommand from nautilus_trader.model.order.base cimport Order from nautilus_trader.trading.portfolio cimport Portfolio cdef class RiskEngine(Component): + cdef dict _clients cdef Portfolio _portfolio cdef ExecutionEngine _exec_engine + cdef readonly int command_count + """The total count of commands received by the engine.\n\n:returns: `int`""" + cdef readonly int event_count + """The total count of events received by the engine.\n\n:returns: `int`""" cdef readonly bint block_all_orders + """If all orders are blocked from being sent.\n\n:returns: `bool`""" + +# -- REGISTRATION ---------------------------------------------------------------------------------- + + cpdef void register_client(self, ExecutionClient client) except * + +# -- ABSTRACT METHODS ------------------------------------------------------------------------------ + + cpdef void _on_start(self) except * + cpdef void _on_stop(self) except * + +# -- COMMANDS -------------------------------------------------------------------------------------- + + cpdef void execute(self, Command command) except * + cpdef void process(self, Event event) except * + +# -- COMMAND HANDLERS ------------------------------------------------------------------------------ + + cdef inline void _execute_command(self, Command command) except * + cdef inline void _handle_trading_command(self, TradingCommand command) except * + cdef inline void _handle_submit_order(self, ExecutionClient client, SubmitOrder command) except * + cdef inline void _handle_submit_bracket_order(self, ExecutionClient client, SubmitBracketOrder command) except * + cdef inline void _handle_amend_order(self, ExecutionClient client, AmendOrder command) except * + cdef inline void _handle_cancel_order(self, ExecutionClient client, CancelOrder command) except * + +# -- EVENT HANDLERS -------------------------------------------------------------------------------- + + cdef inline void _handle_event(self, Event event) except * + +# -- RISK MANAGEMENT ------------------------------------------------------------------------------- - cpdef void set_block_all_orders(self, bint value=*) except * - cpdef void approve_order(self, SubmitOrder command) except * - cpdef void approve_bracket(self, SubmitBracketOrder command) except * cdef list _check_submit_order_risk(self, SubmitOrder command) cdef list _check_submit_bracket_order_risk(self, SubmitBracketOrder command) cdef void _deny_order(self, Order order, str reason) except * + +# -- TEMP ------------------------------------------------------------------------------------------ + + cpdef void set_block_all_orders(self, bint value=*) except * diff --git a/nautilus_trader/risk/engine.pyx b/nautilus_trader/risk/engine.pyx index a0525db4e1ef..cbf6b342c2bb 100644 --- a/nautilus_trader/risk/engine.pyx +++ b/nautilus_trader/risk/engine.pyx @@ -21,12 +21,22 @@ Alternative implementations can be written on top of the generic engine. from nautilus_trader.common.clock cimport Clock from nautilus_trader.common.component cimport Component +from nautilus_trader.common.logging cimport CMD +from nautilus_trader.common.logging cimport EVT from nautilus_trader.common.logging cimport Logger +from nautilus_trader.common.logging cimport RECV from nautilus_trader.core.correctness cimport Condition +from nautilus_trader.core.message cimport Command +from nautilus_trader.core.message cimport Event +from nautilus_trader.execution.client cimport ExecutionClient from nautilus_trader.execution.engine cimport ExecutionEngine +from nautilus_trader.model.commands cimport AmendOrder +from nautilus_trader.model.commands cimport CancelOrder from nautilus_trader.model.commands cimport SubmitBracketOrder from nautilus_trader.model.commands cimport SubmitOrder +from nautilus_trader.model.commands cimport TradingCommand from nautilus_trader.model.events cimport OrderDenied +from nautilus_trader.model.identifiers cimport Venue from nautilus_trader.model.order.base cimport Order from nautilus_trader.trading.portfolio cimport Portfolio @@ -50,13 +60,13 @@ cdef class RiskEngine(Component): Parameters ---------- exec_engine : ExecutionEngine - The execution engine for the risk engine. + The execution engine for the engine. portfolio : Portfolio - The portfolio for the risk engine. + The portfolio for the engine. clock : Clock - The clock for the risk engine. + The clock for the engine. logger : Logger - The logger for the risk engine. + The logger for the engine. config : dict[str, object], optional The configuration options. @@ -65,39 +75,135 @@ cdef class RiskEngine(Component): config = {} super().__init__(clock, logger, name="RiskEngine") + self._clients = {} # type: dict[Venue, ExecutionClient] self._portfolio = portfolio self._exec_engine = exec_engine self.block_all_orders = False - # Check portfolio matches execution engines portfolio - self._exec_engine.check_portfolio_equal(portfolio) + # Counters + self.command_count = 0 + self.event_count = 0 - cpdef void set_block_all_orders(self, bint value=True) except *: + @property + def registered_clients(self): """ - Set the global `block_all_orders` flag to the given value. + The execution clients registered with the engine. + + Returns + ------- + list[Venue] + + """ + return sorted(list(self._clients.keys())) + +# -- REGISTRATION ---------------------------------------------------------------------------------- + + cpdef void register_client(self, ExecutionClient client) except *: + """ + Register the given execution client with the risk engine. Parameters ---------- - value : bool - The flag setting. + client : ExecutionClient + The execution client to register. + + Raises + ------ + ValueError + If client is already registered with the risk engine. """ - self.block_all_orders = value - self._log.warning(f"`block_all_orders` set to {value}.") + Condition.not_none(client, "client") + Condition.not_in(client.venue, self._clients, "client.venue", "self._clients") - cpdef void approve_order(self, SubmitOrder command) except *: + self._clients[client.venue] = client + self._log.info(f"Registered {client}.") + +# -- COMMANDS -------------------------------------------------------------------------------------- + + cpdef void execute(self, Command command) except *: """ - Approve the given command based on risk. + Execute the given command. Parameters ---------- - command : SubmitOrder - The command to approve. + command : Command + The command to execute. """ Condition.not_none(command, "command") + self._execute_command(command) + + cpdef void process(self, Event event) except *: + """ + Process the given event. + + Parameters + ---------- + event : Event + The event to process. + + """ + Condition.not_none(event, "event") + + self._handle_event(event) + +# -- ABSTRACT METHODS ------------------------------------------------------------------------------ + + cpdef void _on_start(self) except *: + pass # Optionally override in subclass + + cpdef void _on_stop(self) except *: + pass # Optionally override in subclass + +# -- ACTION IMPLEMENTATIONS ------------------------------------------------------------------------ + + cpdef void _start(self) except *: + # Do nothing else for now + self._on_start() + + cpdef void _stop(self) except *: + # Do nothing else for now + self._on_stop() + + cpdef void _reset(self) except *: + self.command_count = 0 + self.event_count = 0 + + cpdef void _dispose(self) except *: + pass + # Nothing to dispose for now + +# -- COMMAND HANDLERS ------------------------------------------------------------------------------ + + cdef inline void _execute_command(self, Command command) except *: + self._log.debug(f"{RECV}{CMD} {command}.") + self.command_count += 1 + + if isinstance(command, TradingCommand): + self._handle_trading_command(command) + + cdef inline void _handle_trading_command(self, TradingCommand command) except *: + cdef ExecutionClient client = self._clients.get(command.venue) + if client is None: + self._log.error(f"Cannot handle command: " + f"No client registered for {command.venue}, {command}.") + return # No client to handle command + + if isinstance(command, SubmitOrder): + self._handle_submit_order(client, command) + elif isinstance(command, SubmitBracketOrder): + self._handle_submit_bracket_order(client, command) + elif isinstance(command, AmendOrder): + self._handle_amend_order(client, command) + elif isinstance(command, CancelOrder): + self._handle_cancel_order(client, command) + else: + self._log.error(f"Cannot handle command: unrecognized {command}.") + + cdef inline void _handle_submit_order(self, ExecutionClient client, SubmitOrder command) except *: cdef list risk_msgs = self._check_submit_order_risk(command) if self.block_all_orders: @@ -107,21 +213,9 @@ cdef class RiskEngine(Component): if risk_msgs: self._deny_order(command.order, ",".join(risk_msgs)) else: - command.approve() - self._exec_engine.execute(command) - - cpdef void approve_bracket(self, SubmitBracketOrder command) except *: - """ - Approve the given command based on risk. - - Parameters - ---------- - command : SubmitBracketOrder - The command to approve. - - """ - Condition.not_none(command, "command") + client.submit_order(command) + cdef inline void _handle_submit_bracket_order(self, ExecutionClient client, SubmitBracketOrder command) except *: # TODO: Below currently just cut-and-pasted from above. Can refactor further. cdef list risk_msgs = self._check_submit_bracket_order_risk(command) @@ -134,8 +228,23 @@ cdef class RiskEngine(Component): self._deny_order(command.bracket_order.stop_loss, ",".join(risk_msgs)) self._deny_order(command.bracket_order.take_profit, ",".join(risk_msgs)) else: - command.approve() - self._exec_engine.execute(command) + client.submit_bracket_order(command) + + cdef inline void _handle_amend_order(self, ExecutionClient client, AmendOrder command) except *: + # Pass-through for now + client.amend_order(command) + + cdef inline void _handle_cancel_order(self, ExecutionClient client, CancelOrder command) except *: + # Pass-through for now + client.cancel_order(command) + +# -- EVENT HANDLERS -------------------------------------------------------------------------------- + + cdef inline void _handle_event(self, Event event) except *: + self._log.debug(f"{RECV}{EVT} {event}.") + self.event_count += 1 + +# -- RISK MANAGEMENT ------------------------------------------------------------------------------- cdef list _check_submit_order_risk(self, SubmitOrder command): # Override this implementation with custom logic @@ -155,3 +264,18 @@ cdef class RiskEngine(Component): ) self._exec_engine.process(denied) + +# -- TEMP ------------------------------------------------------------------------------------------ + + cpdef void set_block_all_orders(self, bint value=True) except *: + """ + Set the global `block_all_orders` flag to the given value. + + Parameters + ---------- + value : bool + The flag setting. + + """ + self.block_all_orders = value + self._log.warning(f"`block_all_orders` set to {value}.") diff --git a/nautilus_trader/serialization/serializers.pyx b/nautilus_trader/serialization/serializers.pyx index d68c592d0bb8..fb841ae3af0e 100644 --- a/nautilus_trader/serialization/serializers.pyx +++ b/nautilus_trader/serialization/serializers.pyx @@ -128,7 +128,7 @@ cdef class MsgPackOrderSerializer(OrderSerializer): """ super().__init__() - self.symbol_cache = ObjectCache(InstrumentId, InstrumentId.from_serializable_str_c) + self.symbol_cache = ObjectCache(InstrumentId, InstrumentId.from_str_c) cpdef bytes serialize(self, Order order): # Can be None """ @@ -150,7 +150,7 @@ cdef class MsgPackOrderSerializer(OrderSerializer): cdef dict package = { ID: order.cl_ord_id.value, STRATEGY_ID: order.strategy_id.value, - INSTRUMENT_ID: order.instrument_id.to_serializable_str(), + INSTRUMENT_ID: order.instrument_id.value, ORDER_SIDE: self.convert_snake_to_camel(OrderSideParser.to_str(order.side)), ORDER_TYPE: self.convert_snake_to_camel(OrderTypeParser.to_str(order.type)), QUANTITY: str(order.quantity), @@ -487,7 +487,7 @@ cdef class MsgPackEventSerializer(EventSerializer): elif isinstance(event, OrderInitialized): package[CLIENT_ORDER_ID] = event.cl_ord_id.value package[STRATEGY_ID] = event.strategy_id.value - package[INSTRUMENT_ID] = event.instrument_id.to_serializable_str() + package[INSTRUMENT_ID] = event.instrument_id.value package[ORDER_SIDE] = self.convert_snake_to_camel(OrderSideParser.to_str(event.order_side)) package[ORDER_TYPE] = self.convert_snake_to_camel(OrderTypeParser.to_str(event.order_type)) package[QUANTITY] = str(event.quantity) @@ -562,7 +562,7 @@ cdef class MsgPackEventSerializer(EventSerializer): package[EXECUTION_ID] = event.execution_id.value package[POSITION_ID] = event.position_id.value package[STRATEGY_ID] = event.strategy_id.value - package[INSTRUMENT_ID] = event.instrument_id.to_serializable_str() + package[INSTRUMENT_ID] = event.instrument_id.value package[ORDER_SIDE] = self.convert_snake_to_camel(OrderSideParser.to_str(event.order_side)) package[FILL_QTY] = str(event.fill_qty) package[FILL_PRICE] = str(event.fill_price) diff --git a/nautilus_trader/trading/portfolio.pyx b/nautilus_trader/trading/portfolio.pyx index e9207a0d9436..73cfe66309f0 100644 --- a/nautilus_trader/trading/portfolio.pyx +++ b/nautilus_trader/trading/portfolio.pyx @@ -532,12 +532,12 @@ cdef class Portfolio(PortfolioFacade): cpdef Money unrealized_pnl(self, InstrumentId instrument_id): """ - Return the unrealized PnL for the given instrument_id (if found). + Return the unrealized PnL for the given instrument identifier (if found). Parameters ---------- instrument_id : InstrumentId - The instrument identifier for the unrealized PnL. + The instrument for the unrealized PnL. Returns ------- @@ -558,12 +558,12 @@ cdef class Portfolio(PortfolioFacade): cpdef Money market_value(self, InstrumentId instrument_id): """ - Return the market value for the given instrument_id (if found). + Return the market value for the given instrument identifier (if found). Parameters ---------- instrument_id : InstrumentId - The instrument identifier for the market value. + The instrument for the market value. Returns ------- @@ -632,13 +632,13 @@ cdef class Portfolio(PortfolioFacade): cpdef object net_position(self, InstrumentId instrument_id): """ - Return the net relative position for the given instrument_id. If no positions + Return the net relative position for the given instrument identifier. If no positions for instrument_id then will return `Decimal('0')`. Parameters ---------- instrument_id : InstrumentId - The instrument identifier for the query. + The instrument for the query. Returns ------- @@ -650,12 +650,12 @@ cdef class Portfolio(PortfolioFacade): cpdef bint is_net_long(self, InstrumentId instrument_id) except *: """ Return a value indicating whether the portfolio is net long the given - instrument_id. + instrument identifier. Parameters ---------- instrument_id : InstrumentId - The instrument identifier for the query. + The instrument for the query. Returns ------- @@ -670,12 +670,12 @@ cdef class Portfolio(PortfolioFacade): cpdef bint is_net_short(self, InstrumentId instrument_id) except *: """ Return a value indicating whether the portfolio is net short the given - instrument_id. + instrument identifier. Parameters ---------- instrument_id : InstrumentId - The instrument identifier for the query. + The instrument for the query. Returns ------- @@ -690,12 +690,12 @@ cdef class Portfolio(PortfolioFacade): cpdef bint is_flat(self, InstrumentId instrument_id) except *: """ Return a value indicating whether the portfolio is flat for the given - instrument_id. + instrument identifier. Parameters ---------- instrument_id : InstrumentId, optional - The instrument identifier query filter. + The instrument query filter. Returns ------- diff --git a/nautilus_trader/trading/trader.pxd b/nautilus_trader/trading/trader.pxd index a3011738eaf1..9f78ef1c5af3 100644 --- a/nautilus_trader/trading/trader.pxd +++ b/nautilus_trader/trading/trader.pxd @@ -20,6 +20,7 @@ from nautilus_trader.data.engine cimport DataEngine from nautilus_trader.execution.engine cimport ExecutionEngine from nautilus_trader.model.identifiers cimport TraderId from nautilus_trader.model.identifiers cimport Venue +from nautilus_trader.risk.engine cimport RiskEngine from nautilus_trader.trading.portfolio cimport Portfolio @@ -28,6 +29,7 @@ cdef class Trader(Component): cdef Portfolio _portfolio cdef DataEngine _data_engine cdef ExecutionEngine _exec_engine + cdef RiskEngine _risk_engine cdef ReportProvider _report_provider cdef readonly TraderId id diff --git a/nautilus_trader/trading/trader.pyx b/nautilus_trader/trading/trader.pyx index 8d9931543a3e..0e7e78070006 100644 --- a/nautilus_trader/trading/trader.pyx +++ b/nautilus_trader/trading/trader.pyx @@ -31,6 +31,7 @@ from nautilus_trader.core.correctness cimport Condition from nautilus_trader.data.engine cimport DataEngine from nautilus_trader.execution.engine cimport ExecutionEngine from nautilus_trader.model.identifiers cimport Venue +from nautilus_trader.risk.engine cimport RiskEngine from nautilus_trader.trading.strategy cimport TradingStrategy @@ -46,6 +47,7 @@ cdef class Trader(Component): Portfolio portfolio not None, DataEngine data_engine not None, ExecutionEngine exec_engine not None, + RiskEngine risk_engine not None, Clock clock not None, Logger logger not None, ): @@ -61,9 +63,11 @@ cdef class Trader(Component): portfolio : Portfolio The portfolio for the trader. data_engine : DataEngine - The data engine to register the traders strategies with. + The data engine for the trader. exec_engine : ExecutionEngine - The execution engine to register the traders strategies with. + The execution engine for the trader. + risk_engine : RiskEngine + The risk engine for the trader. clock : Clock The clock for the trader. logger : Logger @@ -91,6 +95,7 @@ cdef class Trader(Component): self._portfolio = portfolio self._data_engine = data_engine self._exec_engine = exec_engine + self._risk_engine = risk_engine self._report_provider = ReportProvider() self.id = trader_id diff --git a/poetry.lock b/poetry.lock index f7379a3f5f9f..71b310e133f1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -109,7 +109,7 @@ pytz = ">=2015.7" [[package]] name = "ccxt" -version = "1.42.98" +version = "1.43.25" description = "A JavaScript / Python / PHP cryptocurrency trading library with support for 130+ exchanges" category = "main" optional = false @@ -284,17 +284,17 @@ python-versions = "*" [[package]] name = "flake8" -version = "3.8.4" +version = "3.9.0" description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.6.0a1,<2.7.0" -pyflakes = ">=2.2.0,<2.3.0" +pycodestyle = ">=2.7.0,<2.8.0" +pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "ib-insync" @@ -310,7 +310,7 @@ nest-asyncio = "*" [[package]] name = "identify" -version = "2.1.2" +version = "2.1.3" description = "File identification library for Python" category = "dev" optional = false @@ -624,7 +624,7 @@ idna = ["idna (>=2.1)"] [[package]] name = "pycodestyle" -version = "2.6.0" +version = "2.7.0" description = "Python style guide checker" category = "dev" optional = false @@ -640,7 +640,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pyflakes" -version = "2.2.0" +version = "2.3.0" description = "passive checker of Python programs" category = "dev" optional = false @@ -1014,7 +1014,7 @@ docs = ["numpydoc"] [metadata] lock-version = "1.1" python-versions = "^3.7.9" -content-hash = "c615bbdcfe2a444ecdb179129393dc053272a02842563aee9b55af00bc8f42c7" +content-hash = "1f2f1e627d9b99102309add4f90659e7cd71ea160010439e14459e0a1710eae2" [metadata.files] aiodns = [ @@ -1093,8 +1093,8 @@ babel = [ {file = "Babel-2.9.0.tar.gz", hash = "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05"}, ] ccxt = [ - {file = "ccxt-1.42.98-py2.py3-none-any.whl", hash = "sha256:f9e055d2610efcbbdf4cfea34de365abfb5341feba6f2c4d473a7a89bd45ff55"}, - {file = "ccxt-1.42.98.tar.gz", hash = "sha256:2c84045630d8e95ef9a0a60e00f30d63e25895c97e0ccf285465f8f33c3c5ef5"}, + {file = "ccxt-1.43.25-py2.py3-none-any.whl", hash = "sha256:a0660b1ef0954492d32fd0c9ab853922a8a8a0a8dd17bfc2f6cb9ced3e9dd39f"}, + {file = "ccxt-1.43.25.tar.gz", hash = "sha256:eee0f2731f5d359efefb22c5cc94f6b8c6aac684b371c4c9758afc980edf9077"}, ] certifi = [ {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, @@ -1268,16 +1268,16 @@ filelock = [ {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, ] flake8 = [ - {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"}, - {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"}, + {file = "flake8-3.9.0-py2.py3-none-any.whl", hash = "sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff"}, + {file = "flake8-3.9.0.tar.gz", hash = "sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0"}, ] ib-insync = [ {file = "ib_insync-0.9.65-py3-none-any.whl", hash = "sha256:1f4601fcf2f1cf1224eea55a130b182a35bedda5fbfda9dd65cc09f57d368a87"}, {file = "ib_insync-0.9.65.tar.gz", hash = "sha256:7fe0bb294bb7a86414ebc0935a5f759ebb3b3abe903907b73ce0ba938a0c0510"}, ] identify = [ - {file = "identify-2.1.2-py2.py3-none-any.whl", hash = "sha256:fab0d3a3ab0d7d5f513985b0335ccccad9d61420c5216fb779237bf7edc3e5d1"}, - {file = "identify-2.1.2.tar.gz", hash = "sha256:e3b7fd755b7ceee44fe22957005a92c2a085c863c2e65a6efdec35d0e06666db"}, + {file = "identify-2.1.3-py2.py3-none-any.whl", hash = "sha256:46d1816c6a4fc2d1e8758f293a5dcc1ae6404ab344179d7c1e73637bf283beb1"}, + {file = "identify-2.1.3.tar.gz", hash = "sha256:ed4a05fb80e3cbd12e83c959f9ff7f729ba6b66ab8d6178850fd5cb4c1cf6c5d"}, ] idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, @@ -1622,16 +1622,16 @@ pycares = [ {file = "pycares-3.1.1.tar.gz", hash = "sha256:18dfd4fd300f570d6c4536c1d987b7b7673b2a9d14346592c5d6ed716df0d104"}, ] pycodestyle = [ - {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, - {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, ] pycparser = [ {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, ] pyflakes = [ - {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, - {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, + {file = "pyflakes-2.3.0-py2.py3-none-any.whl", hash = "sha256:910208209dcea632721cb58363d0f72913d9e8cf64dc6f8ae2e02a3609aba40d"}, + {file = "pyflakes-2.3.0.tar.gz", hash = "sha256:e59fd8e750e588358f1b8885e5a4751203a0516e0ee6d34811089ac294c8806f"}, ] pygments = [ {file = "Pygments-2.8.1-py3-none-any.whl", hash = "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8"}, diff --git a/pyproject.toml b/pyproject.toml index 46ce830ecd2f..f838448662f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "nautilus_trader" -version = "1.110.0" +version = "1.111.0" description = "A high-performance algorithmic trading platform and event-driven backtester" authors = ["Nautech Systems "] license = "LGPL-3.0-or-later" @@ -33,7 +33,7 @@ generate-setup-file = false [tool.poetry.dependencies] python = "^3.7.9" -ccxt = "^1.42.98" +ccxt = "^1.43.25" cython = "^3.0a6" empyrical = "^0.5.5" ib_insync = "^0.9.65" @@ -57,7 +57,7 @@ uvloop = { version = "^0.14.0", markers = "sys_platform != 'win32'" } # A commit was pushed on 7/1/21 to fix the above, # possibly fixed in the next version of Cython. coverage = "^4.5.4" -flake8 = "^3.8.4" +flake8 = "^3.9.0" isort = "^5.7.0" nox = "^2020.12.31" parameterized = "^0.8.1" diff --git a/tests/acceptance_tests/test_backtest_acceptance.py b/tests/acceptance_tests/test_backtest_acceptance.py index 9c4de3a17233..9ab2d1a3f5c5 100644 --- a/tests/acceptance_tests/test_backtest_acceptance.py +++ b/tests/acceptance_tests/test_backtest_acceptance.py @@ -85,7 +85,7 @@ def test_run_ema_cross_strategy(self): # Assert - Should return expected PnL self.assertEqual(2689, strategy.fast_ema.count) self.assertEqual(115043, self.engine.iteration) - self.assertEqual(Money(997731.21, USD), self.engine.portfolio.account(self.venue).balance()) + self.assertEqual(Money(997731.23, USD), self.engine.portfolio.account(self.venue).balance()) def test_rerun_ema_cross_strategy_returns_identical_performance(self): # Arrange @@ -139,7 +139,7 @@ def test_run_multiple_strategies(self): self.assertEqual(2689, strategy1.fast_ema.count) self.assertEqual(2689, strategy2.fast_ema.count) self.assertEqual(115043, self.engine.iteration) - self.assertEqual(Money(994662.72, USD), self.engine.portfolio.account(self.venue).balance()) + self.assertEqual(Money(994662.69, USD), self.engine.portfolio.account(self.venue).balance()) class BacktestAcceptanceTestsGBPUSDWithBars(unittest.TestCase): diff --git a/tests/acceptance_tests/test_backtest_cached.py b/tests/acceptance_tests/test_backtest_cached.py index d5d0bee29dc6..a01037368b93 100644 --- a/tests/acceptance_tests/test_backtest_cached.py +++ b/tests/acceptance_tests/test_backtest_cached.py @@ -54,7 +54,7 @@ def setUp(self): data=data, strategies=[TradingStrategy('000')], bypass_logging=True, - use_tick_cache=True, + use_data_cache=True, ) interest_rate_data = pd.read_csv(os.path.join(PACKAGE_ROOT + "/data/", "short-term-interest.csv")) @@ -86,7 +86,7 @@ def test_run_ema_cross_strategy(self): # Assert - Should return expected PnL self.assertEqual(2689, strategy.fast_ema.count) self.assertEqual(115043, self.engine.iteration) - self.assertEqual(Money(997731.21, USD), self.engine.portfolio.account(self.venue).balance()) + self.assertEqual(Money(997731.23, USD), self.engine.portfolio.account(self.venue).balance()) def test_rerun_ema_cross_strategy_returns_identical_performance(self): # Arrange @@ -140,7 +140,7 @@ def test_run_multiple_strategies(self): self.assertEqual(2689, strategy1.fast_ema.count) self.assertEqual(2689, strategy2.fast_ema.count) self.assertEqual(115043, self.engine.iteration) - self.assertEqual(Money(994662.72, USD), self.engine.portfolio.account(self.venue).balance()) + self.assertEqual(Money(994662.69, USD), self.engine.portfolio.account(self.venue).balance()) class BacktestAcceptanceTestsGBPUSDWithBars(unittest.TestCase): @@ -158,7 +158,7 @@ def setUp(self): data=data, strategies=[TradingStrategy('000')], bypass_logging=True, - use_tick_cache=True, + use_data_cache=True, ) interest_rate_data = pd.read_csv(os.path.join(PACKAGE_ROOT + "/data/", "short-term-interest.csv")) @@ -207,7 +207,7 @@ def setUp(self): data=data, strategies=[TradingStrategy('000')], bypass_logging=True, - use_tick_cache=True, + use_data_cache=True, ) interest_rate_data = pd.read_csv(os.path.join(PACKAGE_ROOT + "/data/", "short-term-interest.csv")) @@ -274,7 +274,7 @@ def setUp(self): data=data, strategies=[TradingStrategy('000')], bypass_logging=True, - use_tick_cache=True, + use_data_cache=True, ) self.engine.add_exchange( @@ -321,7 +321,7 @@ def setUp(self): data=data, strategies=[TradingStrategy('000')], bypass_logging=True, - use_tick_cache=True, + use_data_cache=True, ) self.engine.add_exchange( diff --git a/tests/integration_tests/adapters/ccxt/test_ccxt_providers.py b/tests/integration_tests/adapters/ccxt/test_ccxt_providers.py index f9bf9679f2ba..3a571c5be2c9 100644 --- a/tests/integration_tests/adapters/ccxt/test_ccxt_providers.py +++ b/tests/integration_tests/adapters/ccxt/test_ccxt_providers.py @@ -50,6 +50,7 @@ class CCXTInstrumentProviderTests(unittest.TestCase): # Uncomment to test real API # def test_real_api(self): + # import ccxt # client = ccxt.binance() # provider = CCXTInstrumentProvider(client=client) # @@ -171,7 +172,7 @@ def test_get_all_when_loaded_returns_instruments(self): # Assert self.assertTrue(len(instruments) > 0) self.assertEqual(dict, type(instruments)) - self.assertEqual(Symbol, type(next(iter(instruments)))) + self.assertEqual(InstrumentId, type(next(iter(instruments)))) def test_get_all_when_load_all_is_true_returns_expected_instruments(self): # Arrange @@ -195,7 +196,7 @@ def test_get_all_when_load_all_is_true_returns_expected_instruments(self): # Assert self.assertTrue(len(instruments) > 0) self.assertEqual(dict, type(instruments)) - self.assertEqual(Symbol, type(next(iter(instruments)))) + self.assertEqual(InstrumentId, type(next(iter(instruments)))) def test_get_btcusdt_when_not_loaded_returns_none(self): # Arrange @@ -207,7 +208,7 @@ def test_get_btcusdt_when_not_loaded_returns_none(self): instrument_id = InstrumentId(Symbol("BTC/USDT"), Venue("BINANCE")) # Act - instrument = provider.get(instrument_id) + instrument = provider.find(instrument_id) # Assert self.assertIsNone(instrument) @@ -232,7 +233,7 @@ def test_get_btcusdt_when_loaded_returns_expected_instrument(self): instrument_id = InstrumentId(Symbol("BTC/USDT"), Venue("BINANCE")) # Act - instrument = provider.get(instrument_id) + instrument = provider.find(instrument_id) # Assert self.assertEqual(Instrument, type(instrument)) diff --git a/tests/integration_tests/adapters/ib/test_ib_providers.py b/tests/integration_tests/adapters/ib/test_ib_providers.py index d1e052caebe5..e4df57bfc4a9 100644 --- a/tests/integration_tests/adapters/ib/test_ib_providers.py +++ b/tests/integration_tests/adapters/ib/test_ib_providers.py @@ -17,9 +17,8 @@ import pickle from unittest.mock import MagicMock -#from nautilus_trader.adapters.ib.providers import IBInstrumentProvider -from nautilus_trader.model.currency import Currency -from nautilus_trader.model.enums import AssetClass +from nautilus_trader.adapters.ib.providers import IBInstrumentProvider +from nautilus_trader.model.identifiers import InstrumentId from nautilus_trader.model.identifiers import Symbol from nautilus_trader.model.identifiers import Venue from tests import TESTS_PACKAGE_ROOT @@ -27,36 +26,40 @@ TEST_PATH = TESTS_PACKAGE_ROOT + "/integration_tests/adapters/ib/responses/" -# class TestIBInstrumentProvider: -# -# def test_load_futures_contract_instrument(self): -# # Arrange -# mock_client = MagicMock() -# -# with open(TEST_PATH + "contract_details_cl.pickle", "rb") as file: -# details = pickle.load(file) -# -# print(details) -# mock_client.reqContractDetails.return_value = [details] -# -# provider = IBInstrumentProvider(client=mock_client) -# provider.connect() -# -# instrument_id = FutureInstrumentId( -# symbol=Symbol("CL"), -# exchange=Venue("NYMEX"), -# asset_class=AssetClass.COMMODITY, -# expiry="20211119", -# currency=Currency.from_str("USD"), -# multiplier=1000, -# ) -# -# # Act -# future = provider.load_future(instrument_id) -# -# # Assert -# assert instrument_id == future.instrument_id -# assert 1000, future.multiplier -# assert Decimal("0.01") == future.tick_size -# assert 2, future.price_precision -# # TODO: Test all properties +class TestIBInstrumentProvider: + + def test_load_futures_contract_instrument(self): + # Arrange + mock_client = MagicMock() + + with open(TEST_PATH + "contract_details_cl.pickle", "rb") as file: + details = pickle.load(file) + + print(details) + mock_client.reqContractDetails.return_value = [details] + + provider = IBInstrumentProvider(client=mock_client) + provider.connect() + + instrument_id = InstrumentId( + symbol=Symbol("CL"), + venue=Venue("NYMEX"), + ) + + details = { + "asset_class": "COMMODITY", + "expiry": "20211119", + "currency": "USD", + "multiplier": 1000, + } + + # Act + provider.load(instrument_id, details) + future = provider.find(instrument_id) + + # Assert + assert instrument_id == future.id + assert 1000, future.multiplier + assert Decimal("0.01") == future.tick_size + assert 2, future.price_precision + # TODO: Test all properties diff --git a/tests/integration_tests/adapters/oanda/test_oanda_providers.py b/tests/integration_tests/adapters/oanda/test_oanda_providers.py index 93f57e08d12f..3367a1a6f243 100644 --- a/tests/integration_tests/adapters/oanda/test_oanda_providers.py +++ b/tests/integration_tests/adapters/oanda/test_oanda_providers.py @@ -96,7 +96,7 @@ def test_get_audusd_when_not_loaded_returns_none(self): instrument_id = InstrumentId(Symbol("AUD/USD"), Venue("OANDA")) # Act - instrument = provider.get(instrument_id) + instrument = provider.find(instrument_id) # Assert self.assertIsNone(instrument) @@ -115,7 +115,7 @@ def test_get_audusd_when_loaded_returns_expected_instrument(self): instrument_id = InstrumentId(Symbol("AUD/USD"), Venue("OANDA")) # Act - instrument = provider.get(instrument_id) + instrument = provider.find(instrument_id) # Assert self.assertEqual(Instrument, type(instrument)) diff --git a/tests/performance_tests/test_perf_throttler.py b/tests/performance_tests/test_perf_throttler.py new file mode 100644 index 000000000000..ffe2be36ac79 --- /dev/null +++ b/tests/performance_tests/test_perf_throttler.py @@ -0,0 +1,50 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +from datetime import timedelta + +from nautilus_trader.common.clock import TestClock +from nautilus_trader.common.logging import TestLogger +from nautilus_trader.common.throttler import Throttler +from tests.test_kit.performance import PerformanceHarness + + +class TestThrottlerPerformance: + + def setup(self): + # Fixture setup + self.clock = TestClock() + self.logger = TestLogger(self.clock, bypass_logging=True) + + self.handler = [] + self.throttler = Throttler( + name="Throttler-1", + limit=10000, + interval=timedelta(seconds=1), + output=self.handler.append, + clock=self.clock, + logger=self.logger, + ) + + def send(self): + self.throttler.send("MESSAGE") + + def test_send_unlimited(self): + PerformanceHarness.profile_function(self.send, 10000, 1) + # ~0.0ms / ~0.3μs / 301ns minimum of 10,000 runs @ 1 iteration each run. + + def test_send_when_limited(self): + PerformanceHarness.profile_function(self.send, 100000, 1) + # ~0.0ms / ~0.2μs / 232ns minimum of 100,000 runs @ 1 iteration each run. diff --git a/tests/unit_tests/backtest/test_backtest_engine.py b/tests/unit_tests/backtest/test_backtest_engine.py index 1fec9fa05d69..39c584befa8a 100644 --- a/tests/unit_tests/backtest/test_backtest_engine.py +++ b/tests/unit_tests/backtest/test_backtest_engine.py @@ -45,7 +45,7 @@ def setUp(self): self.engine = BacktestEngine( data=data, strategies=[TradingStrategy("000")], - use_tick_cache=True, + use_data_cache=True, ) self.engine.add_exchange( diff --git a/tests/unit_tests/backtest/test_backtest_loaders.py b/tests/unit_tests/backtest/test_backtest_loaders.py index e75a76dcbdcf..e9f0af324df5 100644 --- a/tests/unit_tests/backtest/test_backtest_loaders.py +++ b/tests/unit_tests/backtest/test_backtest_loaders.py @@ -16,8 +16,6 @@ from decimal import Decimal import unittest -from nautilus_trader.model.currency import Currency -from nautilus_trader.model.enums import CurrencyType from nautilus_trader.model.identifiers import InstrumentId from nautilus_trader.model.identifiers import Symbol from nautilus_trader.model.identifiers import Venue @@ -38,7 +36,8 @@ def test_default_fx_with_5_dp_returns_expected_instrument(self): self.assertEqual(InstrumentId(Symbol("AUD/USD"), Venue("SIM")), instrument.id) self.assertEqual(5, instrument.price_precision) self.assertEqual(Decimal("0.00001"), instrument.tick_size) - self.assertEqual(Currency(code="USD", precision=2, currency_type=CurrencyType.FIAT), instrument.quote_currency) + self.assertEqual("AUD", instrument.base_currency.code) + self.assertEqual("USD", instrument.quote_currency.code) def test_default_fx_with_3_dp_returns_expected_instrument(self): # Arrange @@ -51,7 +50,8 @@ def test_default_fx_with_3_dp_returns_expected_instrument(self): self.assertEqual(InstrumentId(Symbol("USD/JPY"), Venue("SIM")), instrument.id) self.assertEqual(3, instrument.price_precision) self.assertEqual(Decimal("0.001"), instrument.tick_size) - self.assertEqual(Currency(code='JPY', precision=2, currency_type=CurrencyType.FIAT), instrument.quote_currency) + self.assertEqual("USD", instrument.base_currency.code) + self.assertEqual("JPY", instrument.quote_currency.code) class ParquetTickDataLoadersTests(unittest.TestCase): diff --git a/tests/unit_tests/live/test_live_providers.py b/tests/unit_tests/common/test_common_providers.py similarity index 66% rename from tests/unit_tests/live/test_live_providers.py rename to tests/unit_tests/common/test_common_providers.py index 1b7a23a85483..c9fe154616cd 100644 --- a/tests/unit_tests/live/test_live_providers.py +++ b/tests/unit_tests/common/test_common_providers.py @@ -14,9 +14,10 @@ # ------------------------------------------------------------------------------------------------- import asyncio -import unittest -from nautilus_trader.live.providers import InstrumentProvider +import pytest + +from nautilus_trader.common.providers import InstrumentProvider from nautilus_trader.model.identifiers import Venue from tests.test_kit.stubs import TestStubs @@ -24,15 +25,12 @@ AUDUSD = TestStubs.audusd_id() -class LiveProvidersTests(unittest.TestCase): +class TestInstrumentProvider: - def setUp(self): + def setup(self): # Fixture Setup - self.provider = InstrumentProvider( - venue=BITMEX, - load_all=False, - ) + self.provider = InstrumentProvider() def test_load_all_async_when_not_implemented_raises_exception(self): # Fresh isolated loop testing pattern @@ -43,10 +41,8 @@ async def run_test(): # Arrange # Act # Assert - try: + with pytest.raises(NotImplementedError): await self.provider.load_all_async() - except NotImplementedError as ex: - self.assertEqual(NotImplementedError, type(ex)) loop.run_until_complete(run_test()) @@ -54,22 +50,28 @@ def test_load_all_when_not_implemented_raises_exception(self): # Arrange # Act # Assert - self.assertRaises(NotImplementedError, self.provider.load_all) + with pytest.raises(NotImplementedError): + self.provider.load_all() - def test_get_all_when_not_implemented_raises_exception(self): + def test_load_when_not_implemented_raises_exception(self): # Arrange # Act # Assert - self.assertRaises(NotImplementedError, self.provider.get_all) + with pytest.raises(NotImplementedError): + self.provider.load(AUDUSD, {}) - def test_get_when_not_implemented_raises_exception(self): + def test_get_all_when_no_instruments_returns_empty_dict(self): # Arrange # Act + result = self.provider.get_all() + # Assert - self.assertRaises(NotImplementedError, self.provider.get, AUDUSD) + assert result == {} - def test_currency_when_not_implemented_raises_exception(self): + def test_find_when_no_instruments_returns_none(self): # Arrange # Act + result = self.provider.find(AUDUSD) + # Assert - self.assertRaises(NotImplementedError, self.provider.currency, "BTC") + assert result is None diff --git a/tests/unit_tests/common/test_common_queue.py b/tests/unit_tests/common/test_common_queue.py index b34bd4282086..68fd2008d2ed 100644 --- a/tests/unit_tests/common/test_common_queue.py +++ b/tests/unit_tests/common/test_common_queue.py @@ -14,12 +14,13 @@ # ------------------------------------------------------------------------------------------------- import asyncio -import unittest + +import pytest from nautilus_trader.common.queue import Queue -class QueueTests(unittest.TestCase): +class TestQueue: def test_queue_instantiation(self): # Arrange @@ -27,10 +28,10 @@ def test_queue_instantiation(self): # Act # Assert - self.assertEqual(0, queue.maxsize) - self.assertEqual(0, queue.qsize()) - self.assertTrue(queue.empty()) - self.assertFalse(queue.full()) + assert queue.maxsize == 0 + assert queue.qsize() == 0 + assert queue.empty() + assert not queue.full() def test_put_nowait(self): # Arrange @@ -40,8 +41,8 @@ def test_put_nowait(self): queue.put_nowait("A") # Assert - self.assertEqual(1, queue.qsize()) - self.assertFalse(queue.empty()) + assert queue.qsize() == 1 + assert not queue.empty() def test_get_nowait(self): # Arrange @@ -52,8 +53,8 @@ def test_get_nowait(self): item = queue.get_nowait() # Assert - self.assertEqual(0, queue.qsize()) - self.assertEqual("A", item) + assert queue.empty() + assert item == "A" def test_put_nowait_multiple_items(self): # Arrange @@ -67,8 +68,8 @@ def test_put_nowait_multiple_items(self): queue.put_nowait("E") # Assert - self.assertEqual(5, queue.qsize()) - self.assertFalse(queue.empty()) + assert queue.qsize() == 5 + assert not queue.empty() def test_put_to_maxlen_makes_queue_full(self): # Arrange @@ -82,8 +83,8 @@ def test_put_to_maxlen_makes_queue_full(self): queue.put_nowait("E") # Assert - self.assertEqual(5, queue.qsize()) - self.assertTrue(queue.full()) + assert queue.qsize() == 5 + assert queue.full() def test_put_nowait_onto_queue_at_maxsize_raises_queue_full(self): # Arrange @@ -97,7 +98,8 @@ def test_put_nowait_onto_queue_at_maxsize_raises_queue_full(self): queue.put_nowait("E") # Assert - self.assertRaises(asyncio.QueueFull, queue.put_nowait, "F") + with pytest.raises(asyncio.QueueFull): + queue.put_nowait("F") def test_get_nowait_from_empty_queue_raises_queue_empty(self): # Arrange @@ -105,7 +107,8 @@ def test_get_nowait_from_empty_queue_raises_queue_empty(self): # Act # Assert - self.assertRaises(asyncio.QueueEmpty, queue.get_nowait) + with pytest.raises(asyncio.QueueEmpty): + queue.get_nowait() def test_await_put(self): # Fresh isolated loop testing pattern @@ -121,8 +124,8 @@ async def run_test(): item = queue.get_nowait() # Assert - self.assertEqual(0, queue.qsize()) - self.assertEqual("A", item) + assert queue.empty() + assert item == "A" self.loop.run_until_complete(run_test()) @@ -140,7 +143,7 @@ async def run_test(): item = await queue.get() # Assert - self.assertEqual(0, queue.qsize()) - self.assertEqual("A", item) + assert queue.empty() + assert item == "A" self.loop.run_until_complete(run_test()) diff --git a/tests/unit_tests/common/test_common_throttler.py b/tests/unit_tests/common/test_common_throttler.py new file mode 100644 index 000000000000..748dc51ea857 --- /dev/null +++ b/tests/unit_tests/common/test_common_throttler.py @@ -0,0 +1,102 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +from datetime import timedelta + +from nautilus_trader.common.clock import TestClock +from nautilus_trader.common.logging import TestLogger +from nautilus_trader.common.throttler import Throttler +from tests.test_kit.stubs import UNIX_EPOCH + + +class TestThrottler: + + def setup(self): + # Fixture setup + self.clock = TestClock() + self.logger = TestLogger(self.clock) + + self.handler = [] + self.throttler = Throttler( + name="Throttler-1", + limit=5, + interval=timedelta(seconds=1), + output=self.handler.append, + clock=self.clock, + logger=self.logger, + ) + + def test_throttler_instantiation(self): + # Arrange + # Act + # Assert + assert self.throttler.name == "Throttler-1" + assert self.throttler.qsize == 0 + assert not self.throttler.is_active + assert not self.throttler.is_throttling + + def test_send_when_not_active_becomes_active(self): + # Arrange + item = "MESSAGE" + + # Act + self.throttler.send(item) + + # Assert + assert self.throttler.is_active + assert not self.throttler.is_throttling + assert self.handler == ["MESSAGE"] + + def test_send_to_limit_becomes_throttled(self): + # Arrange + item = "MESSAGE" + + # Act: Send 6 items + self.throttler.send(item) + self.throttler.send(item) + self.throttler.send(item) + self.throttler.send(item) + self.throttler.send(item) + self.throttler.send(item) + + # Assert: Only 5 items are sent + assert self.clock.timer_names() == ["Throttler-1-REFRESH-TOKEN"] + assert self.throttler.is_active + assert self.throttler.is_throttling + assert self.handler == ["MESSAGE"] * 5 + assert self.throttler.qsize == 1 + + def test_refresh_when_at_limit_sends_remaining_items(self): + # Arrange + item = "MESSAGE" + + # Act: Send 6 items + self.throttler.send(item) + self.throttler.send(item) + self.throttler.send(item) + self.throttler.send(item) + self.throttler.send(item) + self.throttler.send(item) + + # Act: Trigger refresh token time alert + events = self.clock.advance_time(UNIX_EPOCH + timedelta(seconds=1)) + events[0].handle_py() + + # Assert: Remaining items sent + assert self.clock.timer_names() == ["Throttler-1-REFRESH-TOKEN"] + assert self.throttler.is_active + assert self.throttler.is_throttling is False + assert self.handler == ["MESSAGE"] * 6 + assert self.throttler.qsize == 0 diff --git a/tests/unit_tests/core/test_core_cache.py b/tests/unit_tests/core/test_core_cache.py index a8f43122d256..5577517d76a4 100644 --- a/tests/unit_tests/core/test_core_cache.py +++ b/tests/unit_tests/core/test_core_cache.py @@ -23,7 +23,7 @@ class TestObjectCache: def test_cache_initialization(self): # Arrange - cache = ObjectCache(InstrumentId, InstrumentId.from_serializable_str) + cache = ObjectCache(InstrumentId, InstrumentId.from_str) # Act # Assert @@ -41,7 +41,7 @@ def test_cache_initialization(self): ) def test_get_given_none_raises_value_error(self, value, ex): # Arrange - cache = ObjectCache(InstrumentId, InstrumentId.from_serializable_str) + cache = ObjectCache(InstrumentId, InstrumentId.from_str) # Act # Assert @@ -50,19 +50,19 @@ def test_get_given_none_raises_value_error(self, value, ex): def test_get_from_empty_cache(self): # Arrange - cache = ObjectCache(InstrumentId, InstrumentId.from_serializable_str) + cache = ObjectCache(InstrumentId, InstrumentId.from_str) instrument_id = "AUD/USD.SIM,FX,SPOT" # Act result = cache.get(instrument_id) # Assert - assert instrument_id == result.to_serializable_str() + assert instrument_id == str(result) assert ["AUD/USD.SIM,FX,SPOT"] == cache.keys() def test_get_from_cache(self): # Arrange - cache = ObjectCache(InstrumentId, InstrumentId.from_serializable_str) + cache = ObjectCache(InstrumentId, InstrumentId.from_str) instrument_id = "AUD/USD.SIM,FX,SPOT" cache.get(instrument_id) @@ -72,13 +72,13 @@ def test_get_from_cache(self): result2 = cache.get(instrument_id) # Assert - assert instrument_id == result1.to_serializable_str() + assert instrument_id == str(result1) assert id(result1) == id(result2) assert ["AUD/USD.SIM,FX,SPOT"] == cache.keys() def test_keys_when_cache_empty_returns_empty_list(self): # Arrange - cache = ObjectCache(InstrumentId, InstrumentId.from_serializable_str) + cache = ObjectCache(InstrumentId, InstrumentId.from_str) # Act result = cache.keys() @@ -88,7 +88,7 @@ def test_keys_when_cache_empty_returns_empty_list(self): def test_clear_cache(self): # Arrange - cache = ObjectCache(InstrumentId, InstrumentId.from_serializable_str) + cache = ObjectCache(InstrumentId, InstrumentId.from_str) instrument_id = "AUD/USD.SIM,FX,SPOT" cache.get(instrument_id) diff --git a/tests/unit_tests/data/test_data_client.py b/tests/unit_tests/data/test_data_client.py index 3b9a592ae5c2..a1fa437e6a28 100644 --- a/tests/unit_tests/data/test_data_client.py +++ b/tests/unit_tests/data/test_data_client.py @@ -23,6 +23,7 @@ from nautilus_trader.data.engine import DataEngine from nautilus_trader.model.bar import Bar from nautilus_trader.model.data import DataType +from nautilus_trader.model.data import GenericData from nautilus_trader.model.enums import OrderSide from nautilus_trader.model.identifiers import TradeMatchId from nautilus_trader.model.identifiers import Venue @@ -114,19 +115,21 @@ def test_request_when_not_implemented_raises_exception(self): def test_handle_data_sends_to_data_engine(self): # Arrange data_type = DataType(str, {"Type": "NEWS_WIRE"}) + data = GenericData(data_type, "Some news headline", UNIX_EPOCH) # Act - self.client._handle_data_py(data_type, "Some news headline") + self.client._handle_data_py(data) # Assert self.assertEqual(1, self.data_engine.data_count) def test_handle_data_response_sends_to_data_engine(self): # Arrange - data_type = DataType(float, {"Type": "ECONOMIC_DATA", "topic": "unemployment"}) + data_type = DataType(str, {"Type": "ECONOMIC_DATA", "topic": "unemployment"}) + data = GenericData(data_type, "may 2020, 6.9%", UNIX_EPOCH) # Act - self.client._handle_data_response_py(data_type, 6.6, self.uuid_factory.generate()) + self.client._handle_data_response_py(data, self.uuid_factory.generate()) # Assert self.assertEqual(1, self.data_engine.response_count) diff --git a/tests/unit_tests/execution/test_execution_engine.py b/tests/unit_tests/execution/test_execution_engine.py index e719666c13c7..fb3b9e8105b7 100644 --- a/tests/unit_tests/execution/test_execution_engine.py +++ b/tests/unit_tests/execution/test_execution_engine.py @@ -112,7 +112,7 @@ def setUp(self): def test_registered_venues_returns_expected(self): # Arrange # Act - result = self.exec_engine.registered_venues + result = self.exec_engine.registered_clients # Assert self.assertEqual([Venue("SIM")], result) @@ -123,7 +123,7 @@ def test_deregister_client_removes_client(self): self.exec_engine.deregister_client(self.exec_client) # Assert - self.assertEqual([], self.exec_engine.registered_venues) + self.assertEqual([], self.exec_engine.registered_clients) def test_register_strategy(self): # Arrange diff --git a/tests/unit_tests/live/test_live_execution_client.py b/tests/unit_tests/live/test_live_execution_client.py index dab45332270f..8c9fa21155c0 100644 --- a/tests/unit_tests/live/test_live_execution_client.py +++ b/tests/unit_tests/live/test_live_execution_client.py @@ -20,12 +20,12 @@ from nautilus_trader.common.clock import LiveClock from nautilus_trader.common.factories import OrderFactory from nautilus_trader.common.logging import TestLogger +from nautilus_trader.common.providers import InstrumentProvider from nautilus_trader.common.uuid import UUIDFactory from nautilus_trader.data.cache import DataCache from nautilus_trader.execution.database import BypassExecutionDatabase from nautilus_trader.live.execution_client import LiveExecutionClient from nautilus_trader.live.execution_engine import LiveExecutionEngine -from nautilus_trader.live.providers import InstrumentProvider from nautilus_trader.model.identifiers import StrategyId from nautilus_trader.model.identifiers import TraderId from nautilus_trader.model.identifiers import Venue @@ -80,12 +80,11 @@ def setUp(self): logger=self.logger, ) - instrument_provider = InstrumentProvider(venue=SIM, load_all=False) self.client = LiveExecutionClient( venue=SIM, account_id=self.account_id, engine=self.engine, - instrument_provider=instrument_provider, + instrument_provider=InstrumentProvider(), clock=self.clock, logger=self.logger, ) diff --git a/tests/unit_tests/live/test_live_execution_engine.py b/tests/unit_tests/live/test_live_execution_engine.py index 05d88b3b7bc5..5febe1f4e147 100644 --- a/tests/unit_tests/live/test_live_execution_engine.py +++ b/tests/unit_tests/live/test_live_execution_engine.py @@ -112,6 +112,14 @@ def test_start_when_loop_not_running_logs(self): self.assertTrue(True) # No exceptions raised self.engine.stop() + def test_get_event_loop_returns_expected_loop(self): + # Arrange + # Act + loop = self.engine.get_event_loop() + + # Assert + self.assertEqual(self.loop, loop) + def test_message_qsize_at_max_blocks_on_put_command(self): # Arrange self.engine = LiveExecutionEngine( @@ -204,14 +212,6 @@ def test_message_qsize_at_max_blocks_on_put_event(self): self.assertEqual(1, self.engine.qsize()) self.assertEqual(0, self.engine.command_count) - def test_get_event_loop_returns_expected_loop(self): - # Arrange - # Act - loop = self.engine.get_event_loop() - - # Assert - self.assertEqual(self.loop, loop) - def test_start(self): async def run_test(): # Arrange diff --git a/tests/unit_tests/live/test_live_risk_engine.py b/tests/unit_tests/live/test_live_risk_engine.py new file mode 100644 index 000000000000..5e8cb62868a1 --- /dev/null +++ b/tests/unit_tests/live/test_live_risk_engine.py @@ -0,0 +1,336 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +import asyncio + +from nautilus_trader.analysis.performance import PerformanceAnalyzer +from nautilus_trader.common.clock import LiveClock +from nautilus_trader.common.enums import ComponentState +from nautilus_trader.common.factories import OrderFactory +from nautilus_trader.common.logging import TestLogger +from nautilus_trader.common.uuid import UUIDFactory +from nautilus_trader.data.cache import DataCache +from nautilus_trader.execution.database import BypassExecutionDatabase +from nautilus_trader.live.execution_engine import LiveExecutionEngine +from nautilus_trader.live.risk_engine import LiveRiskEngine +from nautilus_trader.model.commands import SubmitOrder +from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.identifiers import PositionId +from nautilus_trader.model.identifiers import StrategyId +from nautilus_trader.model.identifiers import TraderId +from nautilus_trader.model.identifiers import Venue +from nautilus_trader.model.objects import Quantity +from nautilus_trader.trading.portfolio import Portfolio +from nautilus_trader.trading.strategy import TradingStrategy +from tests.test_kit.mocks import MockExecutionClient +from tests.test_kit.providers import TestInstrumentProvider +from tests.test_kit.stubs import TestStubs + +SIM = Venue("SIM") +AUDUSD_SIM = TestInstrumentProvider.default_fx_ccy("AUD/USD") +GBPUSD_SIM = TestInstrumentProvider.default_fx_ccy("GBP/USD") + + +class TestLiveRiskEngine: + + def setup(self): + # Fixture Setup + self.clock = LiveClock() + self.uuid_factory = UUIDFactory() + self.logger = TestLogger(self.clock) + + self.trader_id = TraderId("TESTER", "000") + self.account_id = TestStubs.account_id() + + self.order_factory = OrderFactory( + trader_id=self.trader_id, + strategy_id=StrategyId("S", "001"), + clock=self.clock, + ) + + self.random_order_factory = OrderFactory( + trader_id=TraderId("RANDOM", "042"), + strategy_id=StrategyId("S", "042"), + clock=self.clock, + ) + + self.portfolio = Portfolio( + clock=self.clock, + logger=self.logger, + ) + self.portfolio.register_cache(DataCache(self.logger)) + + self.analyzer = PerformanceAnalyzer() + + # Fresh isolated loop testing pattern + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + self.database = BypassExecutionDatabase(trader_id=self.trader_id, logger=self.logger) + self.exec_engine = LiveExecutionEngine( + loop=self.loop, + database=self.database, + portfolio=self.portfolio, + clock=self.clock, + logger=self.logger, + ) + + self.venue = Venue("SIM") + self.exec_client = MockExecutionClient( + self.venue, + self.account_id, + self.exec_engine, + self.clock, + self.logger, + ) + + self.risk_engine = LiveRiskEngine( + loop=self.loop, + exec_engine=self.exec_engine, + portfolio=self.portfolio, + clock=self.clock, + logger=self.logger, + config={}, + ) + + self.exec_engine.register_client(self.exec_client) + self.exec_engine.register_risk_engine(self.risk_engine) + + def test_start_when_loop_not_running_logs(self): + # Arrange + # Act + self.risk_engine.start() + + # Assert + assert True + self.risk_engine.stop() + + def test_get_event_loop_returns_expected_loop(self): + # Arrange + # Act + loop = self.risk_engine.get_event_loop() + + # Assert + assert loop == self.loop + + def test_message_qsize_at_max_blocks_on_put_command(self): + # Arrange + self.risk_engine = LiveRiskEngine( + loop=self.loop, + exec_engine=self.exec_engine, + portfolio=self.portfolio, + clock=self.clock, + logger=self.logger, + config={"qsize": 1} + ) + + strategy = TradingStrategy(order_id_tag="001") + strategy.register_trader( + TraderId("TESTER", "000"), + self.clock, + self.logger, + ) + + self.exec_engine.register_strategy(strategy) + + order = strategy.order_factory.market( + AUDUSD_SIM.id, + OrderSide.BUY, + Quantity(100000), + ) + + submit_order = SubmitOrder( + Venue("SIM"), + self.trader_id, + self.account_id, + strategy.id, + PositionId.null(), + order, + self.uuid_factory.generate(), + self.clock.utc_now(), + ) + + # Act + self.risk_engine.execute(submit_order) + self.risk_engine.execute(submit_order) + + # Assert + assert self.risk_engine.qsize() == 1 + assert self.risk_engine.command_count == 0 + + def test_message_qsize_at_max_blocks_on_put_event(self): + # Arrange + self.risk_engine = LiveRiskEngine( + loop=self.loop, + exec_engine=self.exec_engine, + portfolio=self.portfolio, + clock=self.clock, + logger=self.logger, + config={"qsize": 1} + ) + + strategy = TradingStrategy(order_id_tag="001") + strategy.register_trader( + TraderId("TESTER", "000"), + self.clock, + self.logger, + ) + + self.exec_engine.register_strategy(strategy) + + order = strategy.order_factory.market( + AUDUSD_SIM.id, + OrderSide.BUY, + Quantity(100000), + ) + + submit_order = SubmitOrder( + Venue("SIM"), + self.trader_id, + self.account_id, + strategy.id, + PositionId.null(), + order, + self.uuid_factory.generate(), + self.clock.utc_now(), + ) + + event = TestStubs.event_order_submitted(order) + + # Act + self.risk_engine.execute(submit_order) + self.risk_engine.process(event) # Add over max size + + # Assert + assert self.risk_engine.qsize() == 1 + assert self.risk_engine.event_count == 0 + + def test_start(self): + async def run_test(): + # Arrange + # Act + self.risk_engine.start() + await asyncio.sleep(0.1) + + # Assert + assert self.risk_engine.state == ComponentState.RUNNING + + # Tear Down + self.risk_engine.stop() + + self.loop.run_until_complete(run_test()) + + def test_kill_when_running_and_no_messages_on_queues(self): + async def run_test(): + # Arrange + # Act + self.risk_engine.start() + await asyncio.sleep(0) + self.risk_engine.kill() + + # Assert + assert self.risk_engine.state == ComponentState.STOPPED + + self.loop.run_until_complete(run_test()) + + def test_kill_when_not_running_with_messages_on_queue(self): + async def run_test(): + # Arrange + # Act + self.risk_engine.kill() + + # Assert + assert self.risk_engine.qsize() == 0 + + self.loop.run_until_complete(run_test()) + + def test_execute_command_places_command_on_queue(self): + async def run_test(): + # Arrange + self.risk_engine.start() + + strategy = TradingStrategy(order_id_tag="001") + strategy.register_trader( + TraderId("TESTER", "000"), + self.clock, + self.logger, + ) + + self.exec_engine.register_strategy(strategy) + + order = strategy.order_factory.market( + AUDUSD_SIM.id, + OrderSide.BUY, + Quantity(100000), + ) + + submit_order = SubmitOrder( + Venue("SIM"), + self.trader_id, + self.account_id, + strategy.id, + PositionId.null(), + order, + self.uuid_factory.generate(), + self.clock.utc_now(), + ) + + # Act + self.risk_engine.execute(submit_order) + await asyncio.sleep(0.1) + + # Assert + assert self.risk_engine.qsize() == 0 + assert self.risk_engine.command_count == 1 + + # Tear Down + self.risk_engine.stop() + + self.loop.run_until_complete(run_test()) + + def test_handle_position_opening_with_position_id_none(self): + async def run_test(): + # Arrange + self.risk_engine.start() + + strategy = TradingStrategy(order_id_tag="001") + strategy.register_trader( + TraderId("TESTER", "000"), + self.clock, + self.logger, + ) + + self.exec_engine.register_strategy(strategy) + + order = strategy.order_factory.market( + AUDUSD_SIM.id, + OrderSide.BUY, + Quantity(100000), + ) + + event = TestStubs.event_order_submitted(order) + + # Act + self.risk_engine.process(event) + await asyncio.sleep(0.1) + + # Assert + assert self.risk_engine.qsize() == 0 + assert self.risk_engine.event_count == 1 + + # Tear Down + self.risk_engine.stop() + + self.loop.run_until_complete(run_test()) diff --git a/tests/unit_tests/model/test_model_currency.py b/tests/unit_tests/model/test_model_currency.py index 0cfead1db1b0..b53f9a57fddb 100644 --- a/tests/unit_tests/model/test_model_currency.py +++ b/tests/unit_tests/model/test_model_currency.py @@ -31,9 +31,29 @@ class TestCurrency: def test_currency_equality(self): # Arrange - currency1 = Currency("AUD", precision=2, currency_type=CurrencyType.FIAT) - currency2 = Currency("AUD", precision=2, currency_type=CurrencyType.FIAT) - currency3 = Currency("GBP", precision=2, currency_type=CurrencyType.FIAT) + currency1 = Currency( + code="AUD", + precision=2, + iso4217=36, + name="Australian dollar", + currency_type=CurrencyType.FIAT, + ) + + currency2 = Currency( + code="AUD", + precision=2, + iso4217=36, + name="Australian dollar", + currency_type=CurrencyType.FIAT, + ) + + currency3 = Currency( + code="GBP", + precision=2, + iso4217=826, + name="British pound", + currency_type=CurrencyType.FIAT, + ) # Act # Assert @@ -43,7 +63,13 @@ def test_currency_equality(self): def test_currency_hash(self): # Arrange - currency = Currency("AUD", precision=2, currency_type=CurrencyType.FIAT) + currency = Currency( + code="AUD", + precision=2, + iso4217=36, + name="Australian dollar", + currency_type=CurrencyType.FIAT, + ) # Act # Assert @@ -52,12 +78,18 @@ def test_currency_hash(self): def test_str_repr(self): # Arrange - currency = Currency("AUD", precision=2, currency_type=CurrencyType.FIAT) + currency = Currency( + code="AUD", + precision=2, + iso4217=36, + name="Australian dollar", + currency_type=CurrencyType.FIAT, + ) # Act # Assert assert "AUD" == str(currency) - assert "Currency(code=AUD, precision=2, type=FIAT)" == repr(currency) + assert "Currency(code=AUD, name=Australian dollar, precision=2, iso4217=36, type=FIAT)" == repr(currency) def test_from_str_given_unknown_code_returns_none(self): # Arrange diff --git a/tests/unit_tests/model/test_model_identifiers.py b/tests/unit_tests/model/test_model_identifiers.py index f40a15e84598..46c05efe112b 100644 --- a/tests/unit_tests/model/test_model_identifiers.py +++ b/tests/unit_tests/model/test_model_identifiers.py @@ -256,7 +256,7 @@ def test_parse_instrument_id_from_str(self): instrument_id = InstrumentId(Symbol("AUD/USD"), Venue("SIM")) # Act - result = InstrumentId.from_serializable_str(instrument_id.to_serializable_str()) + result = InstrumentId.from_str(str(instrument_id)) # Assert assert instrument_id == result diff --git a/tests/unit_tests/risk/test_risk_engine.py b/tests/unit_tests/risk/test_risk_engine.py index d54209a09138..59b9babd470c 100644 --- a/tests/unit_tests/risk/test_risk_engine.py +++ b/tests/unit_tests/risk/test_risk_engine.py @@ -16,10 +16,14 @@ from nautilus_trader.common.clock import TestClock from nautilus_trader.common.logging import TestLogger from nautilus_trader.common.uuid import UUIDFactory +from nautilus_trader.core.message import Event from nautilus_trader.data.cache import DataCache from nautilus_trader.execution.engine import ExecutionEngine +from nautilus_trader.model.commands import AmendOrder +from nautilus_trader.model.commands import CancelOrder from nautilus_trader.model.commands import SubmitBracketOrder from nautilus_trader.model.commands import SubmitOrder +from nautilus_trader.model.commands import TradingCommand from nautilus_trader.model.enums import OrderSide from nautilus_trader.model.identifiers import PositionId from nautilus_trader.model.identifiers import TraderId @@ -82,6 +86,14 @@ def setup(self): self.exec_engine.register_client(self.exec_client) self.exec_engine.register_risk_engine(self.risk_engine) + def test_registered_clients_returns_expected_list(self): + # Arrange + # Act + result = self.risk_engine.registered_clients + + # Assert + assert result == [Venue('SIM')] + def test_set_block_all_orders_changes_flag_value(self): # Arrange # Act @@ -90,7 +102,26 @@ def test_set_block_all_orders_changes_flag_value(self): # Assert assert self.risk_engine.block_all_orders - def test_approve_order_when_engine_not_overridden_then_approves(self): + def test_given_random_command_logs_and_continues(self): + # Arrange + random = TradingCommand( + self.venue, + self.uuid_factory.generate(), + self.clock.utc_now(), + ) + + self.risk_engine.execute(random) + + def test_given_random_event_logs_and_continues(self): + # Arrange + random = Event( + self.uuid_factory.generate(), + self.clock.utc_now(), + ) + + self.exec_engine.process(random) + + def test_submit_order_with_default_settings_sends_to_client(self): # Arrange self.exec_engine.start() @@ -121,12 +152,12 @@ def test_approve_order_when_engine_not_overridden_then_approves(self): ) # Act - self.risk_engine.approve_order(submit_order) + self.risk_engine.execute(submit_order) # Assert - assert submit_order.approved + assert self.exec_client.calls == ['connect', 'submit_order'] - def test_approve_bracket_when_engine_not_overridden_then_approves(self): + def test_submit_bracket_with_default_settings_sends_to_client(self): # Arrange self.exec_engine.start() @@ -162,10 +193,10 @@ def test_approve_bracket_when_engine_not_overridden_then_approves(self): ) # Act - self.risk_engine.approve_bracket(submit_bracket) + self.risk_engine.execute(submit_bracket) # Assert - assert submit_bracket.approved + assert self.exec_client.calls == ['connect', 'submit_bracket_order'] def test_submit_order_when_block_all_orders_true_then_denies_order(self): # Arrange @@ -203,7 +234,105 @@ def test_submit_order_when_block_all_orders_true_then_denies_order(self): self.exec_engine.execute(submit_order) # Assert - assert not submit_order.approved + assert self.exec_client.calls == ['connect'] + assert self.exec_engine.event_count == 1 + + def test_amend_order_with_default_settings_sends_to_client(self): + # Arrange + self.exec_engine.start() + + strategy = TradingStrategy(order_id_tag="001") + strategy.register_trader( + TraderId("TESTER", "000"), + self.clock, + self.logger, + ) + + self.exec_engine.register_strategy(strategy) + + order = strategy.order_factory.market( + AUDUSD_SIM.id, + OrderSide.BUY, + Quantity(100000), + ) + + submit = SubmitOrder( + self.venue, + self.trader_id, + self.account_id, + strategy.id, + PositionId.null(), + order, + self.uuid_factory.generate(), + self.clock.utc_now(), + ) + + amend = AmendOrder( + self.venue, + self.trader_id, + self.account_id, + order.cl_ord_id, + order.quantity, + Price("1.00010"), + self.uuid_factory.generate(), + self.clock.utc_now(), + ) + + self.risk_engine.execute(submit) + + # Act + self.risk_engine.execute(amend) + + # Assert + assert self.exec_client.calls == ['connect', 'submit_order', 'amend_order'] + + def test_cancel_order_with_default_settings_sends_to_client(self): + # Arrange + self.exec_engine.start() + + strategy = TradingStrategy(order_id_tag="001") + strategy.register_trader( + TraderId("TESTER", "000"), + self.clock, + self.logger, + ) + + self.exec_engine.register_strategy(strategy) + + order = strategy.order_factory.market( + AUDUSD_SIM.id, + OrderSide.BUY, + Quantity(100000), + ) + + submit = SubmitOrder( + self.venue, + self.trader_id, + self.account_id, + strategy.id, + PositionId.null(), + order, + self.uuid_factory.generate(), + self.clock.utc_now(), + ) + + cancel = CancelOrder( + self.venue, + self.trader_id, + self.account_id, + order.cl_ord_id, + order.id, + self.uuid_factory.generate(), + self.clock.utc_now(), + ) + + self.risk_engine.execute(submit) + + # Act + self.risk_engine.execute(cancel) + + # Assert + assert self.exec_client.calls == ['connect', 'submit_order', 'cancel_order'] def test_submit_bracket_when_block_all_orders_true_then_denies_order(self): # Arrange @@ -243,7 +372,8 @@ def test_submit_bracket_when_block_all_orders_true_then_denies_order(self): self.risk_engine.set_block_all_orders() # Act - self.risk_engine.approve_bracket(submit_bracket) + self.exec_engine.execute(submit_bracket) # Assert - assert not submit_bracket.approved + assert self.exec_client.calls == ['connect'] + assert self.exec_engine.event_count == 3 diff --git a/tests/unit_tests/trading/test_trading_trader.py b/tests/unit_tests/trading/test_trading_trader.py index feaf541b3999..0e8a17758601 100644 --- a/tests/unit_tests/trading/test_trading_trader.py +++ b/tests/unit_tests/trading/test_trading_trader.py @@ -33,6 +33,7 @@ from nautilus_trader.model.identifiers import TraderId from nautilus_trader.model.identifiers import Venue from nautilus_trader.model.objects import Money +from nautilus_trader.risk.engine import RiskEngine from nautilus_trader.trading.portfolio import Portfolio from nautilus_trader.trading.strategy import TradingStrategy from nautilus_trader.trading.trader import Trader @@ -110,6 +111,14 @@ def setUp(self): logger=logger, ) + self.risk_engine = RiskEngine( + exec_engine=self.exec_engine, + portfolio=self.portfolio, + clock=clock, + logger=logger, + ) + + self.exec_engine.register_risk_engine(self.risk_engine) self.exec_engine.register_client(self.exec_client) strategies = [ @@ -123,6 +132,7 @@ def setUp(self): portfolio=self.portfolio, data_engine=self.data_engine, exec_engine=self.exec_engine, + risk_engine=self.risk_engine, clock=clock, logger=logger, )