diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4657e06d2d17..57a9a3751522 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -73,7 +73,7 @@ repos: types: [python] - repo: https://github.com/psf/black - rev: 24.2.0 + rev: 24.3.0 hooks: - id: black types_or: [python, pyi] @@ -82,7 +82,7 @@ repos: exclude: "docs/_pygments/monokai.py" - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.2 + rev: v0.3.4 hooks: - id: ruff args: ["--fix"] diff --git a/README.md b/README.md index cc300b40a8f8..73a48897ff9d 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,10 @@ | Platform | Rust | Python | | :----------------- | :------ | :----- | -| `Linux (x86_64)` | 1.76.0+ | 3.10+ | -| `macOS (x86_64)` | 1.76.0+ | 3.10+ | -| `macOS (arm64)` | 1.76.0+ | 3.10+ | -| `Windows (x86_64)` | 1.76.0+ | 3.10+ | +| `Linux (x86_64)` | 1.77.0+ | 3.10+ | +| `macOS (x86_64)` | 1.77.0+ | 3.10+ | +| `macOS (arm64)` | 1.77.0+ | 3.10+ | +| `Windows (x86_64)` | 1.77.0+ | 3.10+ | - **Website:** https://nautilustrader.io - **Docs:** https://docs.nautilustrader.io @@ -135,20 +135,28 @@ This project makes the [Soundness Pledge](https://raphlinus.github.io/rust/2020/ ## Integrations -NautilusTrader is designed in a modular way to work with 'adapters' which provide -connectivity to data providers and/or trading venues - converting their raw API +NautilusTrader is designed in a modular way to work with _adapters_ which provide +connectivity to trading venues and data providers - converting their raw API into a unified interface. The following integrations are currently supported: | Name | ID | Type | Status | Docs | | :-------------------------------------------------------- | :-------------------- | :---------------------- | :------------------------------------------------------ | :------------------------------------------------------------------ | -| [Betfair](https://betfair.com) | `BETFAIR` | Sports betting exchange | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/betfair.html) | -| [Binance](https://binance.com) | `BINANCE` | Crypto exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | -| [Binance US](https://binance.us) | `BINANCE` | Crypto exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | -| [Binance Futures](https://www.binance.com/en/futures) | `BINANCE` | Crypto exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | -| [Bybit](https://www.bybit.com) | `BYBIT` | Crypto exchange (CEX) | ![status](https://img.shields.io/badge/building-orange) | | -| [Databento](https://databento.com) | `DATABENTO` | Data provider | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/databento.html) | +| [Betfair](https://betfair.com) | `BETFAIR` | Sports Betting Exchange | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/betfair.html) | +| [Binance](https://binance.com) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | +| [Binance US](https://binance.us) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | +| [Binance Futures](https://www.binance.com/en/futures) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | +| [Bybit](https://www.bybit.com) | `BYBIT` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/building-orange) | | +| [Databento](https://databento.com) | `DATABENTO` | Data Provider | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/databento.html) | | [Interactive Brokers](https://www.interactivebrokers.com) | `INTERACTIVE_BROKERS` | Brokerage (multi-venue) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/ib.html) | +- `ID:` The default client ID for the integrations adapter clients +- `Type:` The type of integration (often the venue type) + +### Status +- `building` - Under construction and likely not in a usable state +- `beta` - Completed to a minimally working state and in a 'beta' testing phase +- `stable` - Stabilized feature set and API, the integration has been tested by both developers and users to a reasonable level (some bugs may still remain) + Refer to the [Integrations](https://docs.nautilustrader.io/integrations/index.html) documentation for further details. ## Installation diff --git a/RELEASES.md b/RELEASES.md index faad6f5c6084..6d5588599992 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,30 @@ +# NautilusTrader 1.190.0 Beta + +Released on 22nd March 2024 (UTC). + +### Enhancements +- Added Databento adapter `continuous`, `parent` and `instrument_id` symbology support (will infer from symbols) +- Added `DatabaseConfig.timeout` config option for timeout seconds to wait for a new connection +- Added CSV tick and bar data loader params, thanks @rterbush +- Implemented `LogGuard` to ensure global logger is flushed on termination, thanks @ayush-sb and @twitu +- Improved Interactive Brokers client connectivity resilience and component lifecycle, thanks @benjaminsingleton +- Improved Binance execution client ping listen key error handling and logging +- Improved Redis cache adapter and message bus error handling and logging +- Improved Redis port parsing (`DatabaseConfig.port` can now be either a string or integer) +- Refactored `InteractiveBrokersEWrapper`, thanks @rsmb7z +- Redact Redis passwords in strings and logs +- Upgraded `redis` crate to 0.25.2 which bumps up TLS dependencies, and turned on `tls-rustls-webpki-roots` feature flag + +### Breaking Changes +None + +### Fixes +- Fixed JSON format for log file output (was missing `timestamp` and `trader\_id`) +- Fixed `DatabaseConfig` port JSON parsing for Redis (was always defaulting to 6379) +- Fixed `ChandeMomentumOscillator` indicator divide by zero error (both Rust and Cython versions) + +--- + # NautilusTrader 1.189.0 Beta Released on 15th March 2024 (UTC). @@ -5,7 +32,7 @@ Released on 15th March 2024 (UTC). ### Enhancements - Implemented Binance order book snapshot rebuilds on websocket reconnect (see integration guide) - Added additional validations for `OrderMatchingEngine` (will now raise a `RuntimeError` when a price or size precision for `OrderFilled` does not match the instruments precisions) -- Added `LoggingConfig.use_pyo3` option for pyo3 based logging initialization (worse performance but allows visibility into logs originating from Rust) +- Added `LoggingConfig.use_pyo3` config option for pyo3 based logging initialization (worse performance but allows visibility into logs originating from Rust) - Added `exchange` field to `FuturesContract`, `FuturesSpread`, `OptionsContract` and `OptionsSpread` (optional) ### Breaking Changes diff --git a/docs/concepts/data.md b/docs/concepts/data.md index bbb85470848a..9964a606c860 100644 --- a/docs/concepts/data.md +++ b/docs/concepts/data.md @@ -41,7 +41,7 @@ To achieve this, two main components are necessary: ### Data loaders Data loader components are typically specific for the raw source/format and per integration. For instance, Binance order book data is stored in its raw CSV file form with -an entirely different format to [Databento Binary Encoding (DBN)](https://docs.databento.com/knowledge-base/new-users/dbn-encoding/getting-started-with-dbn) files. +an entirely different format to [Databento Binary Encoding (DBN)](https://databento.com/docs/knowledge-base/new-users/dbn-encoding/getting-started-with-dbn) files. ### Data wranglers diff --git a/docs/integrations/databento.md b/docs/integrations/databento.md index a43710e16e73..ad10aa049e5b 100644 --- a/docs/integrations/databento.md +++ b/docs/integrations/databento.md @@ -1,10 +1,10 @@ # Databento ```{warning} -We are currently working on this integration guide - consider it incomplete for now. +We are currently working on this integration guide. ``` -NautilusTrader provides an adapter for integrating with the Databento API and [Databento Binary Encoding (DBN)](https://docs.databento.com/knowledge-base/new-users/dbn-encoding) format data. +NautilusTrader provides an adapter for integrating with the Databento API and [Databento Binary Encoding (DBN)](https://databento.com/docs/knowledge-base/new-users/dbn-encoding) format data. As Databento is purely a market data provider, there is no execution client provided - although a sandbox environment with simulated execution could still be set up. It's also possible to match Databento data with Interactive Brokers execution, or to calculate traditional asset class signals for crypto trading. @@ -17,7 +17,7 @@ The capabilities of this adapter include: [Databento](https://databento.com/signup) currently offers 125 USD in free data credits (historical data only) for new account sign-ups. With careful requests, this is more than enough for testing and evaluation purposes. -It's recommended you make use of the [/metadata.get_cost](https://docs.databento.com/api-reference-historical/metadata/metadata-get-cost) endpoint. +It's recommended you make use of the [/metadata.get_cost](https://databento.com/docs/api-reference-historical/metadata/metadata-get-cost) endpoint. ``` ## Overview @@ -44,13 +44,13 @@ and won't need to necessarily work with these lower level components directly. ## Databento documentation -Databento provides extensive documentation for users which can be found in the [Databento knowledge base](https://docs.databento.com/knowledge-base/new-users). -It's recommended you also refer to the Databento documentation in conjunction with this Nautilus integration guide. +Databento provides extensive documentation for users which can be found in the [Databento knowledge base](https://databento.com/docs/knowledge-base/new-users). +It's recommended you also refer to this Databento documentation in conjunction with this NautilusTrader integration guide. ## Databento Binary Encoding (DBN) Databento Binary Encoding (DBN) is an extremely fast message encoding and storage format for normalized market data. -The [DBN specification](https://docs.databento.com/knowledge-base/new-users/dbn-encoding) includes a simple, self-describing metadata header and a fixed set of struct definitions, +The [DBN specification](https://databento.com/docs/knowledge-base/new-users/dbn-encoding) includes a simple, self-describing metadata header and a fixed set of struct definitions, which enforce a standardized way to normalize market data. The integration provides a decoder which can convert DBN format data to Nautilus objects. @@ -91,7 +91,7 @@ The Nautilus decoder will use the Databento `raw_symbol` for the Nautilus `symbo from the Databento instrument definition message for the Nautilus `venue`. Databento datasets are identified with a *dataset code* which is not the same -as a venue identifier. You can read more about Databento dataset naming conventions [here](https://docs.databento.com/api-reference-historical/basics/datasets). +as a venue identifier. You can read more about Databento dataset naming conventions [here](https://databento.com/docs/api-reference-historical/basics/datasets). Of particular note is for CME Globex MDP 3.0 data (`GLBX.MDP3` dataset code), the following exchanges are all grouped under the `GLBX` venue. These mappings can be determined from the @@ -105,7 +105,7 @@ instruments `exchange` field: - `XNYM` - **New York Mercantile Exchange (NYMEX)** ```{note} -Other venue MICs can be found in the `venue` field of responses from the [metadata.list_publishers](https://docs.databento.com/api-reference-historical/metadata/metadata-list-publishers?historical=http&live=python) endpoint. +Other venue MICs can be found in the `venue` field of responses from the [metadata.list_publishers](https://databento.com/docs/api-reference-historical/metadata/metadata-list-publishers?historical=http&live=python) endpoint. ``` ## Timestamps @@ -127,8 +127,8 @@ When decoding and normalizing Databento to Nautilus we generally assign the Data ```{note} See the following Databento docs for further information: -- [Databento standards and conventions - timestamps](https://docs.databento.com/knowledge-base/new-users/standards-conventions/timestamps) -- [Databento timestamping guide](https://docs.databento.com/knowledge-base/data-integrity/timestamping/timestamps-on-databento-and-how-to-use-them) +- [Databento standards and conventions - timestamps](https://databento.com/docs/knowledge-base/new-users/standards-conventions/timestamps) +- [Databento timestamping guide](https://databento.com/docs/knowledge-base/data-integrity/timestamping/timestamps-on-databento-and-how-to-use-them) ``` ## Data types @@ -136,6 +136,10 @@ See the following Databento docs for further information: The following section discusses Databento schema -> Nautilus data type equivalence and considerations. +```{note} +See the Databento [list of fields by schema guide](https://databento.com/docs/knowledge-base/new-users/fields-by-schema). +``` + ### Instrument definitions Databento provides a single schema to cover all instrument classes, these are @@ -143,17 +147,17 @@ decoded to the appropriate Nautilus `Instrument` types. The following Databento instrument classes are supported by NautilusTrader: -| Databento instrument class | Nautilus instrument type | -|----------------------------|------------------------------| -| STOCK | `Equity` | -| FUTURE | `FuturesContract` | -| CALL | `OptionsContract` | -| PUT | `OptionsContract` | -| FUTURESPREAD | `FuturesSpread` | -| OPTIONSPREAD | `OptionsSpread` | -| MIXEDSPREAD | `OptionsSpread` | -| FXSPOT | `CurrencyPair` | -| BOND | Not yet available | +| Databento instrument class | Code | Nautilus instrument type | +|----------------------------|------|------------------------------| +| Stock | `K` | `Equity` | +| Future | `F` | `FuturesContract` | +| Call | `C` | `OptionsContract` | +| Put | `P` | `OptionsContract` | +| Future spread | `S` | `FuturesSpread` | +| Option spread | `T` | `OptionsSpread` | +| Mixed spread | `M` | `OptionsSpread` | +| FX spot | `X` | `CurrencyPair` | +| Bond | `B` | Not yet available | ### MBO (market by order) @@ -171,9 +175,9 @@ object, which occurs during the replay startup sequence. ### MBP-1 (market by price, top-of-book) -This schema represents the top-of-book only. Like with MBO messages, some +This schema represents the top-of-book only (quotes *and* trades). Like with MBO messages, some messages carry trade information, and so when decoding MBP-1 messages Nautilus -will produce a `QuoteTick` and also a `TradeTick` if the message is a trade. +will produce a `QuoteTick` and *also* a `TradeTick` if the message is a trade. ### OHLCV (bar aggregates) @@ -183,9 +187,9 @@ The Nautilus decoder will normalize the `ts_event` timestamps to the **close** o ### Imbalance & Statistics -The Databento `imbalance` and `statistics` schemas cannot be represented as a built-in Nautilus data types +The Databento `imbalance` and `statistics` schemas cannot be represented as a built-in Nautilus data types, and so they have specific types defined in Rust `DatabentoImbalance` and `DatabentoStatistics`. -Python bindings are provided via pyo3 (Rust) and so the types behaves a little differently to a built-in Nautilus +Python bindings are provided via pyo3 (Rust) so the types behave a little differently to a built-in Nautilus data types, where all attributes are pyo3 provided objects and not directly compatible with certain methods which may expect a Cython provided type. There are pyo3 -> legacy Cython object conversion methods available, which can be found in the API reference. @@ -244,7 +248,7 @@ the Nautilus Parquet data from disk, which achieves extremely high through-put ( than converting DBN -> Nautilus on the fly for every backtest run). ```{note} -Performance benchmarks are under development. +Performance benchmarks are currently under development. ``` ## Loading DBN data diff --git a/docs/integrations/ib.md b/docs/integrations/ib.md index e1c97e96b661..3a669a17fd51 100644 --- a/docs/integrations/ib.md +++ b/docs/integrations/ib.md @@ -1,13 +1,13 @@ # Interactive Brokers -Interactive Brokers (IB) is a trading platform that allows trading across a wide range of financial instruments, including stocks, options, futures, currencies, bonds, funds, and cryptocurrencies. NautilusTrader offers an adapter to integrate with IB using their [Trader Workstation (TWS) API](https://interactivebrokers.github.io/tws-api/index.html) through their Python library, [ibapi](https://github.com/nautechsystems/ibapi). +Interactive Brokers (IB) is a trading platform that allows trading across a wide range of financial instruments, including stocks, options, futures, currencies, bonds, funds, and cryptocurrencies. NautilusTrader offers an adapter to integrate with IB using their [Trader Workstation (TWS) API](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/) through their Python library, [ibapi](https://github.com/nautechsystems/ibapi). -The TWS API serves as an interface to IB's standalone trading applications: TWS and IB Gateway. Both can be downloaded from the IB website. If you haven't installed TWS or IB Gateway yet, refer to the [Initial Setup](https://interactivebrokers.github.io/tws-api/initial_setup.html) guide. In NautilusTrader, you'll establish a connection to one of these applications via the `InteractiveBrokersClient`. +The TWS API serves as an interface to IB's standalone trading applications: TWS and IB Gateway. Both can be downloaded from the IB website. If you haven't installed TWS or IB Gateway yet, refer to the [Initial Setup](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#tws-download) guide. In NautilusTrader, you'll establish a connection to one of these applications via the `InteractiveBrokersClient`. -Alternatively, you can start with a [dockerized version](https://github.com/gnzsnz/ib-gateway-docker) of the IB Gateway, particularly useful when deploying trading strategies on a hosted cloud platform. This requires having [Docker](https://www.docker.com/) installed on your machine, along with the [docker](https://pypi.org/project/docker/) Python package, which NautilusTrader conveniently includes as an extra package. +Alternatively, you can start with a [dockerized version](https://github.com/gnzsnz/ib-gateway-docker) of the IB Gateway, which is particularly useful when deploying trading strategies on a hosted cloud platform. This requires having [Docker](https://www.docker.com/) installed on your machine, along with the [docker](https://pypi.org/project/docker/) Python package, which NautilusTrader conveniently includes as an extra package. ```{note} -The standalone TWS and IB Gateway applications necessitate manual input of username, password, and trading mode (live or paper) at startup. The dockerized version of the IB Gateway handles these steps programmatically. +The standalone TWS and IB Gateway applications require manually inputting username, password, and trading mode (live or paper) at startup. The dockerized version of the IB Gateway handles these steps programmatically. ``` ## Installation @@ -87,7 +87,7 @@ To troubleshoot TWS API incoming message issues, consider starting at the `Inter ## Instruments & Contracts -In IB, a NautilusTrader `Instrument` is equivalent to a [Contract](https://interactivebrokers.github.io/tws-api/contracts.html). Contracts can be either a [basic contract](https://interactivebrokers.github.io/tws-api/classIBApi_1_1Contract.html) or a more [detailed](https://interactivebrokers.github.io/tws-api/classIBApi_1_1ContractDetails.html) version (ContractDetails). The adapter models these using `IBContract` and `IBContractDetails` classes. The latter includes critical data like order types and trading hours, which are absent in the basic contract. As a result, `IBContractDetails` can be converted to an `Instrument` while `IBContract` cannot. +In IB, a NautilusTrader `Instrument` is equivalent to a [Contract](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#contracts). Contracts can be either a [basic contract](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#contract-object) or a more [detailed](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#contract-details) version (ContractDetails). The adapter models these using `IBContract` and `IBContractDetails` classes. The latter includes critical data like order types and trading hours, which are absent in the basic contract. As a result, `IBContractDetails` can be converted to an `Instrument` while `IBContract` cannot. To search for contract information, use the [IB Contract Information Center](https://pennies.interactivebrokers.com/cstools/contract_info/). @@ -195,7 +195,7 @@ instrument_provider_config = InteractiveBrokersInstrumentProviderConfig( ### Data Client -`InteractiveBrokersDataClient` interfaces with IB for streaming and retrieving market data. Upon connection, it configures the [market data type](https://interactivebrokers.github.io/tws-api/market_data_type.html) and loads instruments based on the settings in `InteractiveBrokersInstrumentProviderConfig`. This client can subscribe to and unsubscribe from various market data types, including quote ticks, trade ticks, and bars. +`InteractiveBrokersDataClient` interfaces with IB for streaming and retrieving market data. Upon connection, it configures the [market data type](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#delayed-market-data) and loads instruments based on the settings in `InteractiveBrokersInstrumentProviderConfig`. This client can subscribe to and unsubscribe from various market data types, including quote ticks, trade ticks, and bars. Configurable through `InteractiveBrokersDataClientConfig`, it allows adjustments for handling revised bars, trading hours preferences, and market data types (e.g., `IBMarketDataTypeEnum.REALTIME` or `IBMarketDataTypeEnum.DELAYED_FROZEN`). diff --git a/docs/integrations/index.md b/docs/integrations/index.md index 6609b559f6af..123fdc6c13b7 100644 --- a/docs/integrations/index.md +++ b/docs/integrations/index.md @@ -11,29 +11,30 @@ binance.md databento.md ib.md - ``` -NautilusTrader is designed in a modular way to work with 'adapters' which provide -connectivity to data providers and/or trading venues - converting their raw API +NautilusTrader is designed in a modular way to work with *adapters* which provide +connectivity to trading venues and data providers - converting their raw API into a unified interface. The following integrations are currently supported: -```{warning} -The initial integrations for the project are currently under heavy construction. -It's advised to conduct some of your own testing with small amounts of capital before -running strategies which are able to access larger capital allocations. -``` - | Name | ID | Type | Status | Docs | | :-------------------------------------------------------- | :-------------------- | :---------------------- | :------------------------------------------------------ | :------------------------------------------------------------------ | -| [Betfair](https://betfair.com) | `BETFAIR` | Sports Betting Exchange | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/betfair.html) | +| [Betfair](https://betfair.com) | `BETFAIR` | Sports Betting Exchange | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/betfair.html) | | [Binance](https://binance.com) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | | [Binance US](https://binance.us) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | | [Binance Futures](https://www.binance.com/en/futures) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | | [Bybit](https://www.bybit.com) | `BYBIT` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/building-orange) | | -| [Databento](https://databento.com) | `DATABENTO` | Data provider | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/databento.html) | +| [Databento](https://databento.com) | `DATABENTO` | Data Provider | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/databento.html) | | [Interactive Brokers](https://www.interactivebrokers.com) | `INTERACTIVE_BROKERS` | Brokerage (multi-venue) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/ib.html) | +- `ID:` The default client ID for the integrations adapter clients +- `Type:` The type of integration (often the venue type) + +### Status +- `building` - Under construction and likely not in a usable state +- `beta` - Completed to a minimally working state and in a 'beta' testing phase +- `stable` - Stabilized feature set and API, the integration has been tested by both developers and users to a reasonable level (some bugs may still remain) + ## Implementation goals The primary goal of NautilusTrader is to provide a unified trading system for diff --git a/examples/live/binance/binance_futures_testnet_market_maker.py b/examples/live/binance/binance_futures_testnet_market_maker.py index f2b7c4c1664c..2c42cd766b86 100644 --- a/examples/live/binance/binance_futures_testnet_market_maker.py +++ b/examples/live/binance/binance_futures_testnet_market_maker.py @@ -46,7 +46,7 @@ # log_level_file="DEBUG", # log_file_format="json", log_colors=True, - use_pyo3=False, + use_pyo3=True, ), exec_engine=LiveExecEngineConfig( reconciliation=True, @@ -54,12 +54,12 @@ filter_position_reports=True, ), cache=CacheConfig( - # database=DatabaseConfig(), + # database=DatabaseConfig(timeout=2), timestamps_as_iso8601=True, flush_on_start=False, ), # message_bus=MessageBusConfig( - # database=DatabaseConfig(), + # database=DatabaseConfig(timeout=2), # encoding="json", # timestamps_as_iso8601=True, # streams_prefix="quoters", diff --git a/examples/live/databento/databento_subscriber.py b/examples/live/databento/databento_subscriber.py index 4f69ee4e0e32..e93617570cc7 100644 --- a/examples/live/databento/databento_subscriber.py +++ b/examples/live/databento/databento_subscriber.py @@ -44,8 +44,8 @@ # For correct subscription operation, you must specify all instruments to be immediately # subscribed for as part of the data client configuration instrument_ids = [ - InstrumentId.from_str("ESM4.GLBX"), - # InstrumentId.from_str("ESU4.GLBX"), + # InstrumentId.from_str("ESM4.GLBX"), + InstrumentId.from_str("ES.c.0.GLBX"), # InstrumentId.from_str("AAPL.XNAS"), ] @@ -153,7 +153,7 @@ def on_start(self) -> None: # ) self.subscribe_quote_ticks(instrument_id, client_id=DATABENTO_CLIENT_ID) - # self.subscribe_trade_ticks(instrument_id, client_id=DATABENTO_CLIENT_ID) + self.subscribe_trade_ticks(instrument_id, client_id=DATABENTO_CLIENT_ID) # self.request_quote_ticks(instrument_id) # self.request_trade_ticks(instrument_id) diff --git a/examples/notebooks/databento_data_catalog.ipynb b/examples/notebooks/databento_data_catalog.ipynb index a11079005e7c..2cb7aa47dd17 100644 --- a/examples/notebooks/databento_data_catalog.ipynb +++ b/examples/notebooks/databento_data_catalog.ipynb @@ -76,7 +76,7 @@ "id": "7", "metadata": {}, "source": [ - "We can use a metadata [get_cost endpoint](https://docs.databento.com/api-reference-historical/metadata/metadata-get-cost?historical=python&live=python) from the Databento API to get a quote on the cost, prior to each request.\n", + "We can use a metadata [get_cost endpoint](https://databento.com/docs/api-reference-historical/metadata/metadata-get-cost?historical=python&live=python) from the Databento API to get a quote on the cost, prior to each request.\n", "Each request sequence will first request the cost of the data, and then make a request only if the data doesn't already exist on disk.\n", "\n", "Note the response returned is in USD, displayed as fractional cents." diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 6c303e903d00..5fddd4b53453 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -44,9 +44,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -309,7 +309,7 @@ version = "50.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ff3e9c01f7cd169379d269f926892d0e622a704960350d09d331be3ec9e0029" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", ] [[package]] @@ -362,13 +362,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -454,9 +454,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "95d8e92cac0961e91dbd517496b00f7e9b92363dbe6d42c3198268323798860c" dependencies = [ "addr2line", "cc", @@ -496,9 +496,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" dependencies = [ "serde", ] @@ -566,15 +566,15 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", "syn_derive", ] [[package]] name = "brotli" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -671,7 +671,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da6bc11b07529f16944307272d5bd9b22530bc7d05751717c9d416586cedab49" dependencies = [ "clap 3.2.25", - "heck", + "heck 0.4.1", "indexmap 1.9.3", "log", "proc-macro2", @@ -785,9 +785,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.2" +version = "4.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" +checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" dependencies = [ "clap_builder", ] @@ -938,7 +938,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.5.2", + "clap 4.5.3", "criterion-plot", "is-terminal", "itertools 0.10.5", @@ -1057,7 +1057,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -1068,7 +1068,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -1344,10 +1344,8 @@ dependencies = [ "itoa", "json-writer", "num_enum", - "pyo3", "serde", "streaming-iterator", - "strum 0.26.2", "thiserror", "time", "tokio", @@ -1363,7 +1361,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -1405,7 +1403,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -1415,7 +1413,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -1676,7 +1674,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -1750,9 +1748,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" dependencies = [ "bytes", "fnv", @@ -1769,9 +1767,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943" +checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" dependencies = [ "bytes", "fnv", @@ -1834,6 +1832,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1966,14 +1970,14 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.24", + "h2 0.3.25", "http 0.2.12", "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.6", + "socket2", "tokio", "tower-service", "tracing", @@ -1989,7 +1993,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.2", + "h2 0.4.3", "http 1.1.0", "http-body 1.0.0", "httparse", @@ -2025,7 +2029,7 @@ dependencies = [ "http-body 1.0.0", "hyper 1.2.0", "pin-project-lite", - "socket2 0.5.6", + "socket2", "tokio", ] @@ -2092,6 +2096,7 @@ checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", "hashbrown 0.14.3", + "serde", ] [[package]] @@ -2409,7 +2414,7 @@ dependencies = [ [[package]] name = "nautilus-accounting" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "cbindgen", @@ -2425,13 +2430,12 @@ dependencies = [ [[package]] name = "nautilus-adapters" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "chrono", "criterion", "databento", - "dbn", "indexmap 2.2.5", "itoa", "log", @@ -2457,11 +2461,12 @@ dependencies = [ [[package]] name = "nautilus-backtest" -version = "0.19.0" +version = "0.20.0" dependencies = [ "cbindgen", "nautilus-common", "nautilus-core", + "nautilus-execution", "nautilus-model", "pyo3", "rstest", @@ -2471,7 +2476,7 @@ dependencies = [ [[package]] name = "nautilus-common" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "cbindgen", @@ -2497,13 +2502,13 @@ dependencies = [ [[package]] name = "nautilus-core" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "cbindgen", "chrono", "criterion", - "heck", + "heck 0.5.0", "iai", "pyo3", "rmp-serde", @@ -2516,7 +2521,7 @@ dependencies = [ [[package]] name = "nautilus-execution" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "criterion", @@ -2540,7 +2545,7 @@ dependencies = [ [[package]] name = "nautilus-indicators" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "nautilus-core", @@ -2552,7 +2557,7 @@ dependencies = [ [[package]] name = "nautilus-infrastructure" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "nautilus-common", @@ -2563,11 +2568,12 @@ dependencies = [ "rmp-serde", "rstest", "serde_json", + "tracing", ] [[package]] name = "nautilus-model" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "cbindgen", @@ -2595,7 +2601,7 @@ dependencies = [ [[package]] name = "nautilus-network" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "axum", @@ -2620,7 +2626,7 @@ dependencies = [ [[package]] name = "nautilus-persistence" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "binary-heap-plus", @@ -2644,7 +2650,7 @@ dependencies = [ [[package]] name = "nautilus-pyo3" -version = "0.19.0" +version = "0.20.0" dependencies = [ "nautilus-accounting", "nautilus-adapters", @@ -2820,7 +2826,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -2871,7 +2877,7 @@ version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cfg-if", "foreign-types", "libc", @@ -2888,7 +2894,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -3104,7 +3110,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -3240,7 +3246,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "chrono", "flate2", "hex", @@ -3255,7 +3261,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "chrono", "hex", ] @@ -3286,7 +3292,6 @@ version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" dependencies = [ - "anyhow", "cfg-if", "indoc", "libc", @@ -3354,7 +3359,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -3363,11 +3368,11 @@ version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -3459,9 +3464,9 @@ dependencies = [ [[package]] name = "redis" -version = "0.24.0" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c580d9cbbe1d1b479e8d67cf9daf6a62c957e6846048408b80b43ac3f6af84cd" +checksum = "71d64e978fd98a0e6b105d066ba4889a7301fca65aeac850a877d8797343feeb" dependencies = [ "arc-swap", "async-trait", @@ -3472,18 +3477,19 @@ dependencies = [ "itoa", "percent-encoding", "pin-project-lite", - "rustls 0.21.10", - "rustls-native-certs 0.6.3", - "rustls-pemfile 1.0.4", - "rustls-webpki 0.101.7", + "rustls", + "rustls-native-certs", + "rustls-pemfile 2.1.1", + "rustls-pki-types", "ryu", "sha1_smol", - "socket2 0.4.10", + "socket2", "tokio", "tokio-retry", - "tokio-rustls 0.24.1", + "tokio-rustls", "tokio-util", "url", + "webpki-roots", ] [[package]] @@ -3556,16 +3562,16 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.26" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bf93c4af7a8bb7d879d51cebe797356ff10ae8516ace542b5182d9dcac10b2" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2 0.3.24", + "h2 0.3.25", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.28", @@ -3707,7 +3713,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.52", + "syn 2.0.53", "unicode-ident", ] @@ -3754,29 +3760,17 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] -[[package]] -name = "rustls" -version = "0.21.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - [[package]] name = "rustls" version = "0.22.2" @@ -3786,23 +3780,11 @@ dependencies = [ "log", "ring", "rustls-pki-types", - "rustls-webpki 0.102.2", + "rustls-webpki", "subtle", "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile 1.0.4", - "schannel", - "security-framework", -] - [[package]] name = "rustls-native-certs" version = "0.7.0" @@ -3841,16 +3823,6 @@ version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8" -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "rustls-webpki" version = "0.102.2" @@ -3898,16 +3870,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "seahash" version = "4.1.0" @@ -3966,7 +3928,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4081,9 +4043,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "snafu" @@ -4101,7 +4063,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "syn 1.0.109", @@ -4113,16 +4075,6 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.6" @@ -4187,7 +4139,7 @@ checksum = "01b2e185515564f15375f593fb966b5718bc624ba77fe49fa4616ad619690554" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4263,7 +4215,7 @@ checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" dependencies = [ "dotenvy", "either", - "heck", + "heck 0.4.1", "hex", "once_cell", "proc-macro2", @@ -4289,7 +4241,7 @@ checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", "base64", - "bitflags 2.4.2", + "bitflags 2.5.0", "byteorder", "bytes", "crc", @@ -4331,7 +4283,7 @@ checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", "base64", - "bitflags 2.4.2", + "bitflags 2.5.0", "byteorder", "crc", "dotenvy", @@ -4434,11 +4386,11 @@ version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4447,11 +4399,11 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4473,9 +4425,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" dependencies = [ "proc-macro2", "quote", @@ -4491,7 +4443,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4553,7 +4505,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c138f99377e5d653a371cdad263615634cfc8467685dfe8e73e2b8e98f44b17" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro-error", "proc-macro2", "quote", @@ -4616,7 +4568,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4725,7 +4677,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.6", + "socket2", "tokio-macros", "windows-sys 0.48.0", ] @@ -4738,7 +4690,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4762,23 +4714,13 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.10", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ - "rustls 0.22.2", + "rustls", "rustls-pki-types", "tokio", ] @@ -4801,12 +4743,12 @@ dependencies = [ "futures-util", "log", "native-tls", - "rustls 0.22.2", - "rustls-native-certs 0.7.0", + "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-native-tls", - "tokio-rustls 0.25.0", + "tokio-rustls", "tungstenite", "webpki-roots", ] @@ -4899,7 +4841,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4984,7 +4926,7 @@ dependencies = [ "log", "native-tls", "rand", - "rustls 0.22.2", + "rustls", "rustls-pki-types", "sha1", "thiserror", @@ -5019,7 +4961,7 @@ checksum = "563b3b88238ec95680aef36bdece66896eaa7ce3c0f1b4f39d38fb2435261352" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -5117,9 +5059,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ "getrandom", ] @@ -5132,9 +5074,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec26a25bd6fca441cdd0f769fd7f891bae119f996de31f86a5eddccef54c1d" +checksum = "74797339c3b98616c009c7c3eb53a0ce41e85c8ec66bd3db96ed132d20cfdee8" [[package]] name = "vcpkg" @@ -5200,7 +5142,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", "wasm-bindgen-shared", ] @@ -5234,7 +5176,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5523,7 +5465,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 19d23af3f414..b92f9b6eaa41 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -17,8 +17,8 @@ members = [ ] [workspace.package] -rust-version = "1.76.0" -version = "0.19.0" +rust-version = "1.77.0" +version = "0.20.0" edition = "2021" authors = ["Nautech Systems "] description = "A high-performance algorithmic trading platform and event-driven backtester" @@ -28,26 +28,33 @@ documentation = "https://docs.nautilustrader.io" anyhow = "1.0.81" chrono = "0.4.35" futures = "0.3.30" -indexmap = "2.2.5" +indexmap = { version = "2.2.5", features = ["serde"] } itoa = "1.0.10" once_cell = "1.19.0" log = { version = "0.4.21", features = ["std", "kv_unstable", "serde", "release_max_level_debug"] } -pyo3 = { version = "0.20.3", features = ["anyhow", "rust_decimal"] } +pyo3 = { version = "0.20.3", features = ["rust_decimal"] } pyo3-asyncio = { version = "0.20.0", features = ["tokio-runtime", "tokio", "attributes"] } rand = "0.8.5" -redis = { version = "0.24.0", features = ["tokio-comp", "tls-rustls", "tokio-rustls-comp", "keep-alive", "connection-manager"] } +redis = { version = "0.25.2", features = [ + "connection-manager", + "keep-alive", + "tls-rustls", + "tls-rustls-webpki-roots", + "tokio-comp", + "tokio-rustls-comp", +] } rmp-serde = "1.1.2" rust_decimal = "1.34.3" rust_decimal_macros = "1.34.2" serde = { version = "1.0.197", features = ["derive"] } -serde_json = "1.0.113" +serde_json = "1.0.114" strum = { version = "0.26.2", features = ["derive"] } thiserror = "1.0.58" thousands = "0.2.0" tracing = "0.1.40" tokio = { version = "1.36.0", features = ["full"] } ustr = { version = "1.0.0", features = ["serde"] } -uuid = { version = "1.7.0", features = ["v4"] } +uuid = { version = "1.8.0", features = ["v4"] } # dev-dependencies criterion = "0.5.1" diff --git a/nautilus_core/accounting/Cargo.toml b/nautilus_core/accounting/Cargo.toml index 4acac713c058..f81d62c9e488 100644 --- a/nautilus_core/accounting/Cargo.toml +++ b/nautilus_core/accounting/Cargo.toml @@ -27,6 +27,7 @@ rstest = { workspace = true } cbindgen = { workspace = true, optional = true } [features] +default = [] extension-module = [ "pyo3/extension-module", "nautilus-core/extension-module", @@ -34,4 +35,3 @@ extension-module = [ "nautilus-common/extension-module", ] python = ["pyo3", "nautilus-core/python", "nautilus-model/python"] -default = [] diff --git a/nautilus_core/accounting/src/account/base.rs b/nautilus_core/accounting/src/account/base.rs index fcfaf6163d95..147ffa9ca4d3 100644 --- a/nautilus_core/accounting/src/account/base.rs +++ b/nautilus_core/accounting/src/account/base.rs @@ -15,7 +15,6 @@ use std::collections::HashMap; -use anyhow::Result; use nautilus_model::{ enums::{AccountType, LiquiditySide, OrderSide}, events::{account::state::AccountState, order::filled::OrderFilled}, @@ -45,7 +44,7 @@ pub struct BaseAccount { } impl BaseAccount { - pub fn new(event: AccountState, calculate_account_state: bool) -> Result { + pub fn new(event: AccountState, calculate_account_state: bool) -> anyhow::Result { let mut balances_starting: HashMap = HashMap::new(); let mut balances: HashMap = HashMap::new(); event.balances.iter().for_each(|balance| { @@ -145,7 +144,7 @@ impl BaseAccount { quantity: Quantity, price: Price, use_quote_for_inverse: Option, - ) -> Result { + ) -> anyhow::Result { let base_currency = instrument .base_currency() .unwrap_or(instrument.quote_currency()); @@ -178,7 +177,7 @@ impl BaseAccount { instrument: T, fill: OrderFilled, position: Option, - ) -> Result> { + ) -> anyhow::Result> { let mut pnls: HashMap = HashMap::new(); let quote_currency = instrument.quote_currency(); let base_currency = instrument.base_currency(); @@ -222,7 +221,7 @@ impl BaseAccount { last_px: Price, liquidity_side: LiquiditySide, use_quote_for_inverse: Option, - ) -> Result { + ) -> anyhow::Result { assert!( liquidity_side != LiquiditySide::NoLiquiditySide, "Invalid liquidity side" diff --git a/nautilus_core/accounting/src/account/cash.rs b/nautilus_core/accounting/src/account/cash.rs index eca71a1d88a9..7397d91b999f 100644 --- a/nautilus_core/accounting/src/account/cash.rs +++ b/nautilus_core/accounting/src/account/cash.rs @@ -19,7 +19,6 @@ use std::{ ops::{Deref, DerefMut}, }; -use anyhow::Result; use nautilus_model::{ enums::{AccountType, LiquiditySide, OrderSide}, events::{account::state::AccountState, order::filled::OrderFilled}, @@ -42,7 +41,7 @@ pub struct CashAccount { } impl CashAccount { - pub fn new(event: AccountState, calculate_account_state: bool) -> Result { + pub fn new(event: AccountState, calculate_account_state: bool) -> anyhow::Result { Ok(Self { base: BaseAccount::new(event, calculate_account_state)?, }) @@ -113,7 +112,7 @@ impl Account for CashAccount { quantity: Quantity, price: Price, use_quote_for_inverse: Option, - ) -> Result { + ) -> anyhow::Result { self.base_calculate_balance_locked(instrument, side, quantity, price, use_quote_for_inverse) } fn calculate_pnls( @@ -121,7 +120,7 @@ impl Account for CashAccount { instrument: T, fill: OrderFilled, position: Option, - ) -> Result> { + ) -> anyhow::Result> { self.base_calculate_pnls(instrument, fill, position) } fn calculate_commission( @@ -131,7 +130,7 @@ impl Account for CashAccount { last_px: Price, liquidity_side: LiquiditySide, use_quote_for_inverse: Option, - ) -> Result { + ) -> anyhow::Result { self.base_calculate_commission( instrument, last_qty, diff --git a/nautilus_core/accounting/src/account/margin.rs b/nautilus_core/accounting/src/account/margin.rs index 8f5b578f799f..0ee6da01c474 100644 --- a/nautilus_core/accounting/src/account/margin.rs +++ b/nautilus_core/accounting/src/account/margin.rs @@ -22,7 +22,6 @@ use std::{ ops::{Deref, DerefMut}, }; -use anyhow::Result; use nautilus_model::{ enums::{AccountType, LiquiditySide, OrderSide}, events::{account::state::AccountState, order::filled::OrderFilled}, @@ -54,7 +53,7 @@ pub struct MarginAccount { } impl MarginAccount { - pub fn new(event: AccountState, calculate_account_state: bool) -> Result { + pub fn new(event: AccountState, calculate_account_state: bool) -> anyhow::Result { Ok(Self { base: BaseAccount::new(event, calculate_account_state)?, leverages: HashMap::new(), @@ -322,7 +321,7 @@ impl Account for MarginAccount { quantity: Quantity, price: Price, use_quote_for_inverse: Option, - ) -> Result { + ) -> anyhow::Result { self.base_calculate_balance_locked(instrument, side, quantity, price, use_quote_for_inverse) } fn calculate_pnls( @@ -330,7 +329,7 @@ impl Account for MarginAccount { instrument: T, fill: OrderFilled, position: Option, - ) -> Result> { + ) -> anyhow::Result> { self.base_calculate_pnls(instrument, fill, position) } fn calculate_commission( @@ -340,7 +339,7 @@ impl Account for MarginAccount { last_px: Price, liquidity_side: LiquiditySide, use_quote_for_inverse: Option, - ) -> Result { + ) -> anyhow::Result { self.base_calculate_commission( instrument, last_qty, diff --git a/nautilus_core/accounting/src/account/mod.rs b/nautilus_core/accounting/src/account/mod.rs index 122b24c02a9d..1825998bf831 100644 --- a/nautilus_core/accounting/src/account/mod.rs +++ b/nautilus_core/accounting/src/account/mod.rs @@ -15,7 +15,6 @@ use std::collections::HashMap; -use anyhow::Result; use nautilus_model::{ enums::{LiquiditySide, OrderSide}, events::{account::state::AccountState, order::filled::OrderFilled}, @@ -48,14 +47,14 @@ pub trait Account { quantity: Quantity, price: Price, use_quote_for_inverse: Option, - ) -> Result; + ) -> anyhow::Result; fn calculate_pnls( &self, instrument: T, fill: OrderFilled, position: Option, - ) -> Result>; + ) -> anyhow::Result>; fn calculate_commission( &self, @@ -64,7 +63,7 @@ pub trait Account { last_px: Price, liquidity_side: LiquiditySide, use_quote_for_inverse: Option, - ) -> Result; + ) -> anyhow::Result; } pub mod base; diff --git a/nautilus_core/accounting/src/python/transformer.rs b/nautilus_core/accounting/src/python/transformer.rs index 9e9bcdd946f4..f02acdb9125b 100644 --- a/nautilus_core/accounting/src/python/transformer.rs +++ b/nautilus_core/accounting/src/python/transformer.rs @@ -34,7 +34,8 @@ pub fn cash_account_from_account_events( return Err(to_pyvalue_err("No account events")); } let init_event = account_events[0].clone(); - let mut cash_account = CashAccount::new(init_event, calculate_account_state)?; + let mut cash_account = + CashAccount::new(init_event, calculate_account_state).map_err(to_pyvalue_err)?; for event in account_events.iter().skip(1) { cash_account.apply(event.clone()); } @@ -56,7 +57,8 @@ pub fn margin_account_from_account_events( return Err(to_pyvalue_err("No account events")); } let init_event = account_events[0].clone(); - let mut margin_account = MarginAccount::new(init_event, calculate_account_state)?; + let mut margin_account = + MarginAccount::new(init_event, calculate_account_state).map_err(to_pyvalue_err)?; for event in account_events.iter().skip(1) { margin_account.apply(event.clone()); } diff --git a/nautilus_core/adapters/Cargo.toml b/nautilus_core/adapters/Cargo.toml index 6d476d209e17..18c612ea7216 100644 --- a/nautilus_core/adapters/Cargo.toml +++ b/nautilus_core/adapters/Cargo.toml @@ -36,28 +36,31 @@ tokio = { workspace = true } thiserror = { workspace = true } ustr = { workspace = true } databento = { version = "0.7.1", optional = true } -dbn = { version = "0.16.0", optional = true, features = ["python"] } streaming-iterator = "0.1.9" -time = "0.3.31" +time = "0.3.34" [dev-dependencies] criterion = { workspace = true } rstest = { workspace = true } [features] +default = ["ffi", "python"] extension-module = [ "pyo3/extension-module", "nautilus-common/extension-module", "nautilus-core/extension-module", "nautilus-model/extension-module", ] -databento = ["dep:databento", "dbn", "python"] -ffi = ["nautilus-core/ffi", "nautilus-model/ffi", "nautilus-common/ffi"] +databento = ["dep:databento", "python"] +ffi = [ + "nautilus-common/ffi", + "nautilus-core/ffi", + "nautilus-model/ffi", +] python = [ "pyo3", "pyo3-asyncio", + "nautilus-common/python", "nautilus-core/python", "nautilus-model/python", - "nautilus-common/python", ] -default = ["ffi", "python"] diff --git a/nautilus_core/adapters/src/databento/bin/sandbox.rs b/nautilus_core/adapters/src/databento/bin/sandbox.rs index b84d37edc3b6..b9637d7a1263 100644 --- a/nautilus_core/adapters/src/databento/bin/sandbox.rs +++ b/nautilus_core/adapters/src/databento/bin/sandbox.rs @@ -1,11 +1,11 @@ use std::env; use databento::{ - dbn::{Dataset::GlbxMdp3, SType, Schema}, + dbn::{Dataset::GlbxMdp3, MboMsg, SType, Schema, TradeMsg}, live::Subscription, LiveClient, }; -use dbn::TradeMsg; +use time::OffsetDateTime; #[tokio::main] async fn main() { @@ -20,9 +20,10 @@ async fn main() { client .subscribe( &Subscription::builder() - .schema(Schema::Trades) + .schema(Schema::Mbo) .stype_in(SType::RawSymbol) .symbols("ESM4") + .start(OffsetDateTime::from_unix_timestamp_nanos(0).unwrap()) .build(), ) .await @@ -30,9 +31,18 @@ async fn main() { client.start().await.unwrap(); + let mut count = 0; + while let Some(record) = client.next_record().await.unwrap() { - if let Some(trade) = record.get::() { - println!("{trade:#?}"); + if let Some(msg) = record.get::() { + println!("{msg:#?}"); + } + if let Some(msg) = record.get::() { + println!( + "Received delta: {} {} flags={}", + count, msg.hd.ts_event, msg.flags, + ); + count += 1; } } } diff --git a/nautilus_core/adapters/src/databento/common.rs b/nautilus_core/adapters/src/databento/common.rs index d5e5282495aa..ce5e13097731 100644 --- a/nautilus_core/adapters/src/databento/common.rs +++ b/nautilus_core/adapters/src/databento/common.rs @@ -13,7 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use anyhow::Result; use databento::historical::DateTimeRange; use nautilus_core::time::UnixNanos; use time::OffsetDateTime; @@ -21,7 +20,7 @@ use time::OffsetDateTime; pub const DATABENTO: &str = "DATABENTO"; pub const ALL_SYMBOLS: &str = "ALL_SYMBOLS"; -pub fn get_date_time_range(start: UnixNanos, end: UnixNanos) -> Result { +pub fn get_date_time_range(start: UnixNanos, end: UnixNanos) -> anyhow::Result { Ok(DateTimeRange::from(( OffsetDateTime::from_unix_timestamp_nanos(i128::from(start))?, OffsetDateTime::from_unix_timestamp_nanos(i128::from(end))?, diff --git a/nautilus_core/adapters/src/databento/decode.rs b/nautilus_core/adapters/src/databento/decode.rs index ce213526c9ea..476b8394ab1b 100644 --- a/nautilus_core/adapters/src/databento/decode.rs +++ b/nautilus_core/adapters/src/databento/decode.rs @@ -20,7 +20,7 @@ use std::{ str::FromStr, }; -use anyhow::{anyhow, bail, Result}; +use databento::dbn; use nautilus_core::{datetime::NANOSECONDS_IN_SECOND, time::UnixNanos}; use nautilus_model::{ data::{ @@ -94,29 +94,31 @@ pub fn parse_aggressor_side(c: c_char) -> AggressorSide { } } -pub fn parse_book_action(c: c_char) -> Result { +pub fn parse_book_action(c: c_char) -> anyhow::Result { match c as u8 as char { 'A' => Ok(BookAction::Add), 'C' => Ok(BookAction::Delete), 'F' => Ok(BookAction::Update), 'M' => Ok(BookAction::Update), 'R' => Ok(BookAction::Clear), - _ => bail!("Invalid `BookAction`, was '{c}'"), + _ => anyhow::bail!("Invalid `BookAction`, was '{c}'"), } } -pub fn parse_option_kind(c: c_char) -> Result { +pub fn parse_option_kind(c: c_char) -> anyhow::Result { match c as u8 as char { 'C' => Ok(OptionKind::Call), 'P' => Ok(OptionKind::Put), - _ => bail!("Invalid `OptionKind`, was '{c}'"), + _ => anyhow::bail!("Invalid `OptionKind`, was '{c}'"), } } -pub fn parse_cfi_iso10926(value: &str) -> Result<(Option, Option)> { +pub fn parse_cfi_iso10926( + value: &str, +) -> anyhow::Result<(Option, Option)> { let chars: Vec = value.chars().collect(); if chars.len() < 3 { - bail!("Value string is too short"); + anyhow::bail!("Value string is too short"); } let cfi_category = chars[0]; @@ -145,21 +147,21 @@ pub fn parse_cfi_iso10926(value: &str) -> Result<(Option, Option Result { +pub fn decode_price(value: i64, precision: u8) -> anyhow::Result { match value { 0 | i64::MAX => Price::new(10f64.powi(-i32::from(precision)), precision), _ => Price::from_raw(value, precision), } } -pub fn decode_optional_price(value: i64, precision: u8) -> Result> { +pub fn decode_optional_price(value: i64, precision: u8) -> anyhow::Result> { match value { i64::MAX => Ok(None), _ => Ok(Some(Price::from_raw(value, precision)?)), } } -pub fn decode_optional_quantity_i32(value: i32) -> Result> { +pub fn decode_optional_quantity_i32(value: i32) -> anyhow::Result> { match value { i32::MAX => Ok(None), _ => Ok(Some(Quantity::new(f64::from(value), 0)?)), @@ -169,18 +171,18 @@ pub fn decode_optional_quantity_i32(value: i32) -> Result> { /// # Safety /// /// - Assumes `ptr` is a valid C string pointer. -pub unsafe fn raw_ptr_to_string(ptr: *const c_char) -> Result { +pub unsafe fn raw_ptr_to_string(ptr: *const c_char) -> anyhow::Result { let c_str: &CStr = unsafe { CStr::from_ptr(ptr) }; - let str_slice: &str = c_str.to_str().map_err(|e| anyhow!(e))?; + let str_slice: &str = c_str.to_str().map_err(|e| anyhow::anyhow!(e))?; Ok(str_slice.to_owned()) } /// # Safety /// /// - Assumes `ptr` is a valid C string pointer. -pub unsafe fn raw_ptr_to_ustr(ptr: *const c_char) -> Result { +pub unsafe fn raw_ptr_to_ustr(ptr: *const c_char) -> anyhow::Result { let c_str: &CStr = unsafe { CStr::from_ptr(ptr) }; - let str_slice: &str = c_str.to_str().map_err(|e| anyhow!(e))?; + let str_slice: &str = c_str.to_str().map_err(|e| anyhow::anyhow!(e))?; Ok(Ustr::from(str_slice)) } @@ -240,6 +242,8 @@ pub fn decode_futures_contract_v1( None, // TBD None, // TBD None, // TBD + None, // TBD + None, // TBD msg.ts_recv, // More accurate and reliable timestamp ts_init, ) @@ -275,6 +279,8 @@ pub fn decode_futures_spread_v1( None, // TBD None, // TBD None, // TBD + None, // TBD + None, // TBD msg.ts_recv, // More accurate and reliable timestamp ts_init, ) @@ -317,7 +323,9 @@ pub fn decode_options_contract_v1( None, // TBD None, // TBD None, // TBD - msg.ts_recv, // More accurate and reliable timestamp + None, + None, + msg.ts_recv, // More accurate and reliable timestamp ts_init, ) } @@ -359,6 +367,8 @@ pub fn decode_options_spread_v1( None, // TBD None, // TBD None, // TBD + None, // TBD + None, // TBD msg.ts_recv, // More accurate and reliable timestamp ts_init, ) @@ -541,7 +551,7 @@ pub fn decode_bar_type( // ohlcv-1d BarType::new(instrument_id, BAR_SPEC_1D, AggregationSource::External) } - _ => bail!( + _ => anyhow::bail!( "`rtype` is not a supported bar aggregation, was {}", msg.hd.rtype ), @@ -568,7 +578,7 @@ pub fn decode_ts_event_adjustment(msg: &dbn::OhlcvMsg) -> anyhow::Result bail!( + _ => anyhow::bail!( "`rtype` is not a supported bar aggregation, was {}", msg.hd.rtype ), @@ -618,7 +628,7 @@ pub fn decode_record( (Some(delta), None) => (Some(Data::Delta(delta)), None), (None, Some(trade)) => (Some(Data::Trade(trade)), None), (None, None) => (None, None), - _ => bail!("Invalid `MboMsg` parsing combination"), + _ => anyhow::bail!("Invalid `MboMsg` parsing combination"), } } else if let Some(msg) = record.get::() { let ts_init = determine_timestamp(ts_init, msg.ts_recv); @@ -640,7 +650,7 @@ pub fn decode_record( let bar = decode_ohlcv_msg(msg, instrument_id, price_precision, ts_init)?; (Some(Data::Bar(bar)), None) } else { - bail!("DBN message type is not currently supported") + anyhow::bail!("DBN message type is not currently supported") }; Ok(result) @@ -684,9 +694,9 @@ pub fn decode_instrument_def_msg_v1( instrument_id, ts_init, )?)), - 'B' => bail!("Unsupported `instrument_class` 'B' (BOND)"), - 'X' => bail!("Unsupported `instrument_class` 'X' (FX_SPOT)"), - _ => bail!( + 'B' => anyhow::bail!("Unsupported `instrument_class` 'B' (Bond)"), + 'X' => anyhow::bail!("Unsupported `instrument_class` 'X' (FX spot)"), + _ => anyhow::bail!( "Unsupported `instrument_class` '{}'", msg.instrument_class as u8 as char ), @@ -724,9 +734,9 @@ pub fn decode_instrument_def_msg( instrument_id, ts_init, )?)), - 'B' => bail!("Unsupported `instrument_class` 'B' (BOND)"), - 'X' => bail!("Unsupported `instrument_class` 'X' (FX_SPOT)"), - _ => bail!( + 'B' => anyhow::bail!("Unsupported `instrument_class` 'B' (Bond)"), + 'X' => anyhow::bail!("Unsupported `instrument_class` 'X' (FX spot)"), + _ => anyhow::bail!( "Unsupported `instrument_class` '{}'", msg.instrument_class as u8 as char ), @@ -785,11 +795,13 @@ pub fn decode_futures_contract( decode_price(msg.min_price_increment, currency.precision)?, Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD - None, // TBD - None, // TBD - None, // TBD - None, // TBD - msg.ts_recv, // More accurate and reliable timestamp + None, + None, // TBD + None, // TBD + None, // TBD + None, // TBD + None, // TBD + msg.ts_recv, // More accurate and reliable timestamp ts_init, ) } @@ -824,6 +836,8 @@ pub fn decode_futures_spread( None, // TBD None, // TBD None, // TBD + None, // TBD + None, // TBD msg.ts_recv, // More accurate and reliable timestamp ts_init, ) @@ -866,6 +880,8 @@ pub fn decode_options_contract( None, // TBD None, // TBD None, // TBD + None, // TBD + None, // TBD msg.ts_recv, // More accurate and reliable timestamp ts_init, ) @@ -908,6 +924,8 @@ pub fn decode_options_spread( None, // TBD None, // TBD None, // TBD + None, // TBD + None, // TBD msg.ts_recv, // More accurate and reliable timestamp ts_init, ) diff --git a/nautilus_core/adapters/src/databento/live.rs b/nautilus_core/adapters/src/databento/live.rs index e074f08b4f51..e71ea408fa58 100644 --- a/nautilus_core/adapters/src/databento/live.rs +++ b/nautilus_core/adapters/src/databento/live.rs @@ -15,8 +15,8 @@ use std::{collections::HashMap, ffi::CStr}; -use anyhow::{anyhow, Result}; use databento::{ + dbn, dbn::{PitSymbolMap, Record, SymbolIndex, VersionUpgradePolicy}, live::Subscription, }; @@ -103,7 +103,7 @@ impl DatabentoFeedHandler { } /// Run the feed handler to begin listening for commands and processing messages. - pub async fn run(&mut self) -> Result<()> { + pub async fn run(&mut self) -> anyhow::Result<()> { debug!("Running feed handler"); let clock = get_atomic_clock_realtime(); let mut symbol_map = PitSymbolMap::new(); @@ -129,7 +129,7 @@ impl DatabentoFeedHandler { Err(_) => { self.msg_tx.send(LiveMessage::Close).await?; self.cmd_rx.close(); - return Err(anyhow!("Timeout connecting to LSG")); + return Err(anyhow::anyhow!("Timeout connecting to LSG")); } }; @@ -198,7 +198,7 @@ impl DatabentoFeedHandler { Err(e) => { // Fail the session entirely for now. Consider refining // this strategy to handle specific errors more gracefully. - self.send_msg(LiveMessage::Error(anyhow!(e))).await; + self.send_msg(LiveMessage::Error(anyhow::anyhow!(e))).await; break; } }; @@ -374,7 +374,7 @@ fn handle_instrument_def_msg( msg: &dbn::InstrumentDefMsg, publisher_venue_map: &IndexMap, clock: &AtomicTime, -) -> Result { +) -> anyhow::Result { let c_str: &CStr = unsafe { CStr::from_ptr(msg.raw_symbol.as_ptr()) }; let raw_symbol: &str = c_str.to_str().map_err(to_pyvalue_err)?; diff --git a/nautilus_core/adapters/src/databento/loader.rs b/nautilus_core/adapters/src/databento/loader.rs index 8df18278c8a5..4f171b0cc8e1 100644 --- a/nautilus_core/adapters/src/databento/loader.rs +++ b/nautilus_core/adapters/src/databento/loader.rs @@ -15,7 +15,7 @@ use std::{env, fs, path::PathBuf}; -use anyhow::Result; +use databento::dbn; use dbn::{ compat::InstrumentDefMsgV1, decode::{dbn::Decoder, DbnMetadata, DecodeStream}, @@ -56,7 +56,7 @@ use super::{ /// - STATISTICS -> `DatabentoStatistics` /// /// # References -/// +/// #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.databento") @@ -68,7 +68,7 @@ pub struct DatabentoDataLoader { } impl DatabentoDataLoader { - pub fn new(path: Option) -> Result { + pub fn new(path: Option) -> anyhow::Result { let mut loader = Self { publishers_map: IndexMap::new(), venue_dataset_map: IndexMap::new(), @@ -93,7 +93,7 @@ impl DatabentoDataLoader { } /// Load the publishers data from the file at the given `path`. - pub fn load_publishers(&mut self, path: PathBuf) -> Result<()> { + pub fn load_publishers(&mut self, path: PathBuf) -> anyhow::Result<()> { let file_content = fs::read_to_string(path)?; let publishers: Vec = serde_json::from_str(&file_content)?; @@ -139,7 +139,7 @@ impl DatabentoDataLoader { self.publisher_venue_map.get(&publisher_id) } - pub fn schema_from_file(&self, path: PathBuf) -> Result> { + pub fn schema_from_file(&self, path: PathBuf) -> anyhow::Result> { let decoder = Decoder::from_zstd_file(path)?; let metadata = decoder.metadata(); Ok(metadata.schema.map(|schema| schema.to_string())) @@ -148,7 +148,7 @@ impl DatabentoDataLoader { pub fn read_definition_records( &mut self, path: PathBuf, - ) -> Result> + '_> { + ) -> anyhow::Result> + '_> { let mut decoder = Decoder::from_zstd_file(path)?; decoder.set_upgrade_policy(dbn::VersionUpgradePolicy::Upgrade); let mut dbn_stream = decoder.decode_stream::(); @@ -188,7 +188,7 @@ impl DatabentoDataLoader { path: PathBuf, instrument_id: Option, include_trades: bool, - ) -> Result, Option)>> + '_> + ) -> anyhow::Result, Option)>> + '_> where T: dbn::Record + dbn::HasRType + 'static, { @@ -233,7 +233,7 @@ impl DatabentoDataLoader { &self, path: PathBuf, instrument_id: Option, - ) -> Result> + '_> + ) -> anyhow::Result> + '_> where T: dbn::Record + dbn::HasRType + 'static, { @@ -275,7 +275,7 @@ impl DatabentoDataLoader { &self, path: PathBuf, instrument_id: Option, - ) -> Result> + '_> + ) -> anyhow::Result> + '_> where T: dbn::Record + dbn::HasRType + 'static, { diff --git a/nautilus_core/adapters/src/databento/publishers.json b/nautilus_core/adapters/src/databento/publishers.json index f9f45ff0c837..cbd2492e6c9b 100644 --- a/nautilus_core/adapters/src/databento/publishers.json +++ b/nautilus_core/adapters/src/databento/publishers.json @@ -364,5 +364,119 @@ "dataset": "OPRA.PILLAR", "venue": "SPHR", "description": "OPRA - MIAX Sapphire" + }, + { + "publisher_id": 62, + "dataset": "DBEQ.MAX", + "venue": "XCHI", + "description": "DBEQ Max - NYSE Chicago" + }, + { + "publisher_id": 63, + "dataset": "DBEQ.MAX", + "venue": "XCIS", + "description": "DBEQ Max - NYSE National" + }, + { + "publisher_id": 64, + "dataset": "DBEQ.MAX", + "venue": "IEXG", + "description": "DBEQ Max - IEX" + }, + { + "publisher_id": 65, + "dataset": "DBEQ.MAX", + "venue": "EPRL", + "description": "DBEQ Max - MIAX Pearl" + }, + { + "publisher_id": 66, + "dataset": "DBEQ.MAX", + "venue": "XNAS", + "description": "DBEQ Max - Nasdaq" + }, + { + "publisher_id": 67, + "dataset": "DBEQ.MAX", + "venue": "XNYS", + "description": "DBEQ Max - NYSE" + }, + { + "publisher_id": 68, + "dataset": "DBEQ.MAX", + "venue": "FINN", + "description": "DBEQ Max - FINRA/NYSE TRF" + }, + { + "publisher_id": 69, + "dataset": "DBEQ.MAX", + "venue": "FINY", + "description": "DBEQ Max - FINRA/Nasdaq TRF Carteret" + }, + { + "publisher_id": 70, + "dataset": "DBEQ.MAX", + "venue": "FINC", + "description": "DBEQ Max - FINRA/Nasdaq TRF Chicago" + }, + { + "publisher_id": 71, + "dataset": "DBEQ.MAX", + "venue": "BATS", + "description": "DBEQ Max - CBOE BZX" + }, + { + "publisher_id": 72, + "dataset": "DBEQ.MAX", + "venue": "BATY", + "description": "DBEQ Max - CBOE BYX" + }, + { + "publisher_id": 73, + "dataset": "DBEQ.MAX", + "venue": "EDGA", + "description": "DBEQ Max - CBOE EDGA" + }, + { + "publisher_id": 74, + "dataset": "DBEQ.MAX", + "venue": "EDGX", + "description": "DBEQ Max - CBOE EDGX" + }, + { + "publisher_id": 75, + "dataset": "DBEQ.MAX", + "venue": "XBOS", + "description": "DBEQ Max - Nasdaq BX" + }, + { + "publisher_id": 76, + "dataset": "DBEQ.MAX", + "venue": "XPSX", + "description": "DBEQ Max - Nasdaq PSX" + }, + { + "publisher_id": 77, + "dataset": "DBEQ.MAX", + "venue": "MEMX", + "description": "DBEQ Max - MEMX" + }, + { + "publisher_id": 78, + "dataset": "DBEQ.MAX", + "venue": "XASE", + "description": "DBEQ Max - NYSE American" + }, + { + "publisher_id": 79, + "dataset": "DBEQ.MAX", + "venue": "ARCX", + "description": "DBEQ Max - NYSE Arca" + }, + { + "publisher_id": 80, + "dataset": "DBEQ.MAX", + "venue": "LTSE", + "description": "DBEQ Max - Long-Term Stock Exchange" } ] diff --git a/nautilus_core/adapters/src/databento/python/decode.rs b/nautilus_core/adapters/src/databento/python/decode.rs deleted file mode 100644 index 233d9effd911..000000000000 --- a/nautilus_core/adapters/src/databento/python/decode.rs +++ /dev/null @@ -1,127 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (C) 2015-2024 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. -// ------------------------------------------------------------------------------------------------- - -use anyhow::bail; -use nautilus_core::time::UnixNanos; -use nautilus_model::{ - data::{depth::OrderBookDepth10, trade::TradeTick}, - identifiers::instrument_id::InstrumentId, - instruments::{ - equity::Equity, futures_contract::FuturesContract, options_contract::OptionsContract, - }, -}; -use pyo3::{prelude::*, types::PyTuple}; - -use crate::databento::decode::{ - decode_equity_v1, decode_futures_contract_v1, decode_mbo_msg, decode_mbp10_msg, - decode_mbp1_msg, decode_options_contract_v1, decode_trade_msg, -}; - -#[pyfunction] -#[pyo3(name = "decode_equity")] -pub fn py_decode_equity( - record: &dbn::compat::InstrumentDefMsgV1, - instrument_id: InstrumentId, - ts_init: UnixNanos, -) -> anyhow::Result { - decode_equity_v1(record, instrument_id, ts_init) -} - -#[pyfunction] -#[pyo3(name = "decode_futures_contract")] -pub fn py_decode_futures_contract( - record: &dbn::compat::InstrumentDefMsgV1, - instrument_id: InstrumentId, - ts_init: UnixNanos, -) -> anyhow::Result { - decode_futures_contract_v1(record, instrument_id, ts_init) -} - -#[pyfunction] -#[pyo3(name = "decode_options_contract")] -pub fn py_decode_options_contract( - record: &dbn::compat::InstrumentDefMsgV1, - instrument_id: InstrumentId, - ts_init: UnixNanos, -) -> anyhow::Result { - decode_options_contract_v1(record, instrument_id, ts_init) -} - -#[pyfunction] -#[pyo3(name = "decode_mbo_msg")] -pub fn py_decode_mbo_msg( - py: Python, - record: &dbn::MboMsg, - instrument_id: InstrumentId, - price_precision: u8, - ts_init: UnixNanos, -) -> anyhow::Result { - let (data, _) = decode_mbo_msg(record, instrument_id, price_precision, ts_init, false)?; - if let Some(data) = data { - Ok(data.into_py(py)) - } else { - bail!("Error decoding MBO message") - } -} - -#[pyfunction] -#[pyo3(name = "decode_trade_msg")] -pub fn py_decode_trade_msg( - record: &dbn::TradeMsg, - instrument_id: InstrumentId, - price_precision: u8, - ts_init: UnixNanos, -) -> anyhow::Result { - decode_trade_msg(record, instrument_id, price_precision, ts_init) -} - -#[pyfunction] -#[pyo3(name = "decode_mbp1_msg")] -pub fn py_decode_mbp1_msg( - py: Python, - record: &dbn::Mbp1Msg, - instrument_id: InstrumentId, - price_precision: u8, - ts_init: UnixNanos, - include_trades: bool, -) -> anyhow::Result { - let (quote, maybe_trade) = decode_mbp1_msg( - record, - instrument_id, - price_precision, - ts_init, - include_trades, - )?; - - let quote_py = quote.into_py(py); - match maybe_trade { - Some(trade) => { - let trade_py = trade.into_py(py); - Ok(PyTuple::new(py, &[quote_py, trade_py]).into_py(py)) - } - None => Ok(PyTuple::new(py, &[quote_py, py.None()]).into_py(py)), - } -} - -#[pyfunction] -#[pyo3(name = "decode_mbp10_msg")] -pub fn py_decode_mbp10_msg( - record: &dbn::Mbp10Msg, - instrument_id: InstrumentId, - price_precision: u8, - ts_init: UnixNanos, -) -> anyhow::Result { - decode_mbp10_msg(record, instrument_id, price_precision, ts_init) -} diff --git a/nautilus_core/adapters/src/databento/python/enums.rs b/nautilus_core/adapters/src/databento/python/enums.rs index db045cca0f5d..40006e676489 100644 --- a/nautilus_core/adapters/src/databento/python/enums.rs +++ b/nautilus_core/adapters/src/databento/python/enums.rs @@ -15,6 +15,7 @@ use std::str::FromStr; +use nautilus_core::python::to_pyvalue_err; use pyo3::{prelude::*, types::PyType, PyTypeInfo}; use crate::databento::enums::{DatabentoStatisticType, DatabentoStatisticUpdateAction}; @@ -22,9 +23,9 @@ use crate::databento::enums::{DatabentoStatisticType, DatabentoStatisticUpdateAc #[pymethods] impl DatabentoStatisticType { #[new] - fn py_new(py: Python<'_>, value: &PyAny) -> anyhow::Result { + fn py_new(py: Python<'_>, value: &PyAny) -> PyResult { let t = Self::type_object(py); - Self::py_from_str(t, value) + Self::py_from_str(t, value).map_err(to_pyvalue_err) } fn __hash__(&self) -> isize { @@ -63,10 +64,10 @@ impl DatabentoStatisticType { #[classmethod] #[pyo3(name = "from_str")] - fn py_from_str(_: &PyType, data: &PyAny) -> anyhow::Result { + fn py_from_str(_: &PyType, data: &PyAny) -> PyResult { let data_str: &str = data.str().and_then(|s| s.extract())?; let tokenized = data_str.to_uppercase(); - Self::from_str(&tokenized).map_err(anyhow::Error::new) + Self::from_str(&tokenized).map_err(to_pyvalue_err) } #[classattr] #[pyo3(name = "OPENING_PRICE")] @@ -150,9 +151,9 @@ impl DatabentoStatisticType { #[pymethods] impl DatabentoStatisticUpdateAction { #[new] - fn py_new(py: Python<'_>, value: &PyAny) -> anyhow::Result { + fn py_new(py: Python<'_>, value: &PyAny) -> PyResult { let t = Self::type_object(py); - Self::py_from_str(t, value) + Self::py_from_str(t, value).map_err(to_pyvalue_err) } fn __hash__(&self) -> isize { @@ -191,10 +192,10 @@ impl DatabentoStatisticUpdateAction { #[classmethod] #[pyo3(name = "from_str")] - fn py_from_str(_: &PyType, data: &PyAny) -> anyhow::Result { + fn py_from_str(_: &PyType, data: &PyAny) -> PyResult { let data_str: &str = data.str().and_then(|s| s.extract())?; let tokenized = data_str.to_uppercase(); - Self::from_str(&tokenized).map_err(anyhow::Error::new) + Self::from_str(&tokenized).map_err(to_pyvalue_err) } #[classattr] #[pyo3(name = "ADDED")] diff --git a/nautilus_core/adapters/src/databento/python/historical.rs b/nautilus_core/adapters/src/databento/python/historical.rs index 9ff0317509ca..f8210823ea87 100644 --- a/nautilus_core/adapters/src/databento/python/historical.rs +++ b/nautilus_core/adapters/src/databento/python/historical.rs @@ -13,9 +13,12 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::{fs, num::NonZeroU64, sync::Arc}; +use std::{fs, num::NonZeroU64, str::FromStr, sync::Arc}; -use databento::historical::timeseries::GetRangeParams; +use databento::{ + dbn::{self, SType}, + historical::timeseries::GetRangeParams, +}; use indexmap::IndexMap; use nautilus_core::{ python::to_pyvalue_err, @@ -41,7 +44,7 @@ use crate::databento::{ decode_imbalance_msg, decode_instrument_def_msg, decode_record, decode_statistics_msg, raw_ptr_to_ustr, }, - symbology::decode_nautilus_instrument_id, + symbology::{check_consistent_symbology, decode_nautilus_instrument_id, infer_symbology_type}, types::{DatabentoImbalance, DatabentoPublisher, DatabentoStatistics, PublisherId}, }; @@ -60,13 +63,16 @@ pub struct DatabentoHistoricalClient { #[pymethods] impl DatabentoHistoricalClient { #[new] - fn py_new(key: String, publishers_path: &str) -> anyhow::Result { + fn py_new(key: String, publishers_path: &str) -> PyResult { let client = databento::HistoricalClient::builder() - .key(key.clone())? - .build()?; + .key(key.clone()) + .map_err(to_pyvalue_err)? + .build() + .map_err(to_pyvalue_err)?; let file_content = fs::read_to_string(publishers_path)?; - let publishers_vec: Vec = serde_json::from_str(&file_content)?; + let publishers_vec: Vec = + serde_json::from_str(&file_content).map_err(to_pyvalue_err)?; let publisher_venue_map = publishers_vec .into_iter() @@ -107,19 +113,22 @@ impl DatabentoHistoricalClient { &self, py: Python<'py>, dataset: String, - symbols: String, + symbols: Vec<&str>, start: UnixNanos, end: Option, limit: Option, ) -> PyResult<&'py PyAny> { let client = self.inner.clone(); + let stype_in = infer_symbology_type(symbols.first().unwrap()); + check_consistent_symbology(symbols.as_slice()).map_err(to_pyvalue_err)?; let end = end.unwrap_or(self.clock.get_time_ns()); let time_range = get_date_time_range(start, end).map_err(to_pyvalue_err)?; let params = GetRangeParams::builder() .dataset(dataset) .date_time_range(time_range) .symbols(symbols) + .stype_in(SType::from_str(&stype_in).map_err(to_pyvalue_err)?) .schema(dbn::Schema::Definition) .limit(limit.and_then(NonZeroU64::new)) .build(); @@ -172,19 +181,22 @@ impl DatabentoHistoricalClient { &self, py: Python<'py>, dataset: String, - symbols: String, + symbols: Vec<&str>, start: UnixNanos, end: Option, limit: Option, ) -> PyResult<&'py PyAny> { let client = self.inner.clone(); + let stype_in = infer_symbology_type(symbols.first().unwrap()); + check_consistent_symbology(symbols.as_slice()).map_err(to_pyvalue_err)?; let end = end.unwrap_or(self.clock.get_time_ns()); let time_range = get_date_time_range(start, end).map_err(to_pyvalue_err)?; let params = GetRangeParams::builder() .dataset(dataset) .date_time_range(time_range) .symbols(symbols) + .stype_in(SType::from_str(&stype_in).map_err(to_pyvalue_err)?) .schema(dbn::Schema::Mbp1) .limit(limit.and_then(NonZeroU64::new)) .build(); @@ -236,19 +248,22 @@ impl DatabentoHistoricalClient { &self, py: Python<'py>, dataset: String, - symbols: String, + symbols: Vec<&str>, start: UnixNanos, end: Option, limit: Option, ) -> PyResult<&'py PyAny> { let client = self.inner.clone(); + let stype_in = infer_symbology_type(symbols.first().unwrap()); + check_consistent_symbology(symbols.as_slice()).map_err(to_pyvalue_err)?; let end = end.unwrap_or(self.clock.get_time_ns()); let time_range = get_date_time_range(start, end).map_err(to_pyvalue_err)?; let params = GetRangeParams::builder() .dataset(dataset) .date_time_range(time_range) .symbols(symbols) + .stype_in(SType::from_str(&stype_in).map_err(to_pyvalue_err)?) .schema(dbn::Schema::Trades) .limit(limit.and_then(NonZeroU64::new)) .build(); @@ -301,7 +316,7 @@ impl DatabentoHistoricalClient { &self, py: Python<'py>, dataset: String, - symbols: String, + symbols: Vec<&str>, aggregation: BarAggregation, start: UnixNanos, end: Option, @@ -309,6 +324,8 @@ impl DatabentoHistoricalClient { ) -> PyResult<&'py PyAny> { let client = self.inner.clone(); + let stype_in = infer_symbology_type(symbols.first().unwrap()); + check_consistent_symbology(symbols.as_slice()).map_err(to_pyvalue_err)?; let schema = match aggregation { BarAggregation::Second => dbn::Schema::Ohlcv1S, BarAggregation::Minute => dbn::Schema::Ohlcv1M, @@ -322,6 +339,7 @@ impl DatabentoHistoricalClient { .dataset(dataset) .date_time_range(time_range) .symbols(symbols) + .stype_in(SType::from_str(&stype_in).map_err(to_pyvalue_err)?) .schema(schema) .limit(limit.and_then(NonZeroU64::new)) .build(); @@ -374,19 +392,22 @@ impl DatabentoHistoricalClient { &self, py: Python<'py>, dataset: String, - symbols: String, + symbols: Vec<&str>, start: UnixNanos, end: Option, limit: Option, ) -> PyResult<&'py PyAny> { let client = self.inner.clone(); + let stype_in = infer_symbology_type(symbols.first().unwrap()); + check_consistent_symbology(symbols.as_slice()).map_err(to_pyvalue_err)?; let end = end.unwrap_or(self.clock.get_time_ns()); let time_range = get_date_time_range(start, end).map_err(to_pyvalue_err)?; let params = GetRangeParams::builder() .dataset(dataset) .date_time_range(time_range) .symbols(symbols) + .stype_in(SType::from_str(&stype_in).map_err(to_pyvalue_err)?) .schema(dbn::Schema::Imbalance) .limit(limit.and_then(NonZeroU64::new)) .build(); @@ -428,19 +449,22 @@ impl DatabentoHistoricalClient { &self, py: Python<'py>, dataset: String, - symbols: String, + symbols: Vec<&str>, start: UnixNanos, end: Option, limit: Option, ) -> PyResult<&'py PyAny> { let client = self.inner.clone(); + let stype_in = infer_symbology_type(symbols.first().unwrap()); + check_consistent_symbology(symbols.as_slice()).map_err(to_pyvalue_err)?; let end = end.unwrap_or(self.clock.get_time_ns()); let time_range = get_date_time_range(start, end).map_err(to_pyvalue_err)?; let params = GetRangeParams::builder() .dataset(dataset) .date_time_range(time_range) .symbols(symbols) + .stype_in(SType::from_str(&stype_in).map_err(to_pyvalue_err)?) .schema(dbn::Schema::Statistics) .limit(limit.and_then(NonZeroU64::new)) .build(); diff --git a/nautilus_core/adapters/src/databento/python/live.rs b/nautilus_core/adapters/src/databento/python/live.rs index 0473d91eb080..44dffbd62bb7 100644 --- a/nautilus_core/adapters/src/databento/python/live.rs +++ b/nautilus_core/adapters/src/databento/python/live.rs @@ -15,7 +15,7 @@ use std::{fs, str::FromStr}; -use databento::live::Subscription; +use databento::{dbn, live::Subscription}; use indexmap::IndexMap; use nautilus_core::{ python::{to_pyruntime_err, to_pyvalue_err}, @@ -30,6 +30,7 @@ use tracing::{debug, error, trace}; use super::loader::convert_instrument_to_pyobject; use crate::databento::{ live::{DatabentoFeedHandler, LiveCommand, LiveMessage}, + symbology::{check_consistent_symbology, infer_symbology_type}, types::DatabentoPublisher, }; @@ -120,9 +121,10 @@ fn call_python(py: Python, callback: &PyObject, py_obj: PyObject) -> PyResult<() #[pymethods] impl DatabentoLiveClient { #[new] - pub fn py_new(key: String, dataset: String, publishers_path: String) -> anyhow::Result { + pub fn py_new(key: String, dataset: String, publishers_path: String) -> PyResult { let publishers_json = fs::read_to_string(publishers_path)?; - let publishers_vec: Vec = serde_json::from_str(&publishers_json)?; + let publishers_vec: Vec = + serde_json::from_str(&publishers_json).map_err(to_pyvalue_err)?; let publisher_venue_map = publishers_vec .into_iter() .map(|p| (p.publisher_id, Venue::from(p.venue.as_str()))) @@ -149,12 +151,11 @@ impl DatabentoLiveClient { fn py_subscribe( &mut self, schema: String, - symbols: String, - stype_in: Option, + symbols: Vec<&str>, start: Option, ) -> PyResult<()> { - let stype_in = stype_in.unwrap_or("raw_symbol".to_string()); - + let stype_in = infer_symbology_type(symbols.first().unwrap()); + check_consistent_symbology(symbols.as_slice()).map_err(to_pyvalue_err)?; let mut sub = Subscription::builder() .symbols(symbols) .schema(dbn::Schema::from_str(&schema).map_err(to_pyvalue_err)?) diff --git a/nautilus_core/adapters/src/databento/python/loader.rs b/nautilus_core/adapters/src/databento/python/loader.rs index abc971ab3a5f..7890b2640fdd 100644 --- a/nautilus_core/adapters/src/databento/python/loader.rs +++ b/nautilus_core/adapters/src/databento/python/loader.rs @@ -15,6 +15,7 @@ use std::{collections::HashMap, path::PathBuf}; +use databento::dbn; use nautilus_core::{ffi::cvec::CVec, python::to_pyvalue_err}; use nautilus_model::{ data::{ @@ -42,9 +43,9 @@ impl DatabentoDataLoader { } #[pyo3(name = "load_publishers")] - fn py_load_publishers(&mut self, path: String) -> anyhow::Result<()> { + fn py_load_publishers(&mut self, path: String) -> PyResult<()> { let path_buf = PathBuf::from(path); - self.load_publishers(path_buf) + self.load_publishers(path_buf).map_err(to_pyvalue_err) } #[must_use] @@ -71,14 +72,17 @@ impl DatabentoDataLoader { } #[pyo3(name = "schema_for_file")] - fn py_schema_for_file(&self, path: String) -> anyhow::Result> { + fn py_schema_for_file(&self, path: String) -> PyResult> { self.schema_from_file(PathBuf::from(path)) + .map_err(to_pyvalue_err) } #[pyo3(name = "load_instruments")] - fn py_load_instruments(&mut self, py: Python, path: String) -> anyhow::Result { + fn py_load_instruments(&mut self, py: Python, path: String) -> PyResult { let path_buf = PathBuf::from(path); - let iter = self.read_definition_records(path_buf)?; + let iter = self + .read_definition_records(path_buf) + .map_err(to_pyvalue_err)?; let mut data = Vec::new(); for result in iter { @@ -102,9 +106,11 @@ impl DatabentoDataLoader { &self, path: String, instrument_id: Option, - ) -> anyhow::Result> { + ) -> PyResult> { let path_buf = PathBuf::from(path); - let iter = self.read_records::(path_buf, instrument_id, false)?; + let iter = self + .read_records::(path_buf, instrument_id, false) + .map_err(to_pyvalue_err)?; let mut data = Vec::new(); for result in iter { @@ -115,7 +121,7 @@ impl DatabentoDataLoader { } } Ok((None, _)) => continue, - Err(e) => return Err(e), + Err(e) => return Err(to_pyvalue_err(e)), } } @@ -129,15 +135,13 @@ impl DatabentoDataLoader { path: String, instrument_id: Option, include_trades: Option, - ) -> anyhow::Result { + ) -> PyResult { let path_buf = PathBuf::from(path); - let iter = self.read_records::( - path_buf, - instrument_id, - include_trades.unwrap_or(false), - )?; + let iter = self + .read_records::(path_buf, instrument_id, include_trades.unwrap_or(false)) + .map_err(to_pyvalue_err)?; - exhaust_data_iter_to_pycapsule(py, iter) + exhaust_data_iter_to_pycapsule(py, iter).map_err(to_pyvalue_err) } #[pyo3(name = "load_order_book_depth10")] @@ -145,9 +149,11 @@ impl DatabentoDataLoader { &self, path: String, instrument_id: Option, - ) -> anyhow::Result> { + ) -> PyResult> { let path_buf = PathBuf::from(path); - let iter = self.read_records::(path_buf, instrument_id, false)?; + let iter = self + .read_records::(path_buf, instrument_id, false) + .map_err(to_pyvalue_err)?; let mut data = Vec::new(); for result in iter { @@ -158,7 +164,7 @@ impl DatabentoDataLoader { } } Ok((None, _)) => continue, - Err(e) => return Err(e), + Err(e) => return Err(to_pyvalue_err(e)), } } @@ -171,11 +177,13 @@ impl DatabentoDataLoader { py: Python, path: String, instrument_id: Option, - ) -> anyhow::Result { + ) -> PyResult { let path_buf = PathBuf::from(path); - let iter = self.read_records::(path_buf, instrument_id, false)?; + let iter = self + .read_records::(path_buf, instrument_id, false) + .map_err(to_pyvalue_err)?; - exhaust_data_iter_to_pycapsule(py, iter) + exhaust_data_iter_to_pycapsule(py, iter).map_err(to_pyvalue_err) } #[pyo3(name = "load_quotes")] @@ -184,13 +192,11 @@ impl DatabentoDataLoader { path: String, instrument_id: Option, include_trades: Option, - ) -> anyhow::Result> { + ) -> PyResult> { let path_buf = PathBuf::from(path); - let iter = self.read_records::( - path_buf, - instrument_id, - include_trades.unwrap_or(false), - )?; + let iter = self + .read_records::(path_buf, instrument_id, include_trades.unwrap_or(false)) + .map_err(to_pyvalue_err)?; let mut data = Vec::new(); for result in iter { @@ -201,7 +207,7 @@ impl DatabentoDataLoader { } } Ok((None, _)) => continue, - Err(e) => return Err(e), + Err(e) => return Err(to_pyvalue_err(e)), } } @@ -215,15 +221,13 @@ impl DatabentoDataLoader { path: String, instrument_id: Option, include_trades: Option, - ) -> anyhow::Result { + ) -> PyResult { let path_buf = PathBuf::from(path); - let iter = self.read_records::( - path_buf, - instrument_id, - include_trades.unwrap_or(false), - )?; + let iter = self + .read_records::(path_buf, instrument_id, include_trades.unwrap_or(false)) + .map_err(to_pyvalue_err)?; - exhaust_data_iter_to_pycapsule(py, iter) + exhaust_data_iter_to_pycapsule(py, iter).map_err(to_pyvalue_err) } #[pyo3(name = "load_tbbo_trades")] @@ -231,9 +235,11 @@ impl DatabentoDataLoader { &self, path: String, instrument_id: Option, - ) -> anyhow::Result> { + ) -> PyResult> { let path_buf = PathBuf::from(path); - let iter = self.read_records::(path_buf, instrument_id, false)?; + let iter = self + .read_records::(path_buf, instrument_id, false) + .map_err(to_pyvalue_err)?; let mut data = Vec::new(); for result in iter { @@ -243,7 +249,7 @@ impl DatabentoDataLoader { data.push(trade); } } - Err(e) => return Err(e), + Err(e) => return Err(to_pyvalue_err(e)), } } @@ -256,11 +262,13 @@ impl DatabentoDataLoader { py: Python, path: String, instrument_id: Option, - ) -> anyhow::Result { + ) -> PyResult { let path_buf = PathBuf::from(path); - let iter = self.read_records::(path_buf, instrument_id, false)?; + let iter = self + .read_records::(path_buf, instrument_id, false) + .map_err(to_pyvalue_err)?; - exhaust_data_iter_to_pycapsule(py, iter) + exhaust_data_iter_to_pycapsule(py, iter).map_err(to_pyvalue_err) } #[pyo3(name = "load_trades")] @@ -268,9 +276,11 @@ impl DatabentoDataLoader { &self, path: String, instrument_id: Option, - ) -> anyhow::Result> { + ) -> PyResult> { let path_buf = PathBuf::from(path); - let iter = self.read_records::(path_buf, instrument_id, false)?; + let iter = self + .read_records::(path_buf, instrument_id, false) + .map_err(to_pyvalue_err)?; let mut data = Vec::new(); for result in iter { @@ -281,7 +291,7 @@ impl DatabentoDataLoader { } } Ok((None, _)) => continue, - Err(e) => return Err(e), + Err(e) => return Err(to_pyvalue_err(e)), } } @@ -294,11 +304,13 @@ impl DatabentoDataLoader { py: Python, path: String, instrument_id: Option, - ) -> anyhow::Result { + ) -> PyResult { let path_buf = PathBuf::from(path); - let iter = self.read_records::(path_buf, instrument_id, false)?; + let iter = self + .read_records::(path_buf, instrument_id, false) + .map_err(to_pyvalue_err)?; - exhaust_data_iter_to_pycapsule(py, iter) + exhaust_data_iter_to_pycapsule(py, iter).map_err(to_pyvalue_err) } #[pyo3(name = "load_bars")] @@ -306,9 +318,11 @@ impl DatabentoDataLoader { &self, path: String, instrument_id: Option, - ) -> anyhow::Result> { + ) -> PyResult> { let path_buf = PathBuf::from(path); - let iter = self.read_records::(path_buf, instrument_id, false)?; + let iter = self + .read_records::(path_buf, instrument_id, false) + .map_err(to_pyvalue_err)?; let mut data = Vec::new(); for result in iter { @@ -319,7 +333,7 @@ impl DatabentoDataLoader { } } Ok((None, _)) => continue, - Err(e) => return Err(e), + Err(e) => return Err(to_pyvalue_err(e)), } } @@ -332,11 +346,13 @@ impl DatabentoDataLoader { py: Python, path: String, instrument_id: Option, - ) -> anyhow::Result { + ) -> PyResult { let path_buf = PathBuf::from(path); - let iter = self.read_records::(path_buf, instrument_id, false)?; + let iter = self + .read_records::(path_buf, instrument_id, false) + .map_err(to_pyvalue_err)?; - exhaust_data_iter_to_pycapsule(py, iter) + exhaust_data_iter_to_pycapsule(py, iter).map_err(to_pyvalue_err) } #[pyo3(name = "load_imbalance")] @@ -344,15 +360,17 @@ impl DatabentoDataLoader { &self, path: String, instrument_id: Option, - ) -> anyhow::Result> { + ) -> PyResult> { let path_buf = PathBuf::from(path); - let iter = self.read_imbalance_records::(path_buf, instrument_id)?; + let iter = self + .read_imbalance_records::(path_buf, instrument_id) + .map_err(to_pyvalue_err)?; let mut data = Vec::new(); for result in iter { match result { Ok(item) => data.push(item), - Err(e) => return Err(e), + Err(e) => return Err(to_pyvalue_err(e)), } } @@ -364,15 +382,17 @@ impl DatabentoDataLoader { &self, path: String, instrument_id: Option, - ) -> anyhow::Result> { + ) -> PyResult> { let path_buf = PathBuf::from(path); - let iter = self.read_statistics_records::(path_buf, instrument_id)?; + let iter = self + .read_statistics_records::(path_buf, instrument_id) + .map_err(to_pyvalue_err)?; let mut data = Vec::new(); for result in iter { match result { Ok(item) => data.push(item), - Err(e) => return Err(e), + Err(e) => return Err(to_pyvalue_err(e)), } } diff --git a/nautilus_core/adapters/src/databento/python/mod.rs b/nautilus_core/adapters/src/databento/python/mod.rs index 5da249685115..1011a0b4c0d0 100644 --- a/nautilus_core/adapters/src/databento/python/mod.rs +++ b/nautilus_core/adapters/src/databento/python/mod.rs @@ -13,7 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -pub mod decode; pub mod enums; pub mod historical; pub mod live; @@ -33,13 +32,5 @@ pub fn databento(_: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_function(wrap_pyfunction!(decode::py_decode_equity, m)?)?; - m.add_function(wrap_pyfunction!(decode::py_decode_futures_contract, m)?)?; - m.add_function(wrap_pyfunction!(decode::py_decode_options_contract, m)?)?; - m.add_function(wrap_pyfunction!(decode::py_decode_mbo_msg, m)?)?; - m.add_function(wrap_pyfunction!(decode::py_decode_trade_msg, m)?)?; - m.add_function(wrap_pyfunction!(decode::py_decode_mbp1_msg, m)?)?; - m.add_function(wrap_pyfunction!(decode::py_decode_mbp10_msg, m)?)?; - Ok(()) } diff --git a/nautilus_core/adapters/src/databento/symbology.rs b/nautilus_core/adapters/src/databento/symbology.rs index 7225242d171b..8bd99bb691b0 100644 --- a/nautilus_core/adapters/src/databento/symbology.rs +++ b/nautilus_core/adapters/src/databento/symbology.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use anyhow::{anyhow, bail, Result}; +use databento::dbn; use dbn::Record; use indexmap::IndexMap; use nautilus_model::identifiers::{instrument_id::InstrumentId, symbol::Symbol, venue::Venue}; @@ -24,12 +24,12 @@ pub fn decode_nautilus_instrument_id( record: &dbn::RecordRef, metadata: &dbn::Metadata, publisher_venue_map: &IndexMap, -) -> Result { +) -> anyhow::Result { let publisher = record.publisher().expect("Invalid `publisher` for record"); let publisher_id = publisher as PublisherId; let venue = publisher_venue_map .get(&publisher_id) - .ok_or_else(|| anyhow!("`Venue` not found for `publisher_id` {publisher_id}"))?; + .ok_or_else(|| anyhow::anyhow!("`Venue` not found for `publisher_id` {publisher_id}"))?; let instrument_id = get_nautilus_instrument_id_for_record(record, metadata, *venue)?; Ok(instrument_id) @@ -39,7 +39,7 @@ pub fn get_nautilus_instrument_id_for_record( record: &dbn::RecordRef, metadata: &dbn::Metadata, venue: Venue, -) -> Result { +) -> anyhow::Result { let (instrument_id, nanoseconds) = if let Some(msg) = record.get::() { (msg.hd.instrument_id, msg.ts_recv) } else if let Some(msg) = record.get::() { @@ -55,7 +55,7 @@ pub fn get_nautilus_instrument_id_for_record( } else if let Some(msg) = record.get::() { (msg.hd.instrument_id, msg.ts_recv) } else { - bail!("DBN message type is not currently supported") + anyhow::bail!("DBN message type is not currently supported") }; let duration = time::Duration::nanoseconds(nanoseconds as i64); @@ -66,9 +66,109 @@ pub fn get_nautilus_instrument_id_for_record( let symbol_map = metadata.symbol_map_for_date(date)?; let raw_symbol = symbol_map .get(instrument_id) - .ok_or_else(|| anyhow!("No raw symbol found for {instrument_id}"))?; + .ok_or_else(|| anyhow::anyhow!("No raw symbol found for {instrument_id}"))?; let symbol = Symbol::from_str_unchecked(raw_symbol); Ok(InstrumentId::new(symbol, venue)) } + +#[must_use] +pub fn infer_symbology_type(symbol: &str) -> String { + if symbol.ends_with(".FUT") || symbol.ends_with(".OPT") { + return "parent".to_string(); + } + + let parts: Vec<&str> = symbol.split('.').collect(); + if parts.len() == 3 && parts[2].chars().all(|c| c.is_ascii_digit()) { + return "continuous".to_string(); + } + + if symbol.chars().all(|c| c.is_ascii_digit()) { + return "instrument_id".to_string(); + } + + "raw_symbol".to_string() +} + +pub fn check_consistent_symbology(symbols: &[&str]) -> anyhow::Result<()> { + if symbols.is_empty() { + return Err(anyhow::anyhow!("Symbols was empty")); + }; + + // SAFETY: We checked len so know there must be at least one symbol + let first_symbol = symbols.first().unwrap(); + let first_stype = infer_symbology_type(first_symbol); + + for symbol in symbols { + let next_stype = infer_symbology_type(symbol); + if next_stype != first_stype { + return Err(anyhow::anyhow!( + "Inconsistent symbology types: '{}' for {} vs '{}' for {}", + first_stype, + first_symbol, + next_stype, + symbol + )); + } + } + + Ok(()) +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod tests { + use rstest::*; + + use super::*; + + #[rstest] + #[case("1", "instrument_id")] + #[case("123456789", "instrument_id")] + #[case("AAPL", "raw_symbol")] + #[case("ESM4", "raw_symbol")] + #[case("BRN FMM0024!", "raw_symbol")] + #[case("BRN 99 5617289", "raw_symbol")] + #[case("SPY 240319P00511000", "raw_symbol")] + #[case("ES.FUT", "parent")] + #[case("ES.OPT", "parent")] + #[case("BRN.FUT", "parent")] + #[case("SPX.OPT", "parent")] + #[case("ES.c.0", "continuous")] + #[case("SPX.n.0", "continuous")] + fn test_infer_symbology_type(#[case] symbol: String, #[case] expected: String) { + let result = infer_symbology_type(&symbol); + assert_eq!(result, expected); + } + + #[rstest] + fn test_check_consistent_symbology_when_empty_symbols() { + let symbols: Vec<&str> = vec![]; + let result = check_consistent_symbology(&symbols); + assert!(result.is_err()); + assert_eq!(result.err().unwrap().to_string(), "Symbols was empty"); + } + + #[rstest] + fn test_check_consistent_symbology_when_inconsistent() { + let symbols = vec!["ESM4", "ES.OPT"]; + let result = check_consistent_symbology(&symbols); + assert!(result.is_err()); + assert_eq!( + result.err().unwrap().to_string(), + "Inconsistent symbology types: 'raw_symbol' for ESM4 vs 'parent' for ES.OPT" + ); + } + + #[rstest] + #[case(vec!["AAPL,MSFT"])] + #[case(vec!["ES.OPT,ES.FUT"])] + #[case(vec!["ES.c.0,ES.c.1"])] + fn test_check_consistent_symbology_when_consistent(#[case] symbols: Vec<&str>) { + let result = check_consistent_symbology(&symbols); + assert!(result.is_ok()); + } +} diff --git a/nautilus_core/adapters/src/databento/types.rs b/nautilus_core/adapters/src/databento/types.rs index 21057d6970b3..5f638648c284 100644 --- a/nautilus_core/adapters/src/databento/types.rs +++ b/nautilus_core/adapters/src/databento/types.rs @@ -15,6 +15,7 @@ use std::ffi::c_char; +use databento::dbn; use nautilus_core::time::UnixNanos; use nautilus_model::{ enums::OrderSide, diff --git a/nautilus_core/backtest/Cargo.toml b/nautilus_core/backtest/Cargo.toml index 9091b2caa12b..dd51d23fdefb 100644 --- a/nautilus_core/backtest/Cargo.toml +++ b/nautilus_core/backtest/Cargo.toml @@ -13,6 +13,7 @@ crate-type = ["rlib", "staticlib"] [dependencies] nautilus-common = { path = "../common" } nautilus-core = { path = "../core" } +nautilus-execution = { path = "../execution" } nautilus-model = { path = "../model" } pyo3 = { workspace = true, optional = true } ustr = { workspace = true } @@ -25,12 +26,25 @@ rstest = { workspace = true} cbindgen = { workspace = true, optional = true } [features] +default = ["ffi", "python"] extension-module = [ "pyo3/extension-module", "nautilus-common/extension-module", "nautilus-core/extension-module", + "nautilus-execution/extension-module", "nautilus-model/extension-module", ] -ffi = ["cbindgen", "nautilus-core/ffi", "nautilus-common/ffi"] -python = ["pyo3", "nautilus-core/python", "nautilus-common/python"] -default = ["ffi", "python"] +ffi = [ + "cbindgen", + "nautilus-core/ffi", + "nautilus-common/ffi", + "nautilus-execution/ffi", + "nautilus-model/ffi", +] +python = [ + "pyo3", + "nautilus-core/python", + "nautilus-common/python", + "nautilus-execution/python", + "nautilus-model/python", +] diff --git a/nautilus_core/backtest/src/lib.rs b/nautilus_core/backtest/src/lib.rs index 83e930fd7e61..186dfbc301a7 100644 --- a/nautilus_core/backtest/src/lib.rs +++ b/nautilus_core/backtest/src/lib.rs @@ -14,3 +14,4 @@ // ------------------------------------------------------------------------------------------------- pub mod engine; +pub mod matching_engine; diff --git a/nautilus_core/backtest/src/matching_engine.rs b/nautilus_core/backtest/src/matching_engine.rs new file mode 100644 index 000000000000..b0c51c4fee7d --- /dev/null +++ b/nautilus_core/backtest/src/matching_engine.rs @@ -0,0 +1,66 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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. +// ------------------------------------------------------------------------------------------------- + +#![allow(dead_code)] // Under development + +use std::collections::HashMap; + +use nautilus_common::msgbus::MessageBus; +use nautilus_core::time::AtomicTime; +use nautilus_execution::matching_core::OrderMatchingCore; +use nautilus_model::{ + data::bar::Bar, + enums::{AccountType, BookType, MarketStatus, OmsType}, + identifiers::{account_id::AccountId, trader_id::TraderId, venue::Venue}, + instruments::Instrument, + orderbook::{book_mbo::OrderBookMbo, book_mbp::OrderBookMbp}, + types::price::Price, +}; + +pub struct OrderMatchingEngineConfig { + pub bar_execution: bool, + pub reject_stop_orders: bool, + pub support_gtd_orders: bool, + pub support_contingent_orders: bool, + pub use_position_ids: bool, + pub use_random_ids: bool, + pub use_reduce_only: bool, +} + +pub struct OrderMatchingEngine { + pub venue: Venue, + pub instrument: Box, + pub raw_id: u64, + pub book_type: BookType, + pub oms_type: OmsType, + pub account_type: AccountType, + pub market_status: MarketStatus, + pub config: OrderMatchingEngineConfig, + // pub cache: Cache // TODO + clock: &'static AtomicTime, + msgbus: &'static MessageBus, + book_mbo: Option, + book_mbp: Option, + account_ids: HashMap, + core: OrderMatchingCore, + target_bid: Option, + target_ask: Option, + target_last: Option, + last_bar_bid: Option, + last_bar_ask: Option, + position_count: usize, + order_count: usize, + execution_count: usize, +} diff --git a/nautilus_core/common/Cargo.toml b/nautilus_core/common/Cargo.toml index afa48233f39b..5a01a635feff 100644 --- a/nautilus_core/common/Cargo.toml +++ b/nautilus_core/common/Cargo.toml @@ -39,6 +39,7 @@ tempfile = { workspace = true } cbindgen = { workspace = true, optional = true } [features] +default = [] extension-module = [ "pyo3/extension-module", "nautilus-core/extension-module", @@ -48,4 +49,3 @@ ffi = ["cbindgen", "nautilus-core/ffi", "nautilus-model/ffi"] python = ["pyo3", "pyo3-asyncio", "nautilus-core/python", "nautilus-model/python"] stubs = ["rstest", "nautilus-model/stubs"] redis = ["dep:redis"] -default = [] diff --git a/nautilus_core/common/src/cache.rs b/nautilus_core/common/src/cache.rs new file mode 100644 index 000000000000..18893356eda7 --- /dev/null +++ b/nautilus_core/common/src/cache.rs @@ -0,0 +1,157 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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. +// ------------------------------------------------------------------------------------------------- + +#![allow(dead_code)] // Under development + +use std::{ + collections::{HashMap, HashSet, VecDeque}, + sync::mpsc::Receiver, +}; + +use nautilus_core::uuid::UUID4; +use nautilus_model::{ + data::{ + bar::{Bar, BarType}, + quote::QuoteTick, + trade::TradeTick, + }, + identifiers::{ + account_id::AccountId, client_id::ClientId, client_order_id::ClientOrderId, + component_id::ComponentId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, + position_id::PositionId, strategy_id::StrategyId, symbol::Symbol, trader_id::TraderId, + venue::Venue, venue_order_id::VenueOrderId, + }, + instruments::{synthetic::SyntheticInstrument, Instrument}, + orders::base::Order, + position::Position, + types::currency::Currency, +}; +use ustr::Ustr; + +/// A type of database operation. +#[derive(Clone, Debug)] +pub enum DatabaseOperation { + Insert, + Update, + Delete, +} + +/// Represents a database command to be performed which may be executed 'remotely' across a thread. +#[derive(Clone, Debug)] +pub struct DatabaseCommand { + /// The database operation type. + pub op_type: DatabaseOperation, + /// The primary key for the operation. + pub key: String, + /// The data payload for the operation. + pub payload: Option>>, +} + +impl DatabaseCommand { + pub fn new(op_type: DatabaseOperation, key: String, payload: Option>>) -> Self { + Self { + op_type, + key, + payload, + } + } +} + +/// Provides a generic cache database facade. +/// +/// The main operations take a consistent `key` and `payload` which should provide enough +/// information to implement the cache database in many different technologies. +/// +/// Delete operations may need a `payload` to target specific values. +pub trait CacheDatabase { + type DatabaseType; + + fn new( + trader_id: TraderId, + instance_id: UUID4, + config: HashMap, + ) -> anyhow::Result; + fn flushdb(&mut self) -> anyhow::Result<()>; + fn keys(&mut self, pattern: &str) -> anyhow::Result>; + fn read(&mut self, key: &str) -> anyhow::Result>>; + fn insert(&mut self, key: String, payload: Option>>) -> anyhow::Result<()>; + fn update(&mut self, key: String, payload: Option>>) -> anyhow::Result<()>; + fn delete(&mut self, key: String, payload: Option>>) -> anyhow::Result<()>; + fn handle_messages( + rx: Receiver, + trader_key: String, + config: HashMap, + ); +} + +pub struct CacheConfig { + pub tick_capacity: usize, + pub bar_capacity: usize, + pub snapshot_orders: bool, + pub snapshot_positions: bool, +} + +pub struct CacheIndex { + venue_account: HashMap, + venue_orders: HashMap>, + venue_positions: HashMap>, + order_ids: HashMap, + order_position: HashMap, + order_strategy: HashMap, + order_client: HashMap, + position_strategy: HashMap, + position_orders: HashMap>, + instrument_orders: HashMap>, + instrument_positions: HashMap>, + strategy_orders: HashMap>, + strategy_positions: HashMap>, + exec_algorithm_orders: HashMap>, + exec_spawn_orders: HashMap>, + orders: HashSet, + orders_open: HashSet, + orders_closed: HashSet, + orders_emulated: HashSet, + orders_inflight: HashSet, + orders_pending_cancel: HashSet, + positions: HashSet, + positions_open: HashSet, + positions_closed: HashSet, + actors: HashSet, + strategies: HashSet, + exec_algorithms: HashSet, +} + +pub struct Cache { + config: CacheConfig, + index: CacheIndex, + // database: Option>, TODO + // xrate_calculator: ExchangeRateCalculator TODO + general: HashMap>, + xrate_symbols: HashMap, + quote_ticks: HashMap>, + trade_ticks: HashMap>, + // order_books: HashMap>, TODO: Needs single book + bars: HashMap>, + bars_bid: HashMap, + bars_ask: HashMap, + currencies: HashMap, + instruments: HashMap>, + synthetics: HashMap, + // accounts: HashMap>, TODO: Decide where trait should go + orders: HashMap>>, // TODO: Efficency (use enum) + // order_lists: HashMap>, TODO: Need `OrderList` + positions: HashMap, + position_snapshots: HashMap>, +} diff --git a/nautilus_core/common/src/clock.rs b/nautilus_core/common/src/clock.rs index d6dfd8b5c6f5..b60f9a03fdc5 100644 --- a/nautilus_core/common/src/clock.rs +++ b/nautilus_core/common/src/clock.rs @@ -184,7 +184,7 @@ impl Clock for TestClock { alert_time_ns: UnixNanos, callback: Option, ) { - check_valid_string(name, "`Timer` name").unwrap(); + check_valid_string(name, stringify!(name)).unwrap(); assert!( callback.is_some() | self.default_callback.is_some(), "All Python callbacks were `None`" @@ -211,7 +211,7 @@ impl Clock for TestClock { stop_time_ns: Option, callback: Option, ) { - check_valid_string(name, "`Timer` name").unwrap(); + check_valid_string(name, "name").unwrap(); assert!( callback.is_some() | self.default_callback.is_some(), "All Python callbacks were `None`" @@ -313,7 +313,7 @@ impl Clock for LiveClock { mut alert_time_ns: UnixNanos, callback: Option, ) { - check_valid_string(name, "`Timer` name").unwrap(); + check_valid_string(name, stringify!(name)).unwrap(); assert!( callback.is_some() | self.default_callback.is_some(), "All Python callbacks were `None`" @@ -345,7 +345,7 @@ impl Clock for LiveClock { stop_time_ns: Option, callback: Option, ) { - check_valid_string(name, "`Timer` name").unwrap(); + check_valid_string(name, stringify!(name)).unwrap(); assert!( callback.is_some() | self.default_callback.is_some(), "All Python callbacks were `None`" diff --git a/nautilus_core/common/src/enums.rs b/nautilus_core/common/src/enums.rs index 168569ca4d0e..a8fe533b4faa 100644 --- a/nautilus_core/common/src/enums.rs +++ b/nautilus_core/common/src/enums.rs @@ -203,28 +203,42 @@ pub enum LogLevel { )] pub enum LogColor { /// The default/normal log color. - #[strum(serialize = "")] + #[strum(serialize = "NORMAL")] Normal = 0, /// The green log color, typically used with [`LogLevel::Info`] log levels and associated with success events. - #[strum(serialize = "\x1b[92m")] + #[strum(serialize = "GREEN")] Green = 1, /// The blue log color, typically used with [`LogLevel::Info`] log levels and associated with user actions. - #[strum(serialize = "\x1b[94m")] + #[strum(serialize = "BLUE")] Blue = 2, /// The magenta log color, typically used with [`LogLevel::Info`] log levels. - #[strum(serialize = "\x1b[35m")] + #[strum(serialize = "MAGENTA")] Magenta = 3, /// The cyan log color, typically used with [`LogLevel::Info`] log levels. - #[strum(serialize = "\x1b[36m")] + #[strum(serialize = "CYAN")] Cyan = 4, /// The yellow log color, typically used with [`LogLevel::Warning`] log levels. - #[strum(serialize = "\x1b[1;33m")] + #[strum(serialize = "YELLOW")] Yellow = 5, /// The red log color, typically used with [`LogLevel::Error`] level. - #[strum(serialize = "\x1b[1;31m")] + #[strum(serialize = "RED")] Red = 6, } +impl LogColor { + pub fn as_ansi(&self) -> &str { + match *self { + Self::Normal => "", + Self::Green => "\x1b[92m", + Self::Blue => "\x1b[94m", + Self::Magenta => "\x1b[35m", + Self::Cyan => "\x1b[36m", + Self::Yellow => "\x1b[1;33m", + Self::Red => "\x1b[1;31m", + } + } +} + impl From for LogColor { fn from(value: u8) -> Self { match value { diff --git a/nautilus_core/common/src/ffi/logging.rs b/nautilus_core/common/src/ffi/logging.rs index 903d48d21235..8600e0e19514 100644 --- a/nautilus_core/common/src/ffi/logging.rs +++ b/nautilus_core/common/src/ffi/logging.rs @@ -13,7 +13,10 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::ffi::c_char; +use std::{ + ffi::c_char, + ops::{Deref, DerefMut}, +}; use nautilus_core::{ ffi::{ @@ -28,12 +31,38 @@ use crate::{ enums::{LogColor, LogLevel}, logging::{ self, headers, - logger::{self, LoggerConfig}, + logger::{self, LogGuard, LoggerConfig}, logging_set_bypass, map_log_level_to_filter, parse_component_levels, writer::FileWriterConfig, }, }; +/// Provides a C compatible Foreign Function Interface (FFI) for an underlying [`LogGuard`]. +/// +/// This struct wraps `LogGuard` in a way that makes it compatible with C function +/// calls, enabling interaction with `LogGuard` in a C environment. +/// +/// It implements the `Deref` trait, allowing instances of `LogGuard_API` to be +/// dereferenced to `LogGuard`, providing access to `LogGuard`'s methods without +/// having to manually access the underlying `LogGuard` instance. +#[repr(C)] +#[allow(non_camel_case_types)] +pub struct LogGuard_API(Box); + +impl Deref for LogGuard_API { + type Target = LogGuard; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for LogGuard_API { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + /// Initializes logging. /// /// Logging should be used for Python and sync Rust logic which is most of @@ -63,7 +92,7 @@ pub unsafe extern "C" fn logging_init( is_colored: u8, is_bypassed: u8, print_config: u8, -) { +) -> LogGuard_API { let level_stdout = map_log_level_to_filter(level_stdout); let level_file = map_log_level_to_filter(level_file); @@ -87,7 +116,13 @@ pub unsafe extern "C" fn logging_init( logging_set_bypass(); } - logging::init_logging(trader_id, instance_id, config, file_config); + LogGuard_API(Box::new(logging::init_logging( + trader_id, + instance_id, + config, + file_config, + ))) + // logging::init_logging(trader_id, instance_id, config, file_config); } /// Creates a new log event. @@ -138,8 +173,8 @@ pub unsafe extern "C" fn logging_log_sysinfo(component_ptr: *const c_char) { headers::log_sysinfo(component) } -/// Flushes global logger buffers. +/// Flushes global logger buffers of any records. #[no_mangle] -pub extern "C" fn logger_flush() { - log::logger().flush() +pub extern "C" fn logger_drop(log_guard: LogGuard_API) { + drop(log_guard) } diff --git a/nautilus_core/common/src/ffi/msgbus.rs b/nautilus_core/common/src/ffi/msgbus.rs index 1011cde514f2..477e90309dc0 100644 --- a/nautilus_core/common/src/ffi/msgbus.rs +++ b/nautilus_core/common/src/ffi/msgbus.rs @@ -80,12 +80,10 @@ pub unsafe extern "C" fn msgbus_new( let name = optional_cstr_to_str(name_ptr).map(|s| s.to_string()); let instance_id = UUID4::from(cstr_to_str(instance_id_ptr)); let config = optional_bytes_to_json(config_ptr); - MessageBus_API(Box::new(MessageBus::new( - trader_id, - instance_id, - name, - config, - ))) + MessageBus_API(Box::new( + MessageBus::new(trader_id, instance_id, name, config) + .expect("Error initializing `MessageBus`"), + )) } #[no_mangle] diff --git a/nautilus_core/common/src/lib.rs b/nautilus_core/common/src/lib.rs index 4f77acc139a0..bc8cbb6ef5b1 100644 --- a/nautilus_core/common/src/lib.rs +++ b/nautilus_core/common/src/lib.rs @@ -13,6 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +pub mod cache; pub mod clock; pub mod enums; pub mod factories; diff --git a/nautilus_core/common/src/logging/logger.rs b/nautilus_core/common/src/logging/logger.rs index 96ec5b62294e..e7ac3d32211d 100644 --- a/nautilus_core/common/src/logging/logger.rs +++ b/nautilus_core/common/src/logging/logger.rs @@ -24,6 +24,7 @@ use std::{ thread, }; +use indexmap::IndexMap; use log::{ debug, error, info, kv::{ToValue, Value}, @@ -35,7 +36,7 @@ use nautilus_core::{ uuid::UUID4, }; use nautilus_model::identifiers::trader_id::TraderId; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, Serializer}; use ustr::Ustr; use super::{LOGGING_BYPASSED, LOGGING_REALTIME}; @@ -175,6 +176,12 @@ pub struct LogLine { pub message: String, } +impl fmt::Display for LogLine { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}] {}: {}", self.level, self.component, self.message) + } +} + pub struct LogLineWrapper { line: LogLine, cache: Option, @@ -212,7 +219,7 @@ impl LogLineWrapper { format!( "\x1b[1m{}\x1b[0m {}[{}] {}.{}: {}\x1b[0m\n", self.timestamp, - &self.line.color.to_string(), + &self.line.color.as_ansi(), self.line.level, self.trader_id, &self.line.component, @@ -223,14 +230,25 @@ impl LogLineWrapper { pub fn get_json(&self) -> String { let json_string = - serde_json::to_string(&self.line).expect("Error serializing log event to string"); + serde_json::to_string(&self).expect("Error serializing log event to string"); format!("{json_string}\n") } } -impl fmt::Display for LogLine { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "[{}] {}: {}", self.level, self.component, self.message) +impl Serialize for LogLineWrapper { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut json_obj = IndexMap::new(); + json_obj.insert("timestamp".to_string(), self.timestamp.clone()); + json_obj.insert("trader_id".to_string(), self.trader_id.to_string()); + json_obj.insert("level".to_string(), self.line.level.to_string()); + json_obj.insert("color".to_string(), self.line.color.to_string()); + json_obj.insert("component".to_string(), self.line.component.to_string()); + json_obj.insert("message".to_string(), self.line.message.to_string()); + + json_obj.serialize(serializer) } } @@ -273,17 +291,23 @@ impl Log for Logger { #[allow(clippy::too_many_arguments)] impl Logger { - pub fn init_with_env(trader_id: TraderId, instance_id: UUID4, file_config: FileWriterConfig) { + #[must_use] + pub fn init_with_env( + trader_id: TraderId, + instance_id: UUID4, + file_config: FileWriterConfig, + ) -> LogGuard { let config = LoggerConfig::from_env(); - Logger::init_with_config(trader_id, instance_id, config, file_config); + Logger::init_with_config(trader_id, instance_id, config, file_config) } + #[must_use] pub fn init_with_config( trader_id: TraderId, instance_id: UUID4, config: LoggerConfig, file_config: FileWriterConfig, - ) { + ) -> LogGuard { let (tx, rx) = channel::(); let logger = Self { @@ -322,6 +346,8 @@ impl Logger { eprintln!("Cannot set logger because of error: {e}") } } + + LogGuard::new() } fn handle_messages( @@ -435,6 +461,31 @@ pub fn log(level: LogLevel, color: LogColor, component: Ustr, message: &str) { } } +#[cfg_attr( + feature = "python", + pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.common") +)] +#[derive(Debug)] +pub struct LogGuard {} + +impl LogGuard { + pub fn new() -> Self { + LogGuard {} + } +} + +impl Default for LogGuard { + fn default() -> Self { + Self::new() + } +} + +impl Drop for LogGuard { + fn drop(&mut self) { + log::logger().flush(); + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// @@ -521,7 +572,7 @@ mod tests { ..Default::default() }; - Logger::init_with_config( + let log_guard = Logger::init_with_config( TraderId::from("TRADER-001"), UUID4::new(), config, @@ -564,6 +615,8 @@ mod tests { Duration::from_secs(2), ); + drop(log_guard); // Ensure log buffers are flushed + assert_eq!( log_contents, "1970-01-20T02:20:00.000000000Z [INFO] TRADER-001.RiskEngine: This is a test.\n" @@ -580,7 +633,7 @@ mod tests { ..Default::default() }; - Logger::init_with_config( + let log_guard = Logger::init_with_config( TraderId::from("TRADER-001"), UUID4::new(), config, @@ -613,6 +666,8 @@ mod tests { Duration::from_secs(3), ); + drop(log_guard); // Ensure log buffers are flushed + assert!( std::fs::read_dir(&temp_dir) .expect("Failed to read directory") @@ -634,7 +689,7 @@ mod tests { ..Default::default() }; - Logger::init_with_config( + let log_guard = Logger::init_with_config( TraderId::from("TRADER-001"), UUID4::new(), config, @@ -669,9 +724,11 @@ mod tests { Duration::from_secs(2), ); + drop(log_guard); // Ensure log buffers are flushed + assert_eq!( log_contents, - "{\"level\":\"INFO\",\"color\":\"Normal\",\"component\":\"RiskEngine\",\"message\":\"This is a test.\"}\n" + "{\"timestamp\":\"1970-01-20T02:20:00.000000000Z\",\"trader_id\":\"TRADER-001\",\"level\":\"INFO\",\"color\":\"NORMAL\",\"component\":\"RiskEngine\",\"message\":\"This is a test.\"}\n" ); } } diff --git a/nautilus_core/common/src/logging/mod.rs b/nautilus_core/common/src/logging/mod.rs index d7f4b9e5eca6..55d4bf47f710 100644 --- a/nautilus_core/common/src/logging/mod.rs +++ b/nautilus_core/common/src/logging/mod.rs @@ -27,7 +27,7 @@ use tracing_subscriber::EnvFilter; use ustr::Ustr; use self::{ - logger::{Logger, LoggerConfig}, + logger::{LogGuard, Logger, LoggerConfig}, writer::FileWriterConfig, }; use crate::enums::LogLevel; @@ -116,10 +116,10 @@ pub fn init_logging( instance_id: UUID4, config: LoggerConfig, file_config: FileWriterConfig, -) { +) -> LogGuard { LOGGING_INITIALIZED.store(true, Ordering::Relaxed); LOGGING_COLORED.store(config.is_colored, Ordering::Relaxed); - Logger::init_with_config(trader_id, instance_id, config, file_config); + Logger::init_with_config(trader_id, instance_id, config, file_config) } pub fn map_log_level_to_filter(log_level: LogLevel) -> LevelFilter { diff --git a/nautilus_core/common/src/msgbus.rs b/nautilus_core/common/src/msgbus.rs index 3651d1707087..c15e90209839 100644 --- a/nautilus_core/common/src/msgbus.rs +++ b/nautilus_core/common/src/msgbus.rs @@ -168,13 +168,12 @@ pub struct MessageBus { impl MessageBus { /// Initializes a new instance of the [`MessageBus`]. - #[must_use] pub fn new( trader_id: TraderId, instance_id: UUID4, name: Option, config: Option>, - ) -> Self { + ) -> anyhow::Result { let config = config.unwrap_or_default(); let has_backing = config .get("database") @@ -183,16 +182,14 @@ impl MessageBus { let (tx, rx) = channel::(); let _join_handler = thread::Builder::new() .name("msgbus".to_string()) - .spawn(move || { - Self::handle_messages(rx, trader_id, instance_id, config); - }) + .spawn(move || Self::handle_messages(rx, trader_id, instance_id, config)) .expect("Error spawning `msgbus` thread"); Some(tx) } else { None }; - Self { + Ok(Self { tx, trader_id, instance_id, @@ -206,7 +203,7 @@ impl MessageBus { endpoints: IndexMap::new(), correlation_index: IndexMap::new(), has_backing, - } + }) } /// Returns the registered endpoint addresses. @@ -412,7 +409,7 @@ impl MessageBus { trader_id: TraderId, instance_id: UUID4, config: HashMap, - ) { + ) -> anyhow::Result<()> { let database_config = config .get("database") .expect("No `MessageBusConfig` `database` config specified"); @@ -436,8 +433,8 @@ fn handle_messages_with_redis_if_enabled( trader_id: TraderId, instance_id: UUID4, config: HashMap, -) { - handle_messages_with_redis(rx, trader_id, instance_id, config); +) -> anyhow::Result<()> { + handle_messages_with_redis(rx, trader_id, instance_id, config) } /// Handles messages using a default method if the "redis" feature is not enabled. diff --git a/nautilus_core/common/src/python/logging.rs b/nautilus_core/common/src/python/logging.rs index c6d8d903afa2..a4ad91bf8a43 100644 --- a/nautilus_core/common/src/python/logging.rs +++ b/nautilus_core/common/src/python/logging.rs @@ -25,12 +25,21 @@ use crate::{ enums::{LogColor, LogLevel}, logging::{ self, headers, - logger::{self, LoggerConfig}, + logger::{self, LogGuard, LoggerConfig}, logging_set_bypass, map_log_level_to_filter, parse_level_filter_str, writer::FileWriterConfig, }, }; +#[pymethods] +impl LoggerConfig { + #[staticmethod] + #[pyo3(name = "from_spec")] + pub fn py_from_spec(spec: String) -> Self { + LoggerConfig::from_spec(&spec) + } +} + #[pymethods] impl FileWriterConfig { #[new] @@ -43,15 +52,6 @@ impl FileWriterConfig { } } -#[pymethods] -impl LoggerConfig { - #[staticmethod] - #[pyo3(name = "from_spec")] - pub fn py_from_spec(spec: String) -> Self { - LoggerConfig::from_spec(&spec) - } -} - /// Initialize tracing. /// /// Tracing is meant to be used to trace/debug async Rust code. It can be @@ -94,7 +94,7 @@ pub fn py_init_logging( is_colored: Option, is_bypassed: Option, print_config: Option, -) { +) -> LogGuard { let level_file = level_file .map(map_log_level_to_filter) .unwrap_or(LevelFilter::Off); @@ -113,7 +113,7 @@ pub fn py_init_logging( logging_set_bypass(); } - logging::init_logging(trader_id, instance_id, config, file_config); + logging::init_logging(trader_id, instance_id, config, file_config) } fn parse_component_levels( diff --git a/nautilus_core/common/src/python/mod.rs b/nautilus_core/common/src/python/mod.rs index f96c979e0790..cf5e6c3a54ca 100644 --- a/nautilus_core/common/src/python/mod.rs +++ b/nautilus_core/common/src/python/mod.rs @@ -21,8 +21,6 @@ pub mod versioning; use pyo3::prelude::*; -use crate::logging::{logger::LoggerConfig, writer::FileWriterConfig}; - /// Loaded as nautilus_pyo3.common #[pymodule] pub fn common(_: Python<'_>, m: &PyModule) -> PyResult<()> { @@ -31,8 +29,9 @@ pub fn common(_: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_function(wrap_pyfunction!(logging::py_init_tracing, m)?)?; m.add_function(wrap_pyfunction!(logging::py_init_logging, m)?)?; m.add_function(wrap_pyfunction!(logging::py_logger_log, m)?)?; diff --git a/nautilus_core/common/src/redis.rs b/nautilus_core/common/src/redis.rs index e49ebacfbb56..99cbfb730bac 100644 --- a/nautilus_core/common/src/redis.rs +++ b/nautilus_core/common/src/redis.rs @@ -24,6 +24,7 @@ use nautilus_core::{time::duration_since_unix_epoch, uuid::UUID4}; use nautilus_model::identifiers::trader_id::TraderId; use redis::*; use serde_json::{json, Value}; +use tracing::debug; use crate::msgbus::BusMessage; @@ -36,10 +37,13 @@ pub fn handle_messages_with_redis( trader_id: TraderId, instance_id: UUID4, config: HashMap, -) { - let redis_url = get_redis_url(&config); - let client = redis::Client::open(redis_url).unwrap(); - let mut conn = client.get_connection().unwrap(); +) -> anyhow::Result<()> { + let database_config = config + .get("database") + .ok_or(anyhow::anyhow!("No database config"))?; + debug!("Creating msgbus redis connection"); + let mut conn = create_redis_connection(&database_config.clone())?; + let stream_name = get_stream_name(trader_id, instance_id, &config); // Autotrimming @@ -68,7 +72,7 @@ pub fn handle_messages_with_redis( autotrim_duration, &mut last_trim_index, &mut buffer, - ); + )?; last_drain = Instant::now(); } else { // Continue to receive and handle messages until channel is hung up @@ -88,8 +92,10 @@ pub fn handle_messages_with_redis( autotrim_duration, &mut last_trim_index, &mut buffer, - ); + )?; } + + Ok(()) } fn drain_buffer( @@ -98,7 +104,7 @@ fn drain_buffer( autotrim_duration: Option, last_trim_index: &mut HashMap, buffer: &mut VecDeque, -) { +) -> anyhow::Result<()> { let mut pipe = redis::pipe(); pipe.atomic(); @@ -133,39 +139,88 @@ fn drain_buffer( } } - if let Err(e) = pipe.query::<()>(conn) { - eprintln!("{e}"); - } + pipe.query::<()>(conn).map_err(anyhow::Error::from) } -pub fn get_redis_url(config: &HashMap) -> String { - let empty = Value::Object(serde_json::Map::new()); - let database = config.get("database").unwrap_or(&empty); - - let host = database +pub fn get_redis_url(database_config: &serde_json::Value) -> (String, String) { + let host = database_config .get("host") - .map(|v| v.as_str().unwrap_or("127.0.0.1")); - let port = database.get("port").map(|v| v.as_str().unwrap_or("6379")); - let username = database + .and_then(|v| v.as_str()) + .unwrap_or("127.0.0.1"); + let port = database_config + .get("port") + .and_then(|v| { + v.as_u64() + .or_else(|| v.as_str().and_then(|s| s.parse().ok())) + }) + .unwrap_or(6379); + let username = database_config .get("username") - .map(|v| v.as_str().unwrap_or_default()); - let password = database + .and_then(|v| v.as_str()) + .unwrap_or_default(); + let password = database_config .get("password") - .map(|v| v.as_str().unwrap_or_default()); - let use_ssl = database.get("ssl").unwrap_or(&json!(false)); + .and_then(|v| v.as_str()) + .unwrap_or_default(); + let use_ssl = database_config + .get("ssl") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let redacted_password = if password.len() > 4 { + format!("{}...{}", &password[..2], &password[password.len() - 2..],) + } else { + password.to_string() + }; - format!( - "redis{}://{}:{}@{}:{}", - if use_ssl.as_bool().unwrap_or(false) { - "s" - } else { - "" - }, - username.unwrap_or(""), - password.unwrap_or(""), - host.unwrap(), - port.unwrap(), - ) + let auth_part = if !username.is_empty() && !password.is_empty() { + format!("{}:{}@", username, password) + } else { + String::new() + }; + + let redacted_auth_part = if !username.is_empty() && !password.is_empty() { + format!("{}:{}@", username, redacted_password) + } else { + String::new() + }; + + let url = format!( + "redis{}://{}{}:{}", + if use_ssl { "s" } else { "" }, + auth_part, + host, + port + ); + + let redacted_url = format!( + "redis{}://{}{}:{}", + if use_ssl { "s" } else { "" }, + redacted_auth_part, + host, + port + ); + + (url, redacted_url) +} + +pub fn create_redis_connection(database_config: &serde_json::Value) -> RedisResult { + let (redis_url, redacted_url) = get_redis_url(database_config); + debug!("Connecting to {redacted_url}"); + let default_timeout = 20; + let timeout = get_timeout_duration(database_config, default_timeout); + let client = redis::Client::open(redis_url)?; + let conn = client.get_connection_with_timeout(timeout)?; + debug!("Connected"); + Ok(conn) +} + +pub fn get_timeout_duration(database_config: &serde_json::Value, default: u64) -> Duration { + let timeout_seconds = database_config + .get("timeout") + .and_then(|v| v.as_u64()) + .unwrap_or(default); + Duration::from_secs(timeout_seconds) } pub fn get_buffer_interval(config: &HashMap) -> Duration { @@ -203,7 +258,6 @@ fn get_stream_name( .expect("Invalid configuration: `streams_prefix` is not a string"); stream_name.push_str(stream_prefix); stream_name.push(DELIMITER); - stream_name } @@ -216,6 +270,102 @@ mod tests { use super::*; + #[rstest] + fn test_get_redis_url_default_values() { + let config = json!({}); + let (url, redacted_url) = get_redis_url(&config); + assert_eq!(url, "redis://127.0.0.1:6379"); + assert_eq!(redacted_url, "redis://127.0.0.1:6379"); + } + + #[rstest] + fn test_get_redis_url_full_config_with_ssl() { + let config = json!({ + "host": "example.com", + "port": 6380, + "username": "user", + "password": "pass", + "ssl": true, + }); + let (url, redacted_url) = get_redis_url(&config); + assert_eq!(url, "rediss://user:pass@example.com:6380"); + assert_eq!(redacted_url, "rediss://user:pass@example.com:6380"); + } + + #[rstest] + fn test_get_redis_url_full_config_without_ssl() { + let config = json!({ + "host": "example.com", + "port": 6380, + "username": "username", + "password": "password", + "ssl": false, + }); + let (url, redacted_url) = get_redis_url(&config); + assert_eq!(url, "redis://username:password@example.com:6380"); + assert_eq!(redacted_url, "redis://username:pa...rd@example.com:6380"); + } + + #[rstest] + fn test_get_redis_url_missing_username_and_password() { + let config = json!({ + "host": "example.com", + "port": 6380, + "ssl": false, + }); + let (url, redacted_url) = get_redis_url(&config); + assert_eq!(url, "redis://example.com:6380"); + assert_eq!(redacted_url, "redis://example.com:6380"); + } + + #[rstest] + fn test_get_redis_url_ssl_default_false() { + let config = json!({ + "host": "example.com", + "port": 6380, + "username": "username", + "password": "password", + // "ssl" is intentionally omitted to test default behavior + }); + let (url, redacted_url) = get_redis_url(&config); + assert_eq!(url, "redis://username:password@example.com:6380"); + assert_eq!(redacted_url, "redis://username:pa...rd@example.com:6380"); + } + + #[rstest] + fn test_get_timeout_duration_default() { + let database_config = json!({}); + + let timeout_duration = get_timeout_duration(&database_config, 20); + assert_eq!(timeout_duration, Duration::from_secs(20)); + } + + #[rstest] + fn test_get_timeout_duration() { + let mut database_config = HashMap::new(); + database_config.insert("timeout".to_string(), json!(2)); + + let timeout_duration = get_timeout_duration(&json!(database_config), 20); + assert_eq!(timeout_duration, Duration::from_secs(2)); + } + + #[rstest] + fn test_get_buffer_interval_default() { + let config = HashMap::new(); + + let buffer_interval = get_buffer_interval(&config); + assert_eq!(buffer_interval, Duration::from_millis(0)); + } + + #[rstest] + fn test_get_buffer_interval() { + let mut config = HashMap::new(); + config.insert("buffer_interval_ms".to_string(), json!(100)); + + let buffer_interval = get_buffer_interval(&config); + assert_eq!(buffer_interval, Duration::from_millis(100)); + } + #[rstest] fn test_get_stream_name_with_trader_prefix_and_instance_id() { let trader_id = TraderId::from("tester-123"); @@ -243,20 +393,4 @@ mod tests { let key = get_stream_name(trader_id, instance_id, &config); assert_eq!(key, format!("streams:")); } - - #[rstest] - fn test_get_buffer_interval_default() { - let config = HashMap::new(); - let buffer_interval = get_buffer_interval(&config); - assert_eq!(buffer_interval, Duration::from_millis(0)); - } - - #[rstest] - fn test_get_buffer_interval() { - let mut config = HashMap::new(); - config.insert("buffer_interval_ms".to_string(), json!(100)); - - let buffer_interval = get_buffer_interval(&config); - assert_eq!(buffer_interval, Duration::from_millis(100)); - } } diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index 598d23d1c877..ea9dc9207568 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -140,7 +140,7 @@ impl TestTimer { start_time_ns: UnixNanos, stop_time_ns: Option, ) -> Self { - check_valid_string(name, "`TestTimer` name").unwrap(); + check_valid_string(name, stringify!(name)).unwrap(); Self { name: Ustr::from(name), @@ -232,7 +232,7 @@ impl LiveTimer { stop_time_ns: Option, callback: EventHandler, ) -> Self { - check_valid_string(name, "`TestTimer` name").unwrap(); + check_valid_string(name, stringify!(name)).unwrap(); Self { name: Ustr::from(name), diff --git a/nautilus_core/core/Cargo.toml b/nautilus_core/core/Cargo.toml index ffc67a0da24c..60fbf23c0641 100644 --- a/nautilus_core/core/Cargo.toml +++ b/nautilus_core/core/Cargo.toml @@ -19,7 +19,7 @@ serde = { workspace = true } serde_json = { workspace = true } ustr = { workspace = true } uuid = { workspace = true } -heck = "0.4.1" +heck = "0.5.0" [dev-dependencies] criterion = { workspace = true } @@ -30,8 +30,7 @@ rstest = { workspace = true } cbindgen = { workspace = true, optional = true } [features] +default = [] extension-module = ["pyo3/extension-module"] ffi = ["cbindgen"] python = ["pyo3"] -default = [] - diff --git a/nautilus_core/core/src/correctness.rs b/nautilus_core/core/src/correctness.rs index 4a33565efeb9..7b19ee326cda 100644 --- a/nautilus_core/core/src/correctness.rs +++ b/nautilus_core/core/src/correctness.rs @@ -13,87 +13,130 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use anyhow::{bail, Result}; - const FAILED: &str = "Condition failed:"; -/// Validates the content of a string `s`. +/// Validates the string `s` contains only ASCII characters and has semantic meaning. /// -/// # Panics +/// # Errors /// /// - If `s` is an empty string. /// - If `s` consists solely of whitespace characters. /// - If `s` contains one or more non-ASCII characters. -pub fn check_valid_string(s: &str, desc: &str) -> Result<()> { +pub fn check_valid_string(s: &str, param: &str) -> anyhow::Result<()> { if s.is_empty() { - bail!("{FAILED} invalid string for {desc}, was empty") + anyhow::bail!("{FAILED} invalid string for '{param}', was empty") } else if s.chars().all(char::is_whitespace) { - bail!("{FAILED} invalid string for {desc}, was all whitespace",) + anyhow::bail!("{FAILED} invalid string for '{param}', was all whitespace",) } else if !s.is_ascii() { - bail!("{FAILED} invalid string for {desc} contained a non-ASCII char, was '{s}'",) + anyhow::bail!("{FAILED} invalid string for '{param}' contained a non-ASCII char, was '{s}'",) } else { Ok(()) } } -/// Validates that the string `s` contains the pattern `pat`. -pub fn check_string_contains(s: &str, pat: &str, desc: &str) -> Result<()> { +/// Validates the string `s` if Some, contains only ASCII characters and has semantic meaning. +/// +/// # Errors +/// +/// - If `s` is an empty string. +/// - If `s` consists solely of whitespace characters. +/// - If `s` contains one or more non-ASCII characters. +pub fn check_valid_string_optional(s: Option<&str>, param: &str) -> anyhow::Result<()> { + if let Some(s) = s { + check_valid_string(s, param)?; + } + Ok(()) +} + +/// Validates the string `s` contains the pattern `pat`. +pub fn check_string_contains(s: &str, pat: &str, param: &str) -> anyhow::Result<()> { if !s.contains(pat) { - bail!("{FAILED} invalid string for {desc} did not contain '{pat}', was '{s}'") + anyhow::bail!("{FAILED} invalid string for '{param}' did not contain '{pat}', was '{s}'") } Ok(()) } -/// Validates that `u8` values are equal. -pub fn check_u8_equal(lhs: u8, rhs: u8, lhs_param: &str, rhs_param: &str) -> Result<()> { +/// Validates the `u8` values are equal. +pub fn check_equal_u8(lhs: u8, rhs: u8, lhs_param: &str, rhs_param: &str) -> anyhow::Result<()> { if lhs != rhs { - bail!("{FAILED} '{lhs_param}' u8 of {lhs} was not equal to '{rhs_param}' u8 of {rhs}") + anyhow::bail!( + "{FAILED} '{lhs_param}' u8 of {lhs} was not equal to '{rhs_param}' u8 of {rhs}" + ) + } + Ok(()) +} + +/// Validates the `u64` value is positive (> 0). +pub fn check_positive_u64(value: u64, param: &str) -> anyhow::Result<()> { + if value == 0 { + anyhow::bail!("{FAILED} invalid u64 for '{param}' not positive, was {value}") + } + Ok(()) +} + +/// Validates the `i64` value is positive (> 0). +pub fn check_positive_i64(value: i64, param: &str) -> anyhow::Result<()> { + if value <= 0 { + anyhow::bail!("{FAILED} invalid i64 for '{param}' not positive, was {value}") + } + Ok(()) +} + +/// Validates the `f64` value is non-negative (< 0). +pub fn check_non_negative_f64(value: f64, param: &str) -> anyhow::Result<()> { + if value.is_nan() || value.is_infinite() { + anyhow::bail!("{FAILED} invalid f64 for '{param}', was {value}") + } + if value < 0.0 { + anyhow::bail!("{FAILED} invalid f64 for '{param}' negative, was {value}") } Ok(()) } -/// Validates that the `u8` value is in the inclusive range [`l`, `r`]. -pub fn check_u8_in_range_inclusive(value: u8, l: u8, r: u8, desc: &str) -> Result<()> { +/// Validates the `u8` value is in range [`l`, `r`] (inclusive). +pub fn check_in_range_inclusive_u8(value: u8, l: u8, r: u8, param: &str) -> anyhow::Result<()> { if value < l || value > r { - bail!("{FAILED} invalid u8 for {desc} not in range [{l}, {r}], was {value}") + anyhow::bail!("{FAILED} invalid u8 for '{param}' not in range [{l}, {r}], was {value}") } Ok(()) } -/// Validates that the `u64` value is in the inclusive range [`l`, `r`]. -pub fn check_u64_in_range_inclusive(value: u64, l: u64, r: u64, desc: &str) -> Result<()> { +/// Validates the `u64` value is range [`l`, `r`] (inclusive). +pub fn check_in_range_inclusive_u64(value: u64, l: u64, r: u64, param: &str) -> anyhow::Result<()> { if value < l || value > r { - bail!("{FAILED} invalid u64 for {desc} not in range [{l}, {r}], was {value}") + anyhow::bail!("{FAILED} invalid u64 for '{param}' not in range [{l}, {r}], was {value}") } Ok(()) } -/// Validates that the `i64` value is in the inclusive range [`l`, `r`]. -pub fn check_i64_in_range_inclusive(value: i64, l: i64, r: i64, desc: &str) -> Result<()> { +/// Validates the `i64` value is in range [`l`, `r`] (inclusive). +pub fn check_in_range_inclusive_i64(value: i64, l: i64, r: i64, param: &str) -> anyhow::Result<()> { if value < l || value > r { - bail!("{FAILED} invalid i64 for {desc} not in range [{l}, {r}], was {value}") + anyhow::bail!("{FAILED} invalid i64 for '{param}' not in range [{l}, {r}], was {value}") } Ok(()) } -/// Validates that the `f64` value is in the inclusive range [`l`, `r`]. -pub fn check_f64_in_range_inclusive(value: f64, l: f64, r: f64, desc: &str) -> Result<()> { +/// Validates the `f64` value is in range [`l`, `r`] (inclusive). +pub fn check_in_range_inclusive_f64(value: f64, l: f64, r: f64, param: &str) -> anyhow::Result<()> { if value.is_nan() || value.is_infinite() { - bail!("{FAILED} invalid f64 for {desc}, was {value}") + anyhow::bail!("{FAILED} invalid f64 for '{param}', was {value}") } if value < l || value > r { - bail!("{FAILED} invalid f64 for {desc} not in range [{l}, {r}], was {value}") + anyhow::bail!("{FAILED} invalid f64 for '{param}' not in range [{l}, {r}], was {value}") } Ok(()) } -/// Validates that the `f64` value is non-negative. -pub fn check_f64_non_negative(value: f64, desc: &str) -> Result<()> { - if value.is_nan() || value.is_infinite() { - bail!("{FAILED} invalid f64 for {desc}, was {value}") - } - if value < 0.0 { - bail!("{FAILED} invalid f64 for {desc} negative, was {value}") +/// Validates the `usize` value is in range [`l`, `r`] (inclusive). +pub fn check_in_range_inclusive_usize( + value: usize, + l: usize, + r: usize, + param: &str, +) -> anyhow::Result<()> { + if value < l || value > r { + anyhow::bail!("{FAILED} invalid usize for '{param}' not in range [{l}, {r}], was {value}") } Ok(()) } @@ -113,7 +156,7 @@ mod tests { #[case("a a")] #[case(" a ")] #[case("abc")] - fn test_valid_string_with_valid_value(#[case] s: &str) { + fn test_check_valid_string_with_valid_value(#[case] s: &str) { assert!(check_valid_string(s, "value").is_ok()); } @@ -122,133 +165,195 @@ mod tests { #[case(" ")] // <-- whitespace-only #[case(" ")] // <-- whitespace-only string #[case("🦀")] // <-- contains non-ASCII char - fn test_valid_string_with_invalid_values(#[case] s: &str) { + fn test_check_valid_string_with_invalid_values(#[case] s: &str) { assert!(check_valid_string(s, "value").is_err()); } + #[rstest] + #[case(None)] + #[case(Some(" a"))] + #[case(Some("a "))] + #[case(Some("a a"))] + #[case(Some(" a "))] + #[case(Some("abc"))] + fn test_check_valid_string_optional_with_valid_value(#[case] s: Option<&str>) { + assert!(check_valid_string_optional(s, "value").is_ok()); + } + #[rstest] #[case("a", "a")] - fn test_string_contains_when_it_does_contain(#[case] s: &str, #[case] pat: &str) { + fn test_check_string_contains_when_does_contain(#[case] s: &str, #[case] pat: &str) { assert!(check_string_contains(s, pat, "value").is_ok()); } #[rstest] #[case("a", "b")] - fn test_string_contains_with_invalid_values(#[case] s: &str, #[case] pat: &str) { + fn test_check_string_contains_when_does_not_contain(#[case] s: &str, #[case] pat: &str) { assert!(check_string_contains(s, pat, "value").is_err()); } #[rstest] - #[case(0, 0, 0, "value")] - #[case(0, 0, 1, "value")] - #[case(1, 0, 1, "value")] - fn test_u8_in_range_inclusive_when_valid_values( - #[case] value: u8, - #[case] l: u8, - #[case] r: u8, - #[case] desc: &str, - ) { - assert!(check_u8_in_range_inclusive(value, l, r, desc).is_ok()); - } - - #[rstest] - #[case(0, 1, "left param", "right param")] - #[case(1, 0, "left param", "right param")] - fn test_u8_equal_when_invalid_values( + #[case(0, 0, "left", "right")] + #[case(1, 1, "left", "right")] + fn test_check_equal_u8_when_equal( #[case] lhs: u8, #[case] rhs: u8, #[case] lhs_param: &str, #[case] rhs_param: &str, ) { - assert!(check_u8_equal(lhs, rhs, lhs_param, rhs_param).is_err()); + assert!(check_equal_u8(lhs, rhs, lhs_param, rhs_param).is_ok()); } #[rstest] - #[case(0, 0, "left param", "right param")] - fn test_u8_equal_when_valid_values( + #[case(0, 1, "left", "right")] + #[case(1, 0, "left", "right")] + fn test_check_equal_u8_when_not_equal( #[case] lhs: u8, #[case] rhs: u8, #[case] lhs_param: &str, #[case] rhs_param: &str, ) { - assert!(check_u8_equal(lhs, rhs, lhs_param, rhs_param).is_ok()); + assert!(check_equal_u8(lhs, rhs, lhs_param, rhs_param).is_err()); + } + + #[rstest] + #[case(1, "value")] + fn test_check_positive_u64_when_positive(#[case] value: u64, #[case] param: &str) { + assert!(check_positive_u64(value, param).is_ok()); + } + + #[rstest] + #[case(0, "value")] + fn test_check_positive_u64_when_not_positive(#[case] value: u64, #[case] param: &str) { + assert!(check_positive_u64(value, param).is_err()); + } + + #[rstest] + #[case(1, "value")] + fn test_check_positive_i64_when_positive(#[case] value: i64, #[case] param: &str) { + assert!(check_positive_i64(value, param).is_ok()); + } + + #[rstest] + #[case(0, "value")] + #[case(-1, "value")] + fn test_check_positive_i64_when_not_positive(#[case] value: i64, #[case] param: &str) { + assert!(check_positive_i64(value, param).is_err()); + } + + #[rstest] + #[case(0.0, "value")] + #[case(1.0, "value")] + fn test_check_non_negative_f64_when_not_negative(#[case] value: f64, #[case] param: &str) { + assert!(check_non_negative_f64(value, param).is_ok()); + } + + #[rstest] + #[case(f64::NAN, "value")] + #[case(f64::INFINITY, "value")] + #[case(f64::NEG_INFINITY, "value")] + #[case(-0.1, "value")] + fn test_check_non_negative_f64_when_negative(#[case] value: f64, #[case] param: &str) { + assert!(check_non_negative_f64(value, param).is_err()); + } + + #[rstest] + #[case(0, 0, 0, "value")] + #[case(0, 0, 1, "value")] + #[case(1, 0, 1, "value")] + fn test_check_in_range_inclusive_u8_when_in_range( + #[case] value: u8, + #[case] l: u8, + #[case] r: u8, + #[case] desc: &str, + ) { + assert!(check_in_range_inclusive_u8(value, l, r, desc).is_ok()); } #[rstest] #[case(0, 1, 2, "value")] #[case(3, 1, 2, "value")] - fn test_u8_in_range_inclusive_when_invalid_values( + fn test_check_in_range_inclusive_u8_when_out_of_range( #[case] value: u8, #[case] l: u8, #[case] r: u8, - #[case] desc: &str, + #[case] param: &str, ) { - assert!(check_u8_in_range_inclusive(value, l, r, desc).is_err()); + assert!(check_in_range_inclusive_u8(value, l, r, param).is_err()); } #[rstest] #[case(0, 0, 0, "value")] #[case(0, 0, 1, "value")] #[case(1, 0, 1, "value")] - fn test_u64_in_range_inclusive_when_valid_values( + fn test_check_in_range_inclusive_u64_when_in_range( #[case] value: u64, #[case] l: u64, #[case] r: u64, - #[case] desc: &str, + #[case] param: &str, ) { - assert!(check_u64_in_range_inclusive(value, l, r, desc).is_ok()); + assert!(check_in_range_inclusive_u64(value, l, r, param).is_ok()); } #[rstest] #[case(0, 1, 2, "value")] #[case(3, 1, 2, "value")] - fn test_u64_in_range_inclusive_when_invalid_values( + fn test_check_in_range_inclusive_u64_when_out_of_range( #[case] value: u64, #[case] l: u64, #[case] r: u64, - #[case] desc: &str, + #[case] param: &str, ) { - assert!(check_u64_in_range_inclusive(value, l, r, desc).is_err()); + assert!(check_in_range_inclusive_u64(value, l, r, param).is_err()); } #[rstest] #[case(0, 0, 0, "value")] #[case(0, 0, 1, "value")] #[case(1, 0, 1, "value")] - fn test_i64_in_range_inclusive_when_valid_values( + fn test_check_in_range_inclusive_i64_when_in_range( #[case] value: i64, #[case] l: i64, #[case] r: i64, - #[case] desc: &str, + #[case] param: &str, ) { - assert!(check_i64_in_range_inclusive(value, l, r, desc).is_ok()); + assert!(check_in_range_inclusive_i64(value, l, r, param).is_ok()); } #[rstest] #[case(0, 1, 2, "value")] #[case(3, 1, 2, "value")] - fn test_i64_in_range_inclusive_when_invalid_values( + fn test_check_in_range_inclusive_i64_when_out_of_range( #[case] value: i64, #[case] l: i64, #[case] r: i64, - #[case] desc: &str, + #[case] param: &str, ) { - assert!(check_i64_in_range_inclusive(value, l, r, desc).is_err()); + assert!(check_in_range_inclusive_i64(value, l, r, param).is_err()); } #[rstest] - #[case(0.0, "value")] - #[case(1.0, "value")] - fn test_f64_non_negative_when_valid_values(#[case] value: f64, #[case] desc: &str) { - assert!(check_f64_non_negative(value, desc).is_ok()); + #[case(0, 0, 0, "value")] + #[case(0, 0, 1, "value")] + #[case(1, 0, 1, "value")] + fn test_check_in_range_inclusive_usize_when_in_range( + #[case] value: usize, + #[case] l: usize, + #[case] r: usize, + #[case] param: &str, + ) { + assert!(check_in_range_inclusive_usize(value, l, r, param).is_ok()); } #[rstest] - #[case(f64::NAN, "value")] - #[case(f64::INFINITY, "value")] - #[case(f64::NEG_INFINITY, "value")] - #[case(-0.1, "value")] - fn test_f64_non_negative_when_invalid_values(#[case] value: f64, #[case] desc: &str) { - assert!(check_f64_non_negative(value, desc).is_err()); + #[case(0, 1, 2, "value")] + #[case(3, 1, 2, "value")] + fn test_check_in_range_inclusive_usize_when_out_of_range( + #[case] value: usize, + #[case] l: usize, + #[case] r: usize, + #[case] param: &str, + ) { + assert!(check_in_range_inclusive_usize(value, l, r, param).is_err()); } } diff --git a/nautilus_core/core/src/datetime.rs b/nautilus_core/core/src/datetime.rs index 7aae4af1db79..6995ecf1461c 100644 --- a/nautilus_core/core/src/datetime.rs +++ b/nautilus_core/core/src/datetime.rs @@ -15,7 +15,6 @@ use std::time::{Duration, UNIX_EPOCH}; -use anyhow::{anyhow, Result}; use chrono::{ prelude::{DateTime, Utc}, Datelike, NaiveDate, SecondsFormat, TimeDelta, Weekday, @@ -93,8 +92,9 @@ pub fn unix_nanos_to_iso8601(timestamp_ns: u64) -> String { dt.to_rfc3339_opts(SecondsFormat::Nanos, true) } -pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> Result { - let date = NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| anyhow!("Invalid date"))?; +pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> anyhow::Result { + let date = + NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| anyhow::anyhow!("Invalid date"))?; let current_weekday = date.weekday().number_from_monday(); // Calculate the offset in days for closest weekday (Mon-Fri) @@ -110,19 +110,19 @@ pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> Result // Convert to UNIX nanoseconds let unix_timestamp_ns = last_closest .and_hms_nano_opt(0, 0, 0, 0) - .ok_or_else(|| anyhow!("Failed `and_hms_nano_opt`"))?; + .ok_or_else(|| anyhow::anyhow!("Failed `and_hms_nano_opt`"))?; Ok(unix_timestamp_ns .and_utc() .timestamp_nanos_opt() - .ok_or_else(|| anyhow!("Failed `timestamp_nanos_opt`"))? as UnixNanos) + .ok_or_else(|| anyhow::anyhow!("Failed `timestamp_nanos_opt`"))? as UnixNanos) } -pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> Result { +pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> anyhow::Result { let seconds = timestamp_ns / NANOSECONDS_IN_SECOND; let nanoseconds = (timestamp_ns % NANOSECONDS_IN_SECOND) as u32; let timestamp = DateTime::from_timestamp(seconds as i64, nanoseconds) - .ok_or_else(|| anyhow!("Invalid timestamp {timestamp_ns}"))?; + .ok_or_else(|| anyhow::anyhow!("Invalid timestamp {timestamp_ns}"))?; let now = Utc::now(); Ok(now.signed_duration_since(timestamp) <= TimeDelta::try_days(1).unwrap()) diff --git a/nautilus_core/core/src/parsing.rs b/nautilus_core/core/src/parsing.rs index d571c75a2e78..1910d7d91a51 100644 --- a/nautilus_core/core/src/parsing.rs +++ b/nautilus_core/core/src/parsing.rs @@ -13,8 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use anyhow::{anyhow, Result}; - /// Returns the decimal precision inferred from the given string. #[must_use] pub fn precision_from_str(s: &str) -> u8 { @@ -30,7 +28,7 @@ pub fn precision_from_str(s: &str) -> u8 { } /// Returns a usize from the given bytes. -pub fn bytes_to_usize(bytes: &[u8]) -> Result { +pub fn bytes_to_usize(bytes: &[u8]) -> anyhow::Result { // Check bytes width if bytes.len() >= std::mem::size_of::() { let mut buffer = [0u8; std::mem::size_of::()]; @@ -38,7 +36,7 @@ pub fn bytes_to_usize(bytes: &[u8]) -> Result { Ok(usize::from_le_bytes(buffer)) } else { - Err(anyhow!("Not enough bytes to represent a `usize`")) + Err(anyhow::anyhow!("Not enough bytes to represent a `usize`")) } } diff --git a/nautilus_core/core/src/uuid.rs b/nautilus_core/core/src/uuid.rs index 6c27aa312804..324e49918f02 100644 --- a/nautilus_core/core/src/uuid.rs +++ b/nautilus_core/core/src/uuid.rs @@ -23,6 +23,9 @@ use std::{ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use uuid::Uuid; +/// The maximum length of ASCII characters for a `UUID4` string value (includes null terminator). +const UUID4_LEN: usize = 37; + /// Represents a pseudo-random UUID (universally unique identifier) /// version 4 based on a 128-bit label as specified in RFC 4122. #[repr(C)] @@ -32,8 +35,8 @@ use uuid::Uuid; pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.core") )] pub struct UUID4 { - /// The UUID v4 C string value as a fixed-length byte array. - pub(crate) value: [u8; 37], + /// The UUID v4 value as a fixed-length C string byte array (includes null terminator). + pub(crate) value: [u8; 37], // cbindgen issue using the constant in the array } impl UUID4 { @@ -42,7 +45,7 @@ impl UUID4 { let uuid = Uuid::new_v4(); let c_string = CString::new(uuid.to_string()).expect("`CString` conversion failed"); let bytes = c_string.as_bytes_with_nul(); - let mut value = [0; 37]; + let mut value = [0; UUID4_LEN]; value[..bytes.len()].copy_from_slice(bytes); Self { value } @@ -50,7 +53,7 @@ impl UUID4 { #[must_use] pub fn to_cstr(&self) -> &CStr { - // SAFETY: Unwrap safe as we always store valid C strings + // SAFETY: We always store valid C strings CStr::from_bytes_with_nul(&self.value).unwrap() } } @@ -62,7 +65,7 @@ impl FromStr for UUID4 { let uuid = Uuid::parse_str(s).map_err(|_| "Invalid UUID string")?; let c_string = CString::new(uuid.to_string()).expect("`CString` conversion failed"); let bytes = c_string.as_bytes_with_nul(); - let mut value = [0; 37]; + let mut value = [0; UUID4_LEN]; value[..bytes.len()].copy_from_slice(bytes); Ok(Self { value }) diff --git a/nautilus_core/execution/Cargo.toml b/nautilus_core/execution/Cargo.toml index 94333a9ac259..e7b55093f758 100644 --- a/nautilus_core/execution/Cargo.toml +++ b/nautilus_core/execution/Cargo.toml @@ -33,18 +33,22 @@ criterion = { workspace = true } rstest = { workspace = true } [features] +default = ["ffi", "python"] extension-module = [ "pyo3/extension-module", "nautilus-common/extension-module", "nautilus-core/extension-module", "nautilus-model/extension-module", ] -ffi = ["nautilus-core/ffi", "nautilus-model/ffi", "nautilus-common/ffi"] +ffi = [ + "nautilus-common/ffi", + "nautilus-core/ffi", + "nautilus-model/ffi", +] python = [ "pyo3", "pyo3-asyncio", + "nautilus-common/python", "nautilus-core/python", "nautilus-model/python", - "nautilus-common/python", ] -default = ["ffi", "python"] diff --git a/nautilus_core/execution/src/matching_core.rs b/nautilus_core/execution/src/matching_core.rs index eb2de41a1c5f..630366b73ec9 100644 --- a/nautilus_core/execution/src/matching_core.rs +++ b/nautilus_core/execution/src/matching_core.rs @@ -41,9 +41,9 @@ pub struct OrderMatchingCore { pub last: Option, orders_bid: Vec, orders_ask: Vec, - trigger_stop_order: Option, - fill_market_order: Option, - fill_limit_order: Option, + trigger_stop_order: Option, + fill_market_order: Option, + fill_limit_order: Option, } impl OrderMatchingCore { @@ -51,9 +51,9 @@ impl OrderMatchingCore { pub fn new( instrument_id: InstrumentId, price_increment: Price, - trigger_stop_order: Option, - fill_market_order: Option, - fill_limit_order: Option, + trigger_stop_order: Option, + fill_market_order: Option, + fill_limit_order: Option, ) -> Self { Self { instrument_id, @@ -162,13 +162,17 @@ impl OrderMatchingCore { pub fn match_limit_order(&self, order: &LimitOrderType) { if self.is_limit_matched(order) { - // self.fill_limit_order.call(o) + if let Some(func) = self.fill_limit_order { + func(order.clone()); // TODO: Remove this clone (will need a lifetime) + } } } pub fn match_stop_order(&self, order: &StopOrderType) { if self.is_stop_matched(order) { - // self.fill_stop_order.call(o) + if let Some(func) = self.trigger_stop_order { + func(order.clone()); // TODO: Remove this clone (will need a lifetime) + } } } @@ -194,6 +198,8 @@ impl OrderMatchingCore { //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod tests { + use std::sync::Mutex; + use nautilus_model::{ enums::OrderSide, orders::stubs::TestOrderStubs, types::quantity::Quantity, }; @@ -201,6 +207,9 @@ mod tests { use super::*; + static TRIGGERED_STOPS: Mutex> = Mutex::new(Vec::new()); + static FILLED_LIMITS: Mutex> = Mutex::new(Vec::new()); + fn create_matching_core( instrument_id: InstrumentId, price_increment: Price, @@ -208,13 +217,132 @@ mod tests { OrderMatchingCore::new(instrument_id, price_increment, None, None, None) } + #[rstest] + fn test_add_order_bid_side() { + let instrument_id = InstrumentId::from("AAPL.XNAS"); + let mut matching_core = create_matching_core(instrument_id, Price::from("0.01")); + + let order = TestOrderStubs::limit_order( + instrument_id, + OrderSide::Buy, + Price::from("100.00"), + Quantity::from("100"), + None, + None, + ); + + let passive_order = PassiveOrderType::Limit(LimitOrderType::Limit(order)); + matching_core.add_order(passive_order.clone()).unwrap(); + + assert!(matching_core.get_orders_bid().contains(&passive_order)); + assert!(!matching_core.get_orders_ask().contains(&passive_order)); + assert_eq!(matching_core.get_orders_bid().len(), 1); + assert!(matching_core.get_orders_ask().is_empty()); + } + + #[rstest] + fn test_add_order_ask_side() { + let instrument_id = InstrumentId::from("AAPL.XNAS"); + let mut matching_core = create_matching_core(instrument_id, Price::from("0.01")); + + let order = TestOrderStubs::limit_order( + instrument_id, + OrderSide::Sell, + Price::from("100.00"), + Quantity::from("100"), + None, + None, + ); + + let passive_order = PassiveOrderType::Limit(LimitOrderType::Limit(order)); + matching_core.add_order(passive_order.clone()).unwrap(); + + assert!(matching_core.get_orders_ask().contains(&passive_order)); + assert!(!matching_core.get_orders_bid().contains(&passive_order)); + assert_eq!(matching_core.get_orders_ask().len(), 1); + assert!(matching_core.get_orders_bid().is_empty()); + } + + #[rstest] + fn test_reset() { + let instrument_id = InstrumentId::from("AAPL.XNAS"); + let mut matching_core = create_matching_core(instrument_id, Price::from("0.01")); + + let order = TestOrderStubs::limit_order( + instrument_id, + OrderSide::Sell, + Price::from("100.00"), + Quantity::from("100"), + None, + None, + ); + + let passive_order = PassiveOrderType::Limit(LimitOrderType::Limit(order)); + matching_core.add_order(passive_order).unwrap(); + matching_core.bid = Some(Price::from("100.00")); + matching_core.ask = Some(Price::from("100.00")); + matching_core.last = Some(Price::from("100.00")); + + matching_core.reset(); + + assert!(matching_core.bid.is_none()); + assert!(matching_core.ask.is_none()); + assert!(matching_core.last.is_none()); + assert!(matching_core.get_orders_bid().is_empty()); + assert!(matching_core.get_orders_ask().is_empty()); + } + + #[rstest] + fn test_delete_order_when_not_exists() { + let instrument_id = InstrumentId::from("AAPL.XNAS"); + let mut matching_core = create_matching_core(instrument_id, Price::from("0.01")); + + let order = TestOrderStubs::limit_order( + instrument_id, + OrderSide::Buy, + Price::from("100.00"), + Quantity::from("100"), + None, + None, + ); + + let passive_order = PassiveOrderType::Limit(LimitOrderType::Limit(order)); + let result = matching_core.delete_order(&passive_order); + + assert!(result.is_err()); + } + + #[rstest] + #[case(OrderSide::Buy)] + #[case(OrderSide::Sell)] + fn test_delete_order_when_exists(#[case] order_side: OrderSide) { + let instrument_id = InstrumentId::from("AAPL.XNAS"); + let mut matching_core = create_matching_core(instrument_id, Price::from("0.01")); + + let order = TestOrderStubs::limit_order( + instrument_id, + order_side, + Price::from("100.00"), + Quantity::from("100"), + None, + None, + ); + + let passive_order = PassiveOrderType::Limit(LimitOrderType::Limit(order)); + matching_core.add_order(passive_order.clone()).unwrap(); + matching_core.delete_order(&passive_order).unwrap(); + + assert!(matching_core.get_orders_ask().is_empty()); + assert!(matching_core.get_orders_bid().is_empty()); + } + #[rstest] #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)] #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)] #[case( Some(Price::from("100.00")), Some(Price::from("101.00")), - Price::from("100.00"), + Price::from("100.00"), // <-- Price below ask OrderSide::Buy, false )] @@ -228,14 +356,14 @@ mod tests { #[case( Some(Price::from("100.00")), Some(Price::from("101.00")), - Price::from("102.00"), // <-- Price higher than ask (marketable) + Price::from("102.00"), // <-- Price above ask (marketable) OrderSide::Buy, true )] #[case( Some(Price::from("100.00")), Some(Price::from("101.00")), - Price::from("101.00"), + Price::from("101.00"), // <-- Price above bid OrderSide::Sell, false )] @@ -278,4 +406,154 @@ mod tests { assert_eq!(result, expected); } + + #[rstest] + #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)] + #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)] + #[case( + Some(Price::from("100.00")), + Some(Price::from("101.00")), + Price::from("102.00"), // <-- Trigger above ask + OrderSide::Buy, + false + )] + #[case( + Some(Price::from("100.00")), + Some(Price::from("101.00")), + Price::from("101.00"), // <-- Trigger at ask + OrderSide::Buy, + true + )] + #[case( + Some(Price::from("100.00")), + Some(Price::from("101.00")), + Price::from("100.00"), // <-- Trigger below ask + OrderSide::Buy, + true + )] + #[case( + Some(Price::from("100.00")), + Some(Price::from("101.00")), + Price::from("99.00"), // Trigger below bid + OrderSide::Sell, + false + )] + #[case( + Some(Price::from("100.00")), + Some(Price::from("101.00")), + Price::from("100.00"), // <-- Trigger at bid + OrderSide::Sell, + true + )] + #[case( + Some(Price::from("100.00")), + Some(Price::from("101.00")), + Price::from("101.00"), // <-- Trigger above bid + OrderSide::Sell, + true + )] + fn test_is_stop_matched( + #[case] bid: Option, + #[case] ask: Option, + #[case] trigger_price: Price, + #[case] order_side: OrderSide, + #[case] expected: bool, + ) { + let instrument_id = InstrumentId::from("AAPL.XNAS"); + let mut matching_core = create_matching_core(instrument_id, Price::from("0.01")); + matching_core.bid = bid; + matching_core.ask = ask; + + let order = TestOrderStubs::stop_market_order( + instrument_id, + order_side, + trigger_price, + Quantity::from("100"), + None, + None, + None, + ); + + let result = matching_core.is_stop_matched(&StopOrderType::StopMarket(order)); + + assert_eq!(result, expected); + } + + #[rstest] + #[case(OrderSide::Buy)] + #[case(OrderSide::Sell)] + fn test_match_stop_order_when_triggered(#[case] order_side: OrderSide) { + let instrument_id = InstrumentId::from("AAPL.XNAS"); + let trigger_price = Price::from("100.00"); + + fn trigger_stop_order_handler(order: StopOrderType) { + let order = order; + TRIGGERED_STOPS.lock().unwrap().push(order); + } + + let mut matching_core = OrderMatchingCore::new( + instrument_id, + Price::from("0.01"), + Some(trigger_stop_order_handler), + None, + None, + ); + + matching_core.bid = Some(Price::from("100.00")); + matching_core.ask = Some(Price::from("100.00")); + + let order = TestOrderStubs::stop_market_order( + instrument_id, + order_side, + trigger_price, + Quantity::from("100"), + None, + None, + None, + ); + + matching_core.match_stop_order(&StopOrderType::StopMarket(order.clone())); + + let triggered_stops = TRIGGERED_STOPS.lock().unwrap(); + assert_eq!(triggered_stops.len(), 1); + assert_eq!(triggered_stops[0], StopOrderType::StopMarket(order)); + } + + #[rstest] + #[case(OrderSide::Buy)] + #[case(OrderSide::Sell)] + fn test_match_limit_order_when_triggered(#[case] order_side: OrderSide) { + let instrument_id = InstrumentId::from("AAPL.XNAS"); + let price = Price::from("100.00"); + + fn fill_limit_order_handler(order: LimitOrderType) { + FILLED_LIMITS.lock().unwrap().push(order); + } + + let mut matching_core = OrderMatchingCore::new( + instrument_id, + Price::from("0.01"), + None, + None, + Some(fill_limit_order_handler), + ); + + matching_core.bid = Some(Price::from("100.00")); + matching_core.ask = Some(Price::from("100.00")); + + let order = TestOrderStubs::limit_order( + instrument_id, + order_side, + price, + Quantity::from("100.00"), + None, + None, + ); + + matching_core.match_limit_order(&LimitOrderType::Limit(order.clone())); + + let filled_limits = FILLED_LIMITS.lock().unwrap(); + assert_eq!(filled_limits.len(), 1); + assert_eq!(filled_limits[0], LimitOrderType::Limit(order)); + } } diff --git a/nautilus_core/indicators/Cargo.toml b/nautilus_core/indicators/Cargo.toml index 4c4bf01ef863..2d219672e665 100644 --- a/nautilus_core/indicators/Cargo.toml +++ b/nautilus_core/indicators/Cargo.toml @@ -21,10 +21,14 @@ strum = { workspace = true } rstest = { workspace = true } [features] +default = [] extension-module = [ "pyo3/extension-module", "nautilus-core/extension-module", "nautilus-model/extension-module", ] -python = ["pyo3", "nautilus-core/python", "nautilus-model/python"] -default = [] +python = [ + "pyo3", + "nautilus-core/python", + "nautilus-model/python", +] diff --git a/nautilus_core/indicators/src/average/ama.rs b/nautilus_core/indicators/src/average/ama.rs index cbdd17a705f0..52246dd49491 100644 --- a/nautilus_core/indicators/src/average/ama.rs +++ b/nautilus_core/indicators/src/average/ama.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -110,7 +109,7 @@ impl AdaptiveMovingAverage { period_fast: usize, period_slow: usize, price_type: Option, - ) -> Result { + ) -> anyhow::Result { // Inputs don't require validation, however we return a `Result` // to standardize with other indicators which do need validation. Ok(Self { diff --git a/nautilus_core/indicators/src/average/dema.rs b/nautilus_core/indicators/src/average/dema.rs index 560feba9a1c6..3c465db7f045 100644 --- a/nautilus_core/indicators/src/average/dema.rs +++ b/nautilus_core/indicators/src/average/dema.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -88,7 +87,7 @@ impl Indicator for DoubleExponentialMovingAverage { } impl DoubleExponentialMovingAverage { - pub fn new(period: usize, price_type: Option) -> Result { + pub fn new(period: usize, price_type: Option) -> anyhow::Result { Ok(Self { period, price_type: price_type.unwrap_or(PriceType::Last), diff --git a/nautilus_core/indicators/src/average/ema.rs b/nautilus_core/indicators/src/average/ema.rs index 9bb4ba54f6f6..63829e68036b 100644 --- a/nautilus_core/indicators/src/average/ema.rs +++ b/nautilus_core/indicators/src/average/ema.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -79,7 +78,7 @@ impl Indicator for ExponentialMovingAverage { } impl ExponentialMovingAverage { - pub fn new(period: usize, price_type: Option) -> Result { + pub fn new(period: usize, price_type: Option) -> anyhow::Result { // Inputs don't require validation, however we return a `Result` // to standardize with other indicators which do need validation. Ok(Self { diff --git a/nautilus_core/indicators/src/average/hma.rs b/nautilus_core/indicators/src/average/hma.rs index dab724292dd8..cc025cf6e1ef 100644 --- a/nautilus_core/indicators/src/average/hma.rs +++ b/nautilus_core/indicators/src/average/hma.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -97,7 +96,7 @@ fn _get_weights(size: usize) -> Vec { } impl HullMovingAverage { - pub fn new(period: usize, price_type: Option) -> Result { + pub fn new(period: usize, price_type: Option) -> anyhow::Result { let period_halved = period / 2; let period_sqrt = (period as f64).sqrt() as usize; diff --git a/nautilus_core/indicators/src/average/rma.rs b/nautilus_core/indicators/src/average/rma.rs index 46c8b9cc59e1..fc43d102a68c 100644 --- a/nautilus_core/indicators/src/average/rma.rs +++ b/nautilus_core/indicators/src/average/rma.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -79,7 +78,7 @@ impl Indicator for WilderMovingAverage { } impl WilderMovingAverage { - pub fn new(period: usize, price_type: Option) -> Result { + pub fn new(period: usize, price_type: Option) -> anyhow::Result { // Inputs don't require validation, however we return a `Result` // to standardize with other indicators which do need validation. // The Wilder Moving Average is The Wilder's Moving Average is simply diff --git a/nautilus_core/indicators/src/average/sma.rs b/nautilus_core/indicators/src/average/sma.rs index fcaff18f75c3..705ace65122b 100644 --- a/nautilus_core/indicators/src/average/sma.rs +++ b/nautilus_core/indicators/src/average/sma.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -78,7 +77,7 @@ impl Indicator for SimpleMovingAverage { } impl SimpleMovingAverage { - pub fn new(period: usize, price_type: Option) -> Result { + pub fn new(period: usize, price_type: Option) -> anyhow::Result { // Inputs don't require validation, however we return a `Result` // to standardize with other indicators which do need validation. Ok(Self { diff --git a/nautilus_core/indicators/src/average/vidya.rs b/nautilus_core/indicators/src/average/vidya.rs index 02e0cd339f4b..eb780edb1520 100644 --- a/nautilus_core/indicators/src/average/vidya.rs +++ b/nautilus_core/indicators/src/average/vidya.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -91,7 +90,7 @@ impl VariableIndexDynamicAverage { period: usize, price_type: Option, cmo_ma_type: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { period, price_type: price_type.unwrap_or(PriceType::Last), diff --git a/nautilus_core/indicators/src/average/wma.rs b/nautilus_core/indicators/src/average/wma.rs index aef7763de342..b2bb5ee94531 100644 --- a/nautilus_core/indicators/src/average/wma.rs +++ b/nautilus_core/indicators/src/average/wma.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -53,7 +52,11 @@ impl Display for WeightedMovingAverage { } impl WeightedMovingAverage { - pub fn new(period: usize, weights: Vec, price_type: Option) -> Result { + pub fn new( + period: usize, + weights: Vec, + price_type: Option, + ) -> anyhow::Result { if weights.len() != period { return Err(anyhow::anyhow!("Weights length must be equal to period")); } diff --git a/nautilus_core/indicators/src/book/imbalance.rs b/nautilus_core/indicators/src/book/imbalance.rs index 9f5c85f670dd..46311f0455ee 100644 --- a/nautilus_core/indicators/src/book/imbalance.rs +++ b/nautilus_core/indicators/src/book/imbalance.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use nautilus_model::{ orderbook::{book_mbo::OrderBookMbo, book_mbp::OrderBookMbp}, types::quantity::Quantity, @@ -72,7 +71,7 @@ impl Indicator for BookImbalanceRatio { } impl BookImbalanceRatio { - pub fn new() -> Result { + pub fn new() -> anyhow::Result { // Inputs don't require validation, however we return a `Result` // to standardize with other indicators which do need validation. Ok(Self { diff --git a/nautilus_core/indicators/src/momentum/aroon.rs b/nautilus_core/indicators/src/momentum/aroon.rs index ee3c464d9d44..db0ce27d0779 100644 --- a/nautilus_core/indicators/src/momentum/aroon.rs +++ b/nautilus_core/indicators/src/momentum/aroon.rs @@ -18,7 +18,6 @@ use std::{ fmt::{Debug, Display}, }; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -92,7 +91,7 @@ impl Indicator for AroonOscillator { } impl AroonOscillator { - pub fn new(period: usize) -> Result { + pub fn new(period: usize) -> anyhow::Result { Ok(Self { period, high_inputs: VecDeque::with_capacity(period), diff --git a/nautilus_core/indicators/src/momentum/cmo.rs b/nautilus_core/indicators/src/momentum/cmo.rs index 263ddadc2b2a..d94dc6f57217 100644 --- a/nautilus_core/indicators/src/momentum/cmo.rs +++ b/nautilus_core/indicators/src/momentum/cmo.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use nautilus_model::data::{bar::Bar, quote::QuoteTick, trade::TradeTick}; use crate::{ @@ -82,7 +81,7 @@ impl Indicator for ChandeMomentumOscillator { } impl ChandeMomentumOscillator { - pub fn new(period: usize, ma_type: Option) -> Result { + pub fn new(period: usize, ma_type: Option) -> anyhow::Result { Ok(Self { period, ma_type: ma_type.unwrap_or(MovingAverageType::Wilder), @@ -119,8 +118,13 @@ impl ChandeMomentumOscillator { self.initialized = true; } if self.initialized { - self.value = 100.0 * (self._average_gain.value() - self._average_loss.value()) - / (self._average_gain.value() + self._average_loss.value()); + let divisor = self._average_gain.value() + self._average_loss.value(); + if divisor == 0.0 { + self.value = 0.0; + } else { + self.value = + 100.0 * (self._average_gain.value() - self._average_loss.value()) / divisor; + } } self._previous_close = close; } diff --git a/nautilus_core/indicators/src/momentum/rsi.rs b/nautilus_core/indicators/src/momentum/rsi.rs index 2ad93d9723a8..ea0f2c5ea9b8 100644 --- a/nautilus_core/indicators/src/momentum/rsi.rs +++ b/nautilus_core/indicators/src/momentum/rsi.rs @@ -15,7 +15,6 @@ use std::fmt::{Debug, Display}; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -87,7 +86,7 @@ impl Indicator for RelativeStrengthIndex { } impl RelativeStrengthIndex { - pub fn new(period: usize, ma_type: Option) -> Result { + pub fn new(period: usize, ma_type: Option) -> anyhow::Result { Ok(Self { period, ma_type: ma_type.unwrap_or(MovingAverageType::Exponential), diff --git a/nautilus_core/indicators/src/ratio/efficiency_ratio.rs b/nautilus_core/indicators/src/ratio/efficiency_ratio.rs index 3c994ab55ef9..0cda086051d4 100644 --- a/nautilus_core/indicators/src/ratio/efficiency_ratio.rs +++ b/nautilus_core/indicators/src/ratio/efficiency_ratio.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -80,7 +79,7 @@ impl Indicator for EfficiencyRatio { } impl EfficiencyRatio { - pub fn new(period: usize, price_type: Option) -> Result { + pub fn new(period: usize, price_type: Option) -> anyhow::Result { Ok(Self { period, price_type: price_type.unwrap_or(PriceType::Last), diff --git a/nautilus_core/indicators/src/volatility/atr.rs b/nautilus_core/indicators/src/volatility/atr.rs index 94dfd38a93fd..d41014b7850c 100644 --- a/nautilus_core/indicators/src/volatility/atr.rs +++ b/nautilus_core/indicators/src/volatility/atr.rs @@ -15,7 +15,6 @@ use std::fmt::{Debug, Display}; -use anyhow::Result; use nautilus_model::data::bar::Bar; use crate::{ @@ -89,7 +88,7 @@ impl AverageTrueRange { ma_type: Option, use_previous: Option, value_floor: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { period, ma_type: ma_type.unwrap_or(MovingAverageType::Simple), diff --git a/nautilus_core/infrastructure/Cargo.toml b/nautilus_core/infrastructure/Cargo.toml index 886209f35292..5a0e69816669 100644 --- a/nautilus_core/infrastructure/Cargo.toml +++ b/nautilus_core/infrastructure/Cargo.toml @@ -19,11 +19,13 @@ pyo3 = { workspace = true, optional = true } redis = { workspace = true, optional = true } rmp-serde = { workspace = true } serde_json = { workspace = true } +tracing = {workspace = true } [dev-dependencies] rstest = { workspace = true } [features] +default = ["redis"] extension-module = [ "pyo3/extension-module", "nautilus-common/extension-module", @@ -32,4 +34,3 @@ extension-module = [ ] python = ["pyo3"] redis = ["dep:redis"] -default = ["redis"] diff --git a/nautilus_core/infrastructure/src/cache.rs b/nautilus_core/infrastructure/src/cache.rs deleted file mode 100644 index 4c0f7c985ef9..000000000000 --- a/nautilus_core/infrastructure/src/cache.rs +++ /dev/null @@ -1,76 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (C) 2015-2024 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. -// ------------------------------------------------------------------------------------------------- - -use std::{collections::HashMap, sync::mpsc::Receiver}; - -use anyhow::Result; -use nautilus_core::uuid::UUID4; -use nautilus_model::identifiers::trader_id::TraderId; - -/// A type of database operation. -#[derive(Clone, Debug)] -pub enum DatabaseOperation { - Insert, - Update, - Delete, -} - -/// Represents a database command to be performed which may be executed 'remotely' across a thread. -#[derive(Clone, Debug)] -pub struct DatabaseCommand { - /// The database operation type. - pub op_type: DatabaseOperation, - /// The primary key for the operation. - pub key: String, - /// The data payload for the operation. - pub payload: Option>>, -} - -impl DatabaseCommand { - pub fn new(op_type: DatabaseOperation, key: String, payload: Option>>) -> Self { - Self { - op_type, - key, - payload, - } - } -} - -/// Provides a generic cache database facade. -/// -/// The main operations take a consistent `key` and `payload` which should provide enough -/// information to implement the cache database in many different technologies. -/// -/// Delete operations may need a `payload` to target specific values. -pub trait CacheDatabase { - type DatabaseType; - - fn new( - trader_id: TraderId, - instance_id: UUID4, - config: HashMap, - ) -> Result; - fn flushdb(&mut self) -> Result<()>; - fn keys(&mut self, pattern: &str) -> Result>; - fn read(&mut self, key: &str) -> Result>>; - fn insert(&mut self, key: String, payload: Option>>) -> Result<()>; - fn update(&mut self, key: String, payload: Option>>) -> Result<()>; - fn delete(&mut self, key: String, payload: Option>>) -> Result<()>; - fn handle_messages( - rx: Receiver, - trader_key: String, - config: HashMap, - ); -} diff --git a/nautilus_core/infrastructure/src/lib.rs b/nautilus_core/infrastructure/src/lib.rs index a05090fafc7d..ae05731698d4 100644 --- a/nautilus_core/infrastructure/src/lib.rs +++ b/nautilus_core/infrastructure/src/lib.rs @@ -13,8 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -pub mod cache; - #[cfg(feature = "python")] pub mod python; diff --git a/nautilus_core/infrastructure/src/python/cache.rs b/nautilus_core/infrastructure/src/python/cache.rs index 25d74b6b3d34..e8f86bcf622e 100644 --- a/nautilus_core/infrastructure/src/python/cache.rs +++ b/nautilus_core/infrastructure/src/python/cache.rs @@ -15,6 +15,7 @@ use std::collections::HashMap; +use nautilus_common::cache::CacheDatabase; use nautilus_core::{ python::{to_pyruntime_err, to_pyvalue_err}, uuid::UUID4, @@ -22,7 +23,7 @@ use nautilus_core::{ use nautilus_model::identifiers::trader_id::TraderId; use pyo3::{prelude::*, types::PyBytes}; -use crate::{cache::CacheDatabase, redis::RedisCacheDatabase}; +use crate::redis::RedisCacheDatabase; #[pymethods] impl RedisCacheDatabase { diff --git a/nautilus_core/infrastructure/src/redis.rs b/nautilus_core/infrastructure/src/redis.rs index 657e2c794880..a3b7471450bf 100644 --- a/nautilus_core/infrastructure/src/redis.rs +++ b/nautilus_core/infrastructure/src/redis.rs @@ -20,14 +20,15 @@ use std::{ time::{Duration, Instant}, }; -use anyhow::{anyhow, bail, Result}; -use nautilus_common::redis::{get_buffer_interval, get_redis_url}; +use nautilus_common::{ + cache::{CacheDatabase, DatabaseCommand, DatabaseOperation}, + redis::{create_redis_connection, get_buffer_interval}, +}; use nautilus_core::uuid::UUID4; use nautilus_model::identifiers::trader_id::TraderId; use redis::{Commands, Connection, Pipeline}; -use serde_json::json; - -use crate::cache::{CacheDatabase, DatabaseCommand, DatabaseOperation}; +use serde_json::{json, Value}; +use tracing::debug; // Error constants const CHANNEL_TX_FAILED: &str = "Failed to send to channel"; @@ -81,10 +82,12 @@ impl CacheDatabase for RedisCacheDatabase { trader_id: TraderId, instance_id: UUID4, config: HashMap, - ) -> Result { - let redis_url = get_redis_url(&config); - let client = redis::Client::open(redis_url)?; - let conn = client.get_connection().unwrap(); + ) -> anyhow::Result { + let database_config = config + .get("database") + .ok_or(anyhow::anyhow!("No database config"))?; + debug!("Creating cache-read redis connection"); + let conn = create_redis_connection(&database_config.clone())?; let (tx, rx) = channel::(); let trader_key = get_trader_key(trader_id, instance_id, &config); @@ -105,21 +108,23 @@ impl CacheDatabase for RedisCacheDatabase { }) } - fn flushdb(&mut self) -> Result<()> { + fn flushdb(&mut self) -> anyhow::Result<()> { match redis::cmd(FLUSHDB).query::<()>(&mut self.conn) { Ok(_) => Ok(()), Err(e) => Err(e.into()), } } - fn keys(&mut self, pattern: &str) -> Result> { + fn keys(&mut self, pattern: &str) -> anyhow::Result> { + let pattern = format!("{}{DELIMITER}{}", self.trader_key, pattern); + debug!("Querying keys: {pattern}"); match self.conn.keys(pattern) { Ok(keys) => Ok(keys), Err(e) => Err(e.into()), } } - fn read(&mut self, key: &str) -> Result>> { + fn read(&mut self, key: &str) -> anyhow::Result>> { let collection = get_collection_key(key)?; let key = format!("{}{DELIMITER}{}", self.trader_key, key); @@ -134,31 +139,31 @@ impl CacheDatabase for RedisCacheDatabase { POSITIONS => read_list(&mut self.conn, &key), ACTORS => read_string(&mut self.conn, &key), STRATEGIES => read_string(&mut self.conn, &key), - _ => bail!("Unsupported operation: `read` for collection '{collection}'"), + _ => anyhow::bail!("Unsupported operation: `read` for collection '{collection}'"), } } - fn insert(&mut self, key: String, payload: Option>>) -> Result<()> { + fn insert(&mut self, key: String, payload: Option>>) -> anyhow::Result<()> { let op = DatabaseCommand::new(DatabaseOperation::Insert, key, payload); match self.tx.send(op) { Ok(_) => Ok(()), - Err(e) => bail!("{CHANNEL_TX_FAILED}: {e}"), + Err(e) => anyhow::bail!("{CHANNEL_TX_FAILED}: {e}"), } } - fn update(&mut self, key: String, payload: Option>>) -> Result<()> { + fn update(&mut self, key: String, payload: Option>>) -> anyhow::Result<()> { let op = DatabaseCommand::new(DatabaseOperation::Update, key, payload); match self.tx.send(op) { Ok(_) => Ok(()), - Err(e) => bail!("{CHANNEL_TX_FAILED}: {e}"), + Err(e) => anyhow::bail!("{CHANNEL_TX_FAILED}: {e}"), } } - fn delete(&mut self, key: String, payload: Option>>) -> Result<()> { + fn delete(&mut self, key: String, payload: Option>>) -> anyhow::Result<()> { let op = DatabaseCommand::new(DatabaseOperation::Delete, key, payload); match self.tx.send(op) { Ok(_) => Ok(()), - Err(e) => bail!("{CHANNEL_TX_FAILED}: {e}"), + Err(e) => anyhow::bail!("{CHANNEL_TX_FAILED}: {e}"), } } @@ -167,9 +172,10 @@ impl CacheDatabase for RedisCacheDatabase { trader_key: String, config: HashMap, ) { - let redis_url = get_redis_url(&config); - let client = redis::Client::open(redis_url).unwrap(); - let mut conn = client.get_connection().unwrap(); + let empty = Value::Object(serde_json::Map::new()); + let database_config = config.get("database").unwrap_or(&empty); + debug!("Creating cache-write redis connection"); + let mut conn = create_redis_connection(&database_config.clone()).unwrap(); // Buffering let mut buffer: VecDeque = VecDeque::new(); @@ -269,7 +275,7 @@ fn drain_buffer(conn: &mut Connection, trader_key: &str, buffer: &mut VecDeque Result>> { +fn read_index(conn: &mut Connection, key: &str) -> anyhow::Result>> { let index_key = get_index_key(key)?; match index_key { INDEX_ORDER_IDS => read_set(conn, key), @@ -283,11 +289,11 @@ fn read_index(conn: &mut Connection, key: &str) -> Result>> { INDEX_POSITIONS => read_set(conn, key), INDEX_POSITIONS_OPEN => read_set(conn, key), INDEX_POSITIONS_CLOSED => read_set(conn, key), - _ => bail!("Index unknown '{index_key}' on read"), + _ => anyhow::bail!("Index unknown '{index_key}' on read"), } } -fn read_string(conn: &mut Connection, key: &str) -> Result>> { +fn read_string(conn: &mut Connection, key: &str) -> anyhow::Result>> { let result: Vec = conn.get(key)?; if result.is_empty() { @@ -297,25 +303,30 @@ fn read_string(conn: &mut Connection, key: &str) -> Result>> { } } -fn read_set(conn: &mut Connection, key: &str) -> Result>> { +fn read_set(conn: &mut Connection, key: &str) -> anyhow::Result>> { let result: Vec> = conn.smembers(key)?; Ok(result) } -fn read_hset(conn: &mut Connection, key: &str) -> Result>> { +fn read_hset(conn: &mut Connection, key: &str) -> anyhow::Result>> { let result: HashMap = conn.hgetall(key)?; let json = serde_json::to_string(&result)?; Ok(vec![json.into_bytes()]) } -fn read_list(conn: &mut Connection, key: &str) -> Result>> { +fn read_list(conn: &mut Connection, key: &str) -> anyhow::Result>> { let result: Vec> = conn.lrange(key, 0, -1)?; Ok(result) } -fn insert(pipe: &mut Pipeline, collection: &str, key: &str, value: Vec<&[u8]>) -> Result<()> { +fn insert( + pipe: &mut Pipeline, + collection: &str, + key: &str, + value: Vec<&[u8]>, +) -> anyhow::Result<()> { if value.is_empty() { - bail!("Empty `payload` for `insert`") + anyhow::bail!("Empty `payload` for `insert`") } match collection { @@ -364,11 +375,11 @@ fn insert(pipe: &mut Pipeline, collection: &str, key: &str, value: Vec<&[u8]>) - insert_string(pipe, key, value[0]); Ok(()) } - _ => bail!("Unsupported operation: `insert` for collection '{collection}'"), + _ => anyhow::bail!("Unsupported operation: `insert` for collection '{collection}'"), } } -fn insert_index(pipe: &mut Pipeline, key: &str, value: &[&[u8]]) -> Result<()> { +fn insert_index(pipe: &mut Pipeline, key: &str, value: &[&[u8]]) -> anyhow::Result<()> { let index_key = get_index_key(key)?; match index_key { INDEX_ORDER_IDS => { @@ -415,7 +426,7 @@ fn insert_index(pipe: &mut Pipeline, key: &str, value: &[&[u8]]) -> Result<()> { insert_set(pipe, key, value[0]); Ok(()) } - _ => bail!("Index unknown '{index_key}' on insert"), + _ => anyhow::bail!("Index unknown '{index_key}' on insert"), } } @@ -435,9 +446,14 @@ fn insert_list(pipe: &mut Pipeline, key: &str, value: &[u8]) { pipe.rpush(key, value); } -fn update(pipe: &mut Pipeline, collection: &str, key: &str, value: Vec<&[u8]>) -> Result<()> { +fn update( + pipe: &mut Pipeline, + collection: &str, + key: &str, + value: Vec<&[u8]>, +) -> anyhow::Result<()> { if value.is_empty() { - bail!("Empty `payload` for `update`") + anyhow::bail!("Empty `payload` for `update`") } match collection { @@ -453,7 +469,7 @@ fn update(pipe: &mut Pipeline, collection: &str, key: &str, value: Vec<&[u8]>) - update_list(pipe, key, value[0]); Ok(()) } - _ => bail!("Unsupported operation: `update` for collection '{collection}'"), + _ => anyhow::bail!("Unsupported operation: `update` for collection '{collection}'"), } } @@ -466,7 +482,7 @@ fn delete( collection: &str, key: &str, value: Option>, -) -> Result<()> { +) -> anyhow::Result<()> { match collection { INDEX => remove_index(pipe, key, value), ACTORS => { @@ -477,12 +493,12 @@ fn delete( delete_string(pipe, key); Ok(()) } - _ => bail!("Unsupported operation: `delete` for collection '{collection}'"), + _ => anyhow::bail!("Unsupported operation: `delete` for collection '{collection}'"), } } -fn remove_index(pipe: &mut Pipeline, key: &str, value: Option>) -> Result<()> { - let value = value.ok_or_else(|| anyhow!("Empty `payload` for `delete` '{key}'"))?; +fn remove_index(pipe: &mut Pipeline, key: &str, value: Option>) -> anyhow::Result<()> { + let value = value.ok_or_else(|| anyhow::anyhow!("Empty `payload` for `delete` '{key}'"))?; let index_key = get_index_key(key)?; match index_key { @@ -510,7 +526,7 @@ fn remove_index(pipe: &mut Pipeline, key: &str, value: Option>) -> Re remove_from_set(pipe, key, value[0]); Ok(()) } - _ => bail!("Unsupported index operation: remove from '{index_key}'"), + _ => anyhow::bail!("Unsupported index operation: remove from '{index_key}'"), } } @@ -543,16 +559,20 @@ fn get_trader_key( key } -fn get_collection_key(key: &str) -> Result<&str> { +fn get_collection_key(key: &str) -> anyhow::Result<&str> { key.split_once(DELIMITER) .map(|(collection, _)| collection) - .ok_or_else(|| anyhow!("Invalid `key`, missing a '{DELIMITER}' delimiter, was {key}")) + .ok_or_else(|| { + anyhow::anyhow!("Invalid `key`, missing a '{DELIMITER}' delimiter, was {key}") + }) } -fn get_index_key(key: &str) -> Result<&str> { +fn get_index_key(key: &str) -> anyhow::Result<&str> { key.split_once(DELIMITER) .map(|(_, index_key)| index_key) - .ok_or_else(|| anyhow!("Invalid `key`, missing a '{DELIMITER}' delimiter, was {key}")) + .ok_or_else(|| { + anyhow::anyhow!("Invalid `key`, missing a '{DELIMITER}' delimiter, was {key}") + }) } // This function can be used when we handle cache serialization in Rust @@ -570,13 +590,13 @@ fn get_encoding(config: &HashMap) -> String { fn deserialize_payload( encoding: &str, payload: &[u8], -) -> Result> { +) -> anyhow::Result> { match encoding { "msgpack" => rmp_serde::from_slice(payload) - .map_err(|e| anyhow!("Failed to deserialize msgpack `payload`: {e}")), + .map_err(|e| anyhow::anyhow!("Failed to deserialize msgpack `payload`: {e}")), "json" => serde_json::from_slice(payload) - .map_err(|e| anyhow!("Failed to deserialize json `payload`: {e}")), - _ => Err(anyhow!("Unsupported encoding: {encoding}")), + .map_err(|e| anyhow::anyhow!("Failed to deserialize json `payload`: {e}")), + _ => Err(anyhow::anyhow!("Unsupported encoding: {encoding}")), } } diff --git a/nautilus_core/model/Cargo.toml b/nautilus_core/model/Cargo.toml index 6819d59bc779..fc6d0609bd75 100644 --- a/nautilus_core/model/Cargo.toml +++ b/nautilus_core/model/Cargo.toml @@ -39,6 +39,8 @@ iai = { workspace = true } cbindgen = { workspace = true, optional = true } [features] +default = ["trivial_copy"] +trivial_copy = [] # Enables deriving the `Copy` trait for data types (should be included in default) extension-module = [ "pyo3/extension-module", "nautilus-core/extension-module", @@ -46,8 +48,6 @@ extension-module = [ ffi = ["cbindgen", "nautilus-core/ffi"] python = ["pyo3", "nautilus-core/python"] stubs = ["rstest"] -trivial_copy = [] # Enables deriving the `Copy` trait for data types (should be included in default) -default = ["trivial_copy"] [[bench]] name = "criterion_fixed_precision_benchmark" diff --git a/nautilus_core/model/src/data/quote.rs b/nautilus_core/model/src/data/quote.rs index d37d1daa8a62..a0405875679e 100644 --- a/nautilus_core/model/src/data/quote.rs +++ b/nautilus_core/model/src/data/quote.rs @@ -20,9 +20,8 @@ use std::{ hash::Hash, }; -use anyhow::Result; use indexmap::IndexMap; -use nautilus_core::{correctness::check_u8_equal, serialization::Serializable, time::UnixNanos}; +use nautilus_core::{correctness::check_equal_u8, serialization::Serializable, time::UnixNanos}; use serde::{Deserialize, Serialize}; use crate::{ @@ -66,14 +65,14 @@ impl QuoteTick { ask_size: Quantity, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { - check_u8_equal( + ) -> anyhow::Result { + check_equal_u8( bid_price.precision, ask_price.precision, "bid_price.precision", "ask_price.precision", )?; - check_u8_equal( + check_equal_u8( bid_size.precision, ask_size.precision, "bid_size.precision", diff --git a/nautilus_core/model/src/enums.rs b/nautilus_core/model/src/enums.rs index b7fb6383fba5..0917e05dd0e5 100644 --- a/nautilus_core/model/src/enums.rs +++ b/nautilus_core/model/src/enums.rs @@ -493,8 +493,8 @@ pub enum InstrumentCloseType { )] #[allow(clippy::enum_variant_names)] pub enum LiquiditySide { - /// No specific liqudity side. - NoLiquiditySide = 0, // Will be replaced by `Option` + /// No liquidity side specified. + NoLiquiditySide = 0, /// The order passively provided liqudity to the market to complete the trade (made a market). Maker = 1, /// The order aggressively took liqudity from the market to complete the trade. diff --git a/nautilus_core/model/src/events/account/state.rs b/nautilus_core/model/src/events/account/state.rs index 71be35979981..1ec3ef73d5bb 100644 --- a/nautilus_core/model/src/events/account/state.rs +++ b/nautilus_core/model/src/events/account/state.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -58,7 +57,7 @@ impl AccountState { ts_event: UnixNanos, ts_init: UnixNanos, base_currency: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { account_id, account_type, diff --git a/nautilus_core/model/src/events/order/accepted.rs b/nautilus_core/model/src/events/order/accepted.rs index 70122f5bed3b..f2a0663b297f 100644 --- a/nautilus_core/model/src/events/order/accepted.rs +++ b/nautilus_core/model/src/events/order/accepted.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -59,7 +58,7 @@ impl OrderAccepted { ts_event: UnixNanos, ts_init: UnixNanos, reconciliation: bool, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/cancel_rejected.rs b/nautilus_core/model/src/events/order/cancel_rejected.rs index 6d413617e491..17e073c126f1 100644 --- a/nautilus_core/model/src/events/order/cancel_rejected.rs +++ b/nautilus_core/model/src/events/order/cancel_rejected.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -62,7 +61,7 @@ impl OrderCancelRejected { reconciliation: bool, venue_order_id: Option, account_id: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/canceled.rs b/nautilus_core/model/src/events/order/canceled.rs index ce64080d2b1d..ad0d8ca6bdbc 100644 --- a/nautilus_core/model/src/events/order/canceled.rs +++ b/nautilus_core/model/src/events/order/canceled.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -59,7 +58,7 @@ impl OrderCanceled { reconciliation: bool, venue_order_id: Option, account_id: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/denied.rs b/nautilus_core/model/src/events/order/denied.rs index 479587c0f08d..811aee711e2d 100644 --- a/nautilus_core/model/src/events/order/denied.rs +++ b/nautilus_core/model/src/events/order/denied.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -56,7 +55,7 @@ impl OrderDenied { event_id: UUID4, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/emulated.rs b/nautilus_core/model/src/events/order/emulated.rs index 276f3d287035..860e90d060f0 100644 --- a/nautilus_core/model/src/events/order/emulated.rs +++ b/nautilus_core/model/src/events/order/emulated.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -53,7 +52,7 @@ impl OrderEmulated { event_id: UUID4, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/expired.rs b/nautilus_core/model/src/events/order/expired.rs index db5583959606..b6f264c53fb4 100644 --- a/nautilus_core/model/src/events/order/expired.rs +++ b/nautilus_core/model/src/events/order/expired.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -59,7 +58,7 @@ impl OrderExpired { reconciliation: bool, venue_order_id: Option, account_id: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/filled.rs b/nautilus_core/model/src/events/order/filled.rs index 19f6aee315cb..3df1d0d727f2 100644 --- a/nautilus_core/model/src/events/order/filled.rs +++ b/nautilus_core/model/src/events/order/filled.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -82,7 +81,7 @@ impl OrderFilled { reconciliation: bool, position_id: Option, commission: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/initialized.rs b/nautilus_core/model/src/events/order/initialized.rs index 4b31dfea83f1..7d8a442321ed 100644 --- a/nautilus_core/model/src/events/order/initialized.rs +++ b/nautilus_core/model/src/events/order/initialized.rs @@ -18,7 +18,6 @@ use std::{ fmt::{Display, Formatter}, }; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -154,7 +153,7 @@ impl OrderInitialized { exec_algorithm_params: Option>, exec_spawn_id: Option, tags: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/modify_rejected.rs b/nautilus_core/model/src/events/order/modify_rejected.rs index 384b7de4c9e0..cd671e25b37a 100644 --- a/nautilus_core/model/src/events/order/modify_rejected.rs +++ b/nautilus_core/model/src/events/order/modify_rejected.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -62,7 +61,7 @@ impl OrderModifyRejected { reconciliation: bool, venue_order_id: Option, account_id: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/pending_cancel.rs b/nautilus_core/model/src/events/order/pending_cancel.rs index c6a792697c4c..f483bcf47d33 100644 --- a/nautilus_core/model/src/events/order/pending_cancel.rs +++ b/nautilus_core/model/src/events/order/pending_cancel.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -59,7 +58,7 @@ impl OrderPendingCancel { ts_init: UnixNanos, reconciliation: bool, venue_order_id: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/pending_update.rs b/nautilus_core/model/src/events/order/pending_update.rs index 9bd4a45561d8..9377710019c2 100644 --- a/nautilus_core/model/src/events/order/pending_update.rs +++ b/nautilus_core/model/src/events/order/pending_update.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -59,7 +58,7 @@ impl OrderPendingUpdate { ts_init: UnixNanos, reconciliation: bool, venue_order_id: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/rejected.rs b/nautilus_core/model/src/events/order/rejected.rs index 146e804a52be..ed8e4073f697 100644 --- a/nautilus_core/model/src/events/order/rejected.rs +++ b/nautilus_core/model/src/events/order/rejected.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -60,7 +59,7 @@ impl OrderRejected { ts_event: UnixNanos, ts_init: UnixNanos, reconciliation: bool, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/released.rs b/nautilus_core/model/src/events/order/released.rs index d35d9169088d..762404e068d8 100644 --- a/nautilus_core/model/src/events/order/released.rs +++ b/nautilus_core/model/src/events/order/released.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -58,7 +57,7 @@ impl OrderReleased { event_id: UUID4, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/submitted.rs b/nautilus_core/model/src/events/order/submitted.rs index ffa7eacc9188..6d4c40ba16ed 100644 --- a/nautilus_core/model/src/events/order/submitted.rs +++ b/nautilus_core/model/src/events/order/submitted.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -55,7 +54,7 @@ impl OrderSubmitted { event_id: UUID4, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/triggered.rs b/nautilus_core/model/src/events/order/triggered.rs index 26e3e1bd6ac4..873adac70c3f 100644 --- a/nautilus_core/model/src/events/order/triggered.rs +++ b/nautilus_core/model/src/events/order/triggered.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -59,7 +58,7 @@ impl OrderTriggered { reconciliation: bool, venue_order_id: Option, account_id: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/updated.rs b/nautilus_core/model/src/events/order/updated.rs index 96a3393d7480..c33c8025d258 100644 --- a/nautilus_core/model/src/events/order/updated.rs +++ b/nautilus_core/model/src/events/order/updated.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -68,7 +67,7 @@ impl OrderUpdated { account_id: Option, price: Option, trigger_price: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/identifiers/account_id.rs b/nautilus_core/model/src/identifiers/account_id.rs index cb6091dd0c69..b4bb1634d4b5 100644 --- a/nautilus_core/model/src/identifiers/account_id.rs +++ b/nautilus_core/model/src/identifiers/account_id.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use nautilus_core::correctness::{check_string_contains, check_valid_string}; use ustr::Ustr; @@ -41,12 +40,12 @@ pub struct AccountId { } impl AccountId { - pub fn new(s: &str) -> Result { - check_valid_string(s, "`accountid` value")?; - check_string_contains(s, "-", "`AccountId` value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; + check_string_contains(value, "-", stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } } diff --git a/nautilus_core/model/src/identifiers/client_id.rs b/nautilus_core/model/src/identifiers/client_id.rs index 91567cea588f..54d2a6807b30 100644 --- a/nautilus_core/model/src/identifiers/client_id.rs +++ b/nautilus_core/model/src/identifiers/client_id.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use nautilus_core::correctness::check_valid_string; use ustr::Ustr; @@ -35,11 +34,11 @@ pub struct ClientId { } impl ClientId { - pub fn new(s: &str) -> Result { - check_valid_string(s, "`ClientId` value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } } diff --git a/nautilus_core/model/src/identifiers/client_order_id.rs b/nautilus_core/model/src/identifiers/client_order_id.rs index e484b64d3c05..3bbb7dfd79fe 100644 --- a/nautilus_core/model/src/identifiers/client_order_id.rs +++ b/nautilus_core/model/src/identifiers/client_order_id.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use nautilus_core::correctness::check_valid_string; use ustr::Ustr; @@ -35,11 +34,11 @@ pub struct ClientOrderId { } impl ClientOrderId { - pub fn new(s: &str) -> Result { - check_valid_string(s, "`ClientOrderId` value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } } diff --git a/nautilus_core/model/src/identifiers/component_id.rs b/nautilus_core/model/src/identifiers/component_id.rs index ced0a8c39b56..0b607f7fae1f 100644 --- a/nautilus_core/model/src/identifiers/component_id.rs +++ b/nautilus_core/model/src/identifiers/component_id.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use nautilus_core::correctness::check_valid_string; use ustr::Ustr; @@ -35,11 +34,11 @@ pub struct ComponentId { } impl ComponentId { - pub fn new(s: &str) -> Result { - check_valid_string(s, "`ComponentId` value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } } diff --git a/nautilus_core/model/src/identifiers/exec_algorithm_id.rs b/nautilus_core/model/src/identifiers/exec_algorithm_id.rs index 7380d8edfc87..7cb4d9f967d0 100644 --- a/nautilus_core/model/src/identifiers/exec_algorithm_id.rs +++ b/nautilus_core/model/src/identifiers/exec_algorithm_id.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use nautilus_core::correctness::check_valid_string; use ustr::Ustr; @@ -35,11 +34,11 @@ pub struct ExecAlgorithmId { } impl ExecAlgorithmId { - pub fn new(s: &str) -> Result { - check_valid_string(s, "`ExecAlgorithmId` value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } } diff --git a/nautilus_core/model/src/identifiers/instrument_id.rs b/nautilus_core/model/src/identifiers/instrument_id.rs index 669feebf7f9f..ef154a93310a 100644 --- a/nautilus_core/model/src/identifiers/instrument_id.rs +++ b/nautilus_core/model/src/identifiers/instrument_id.rs @@ -19,7 +19,6 @@ use std::{ str::FromStr, }; -use anyhow::{anyhow, bail, Result}; use serde::{Deserialize, Deserializer, Serialize}; use crate::identifiers::{symbol::Symbol, venue::Venue}; @@ -55,16 +54,16 @@ impl InstrumentId { impl FromStr for InstrumentId { type Err = anyhow::Error; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> anyhow::Result { match s.rsplit_once('.') { Some((symbol_part, venue_part)) => Ok(Self { symbol: Symbol::new(symbol_part) - .map_err(|e| anyhow!(err_message(s, e.to_string())))?, + .map_err(|e| anyhow::anyhow!(err_message(s, e.to_string())))?, venue: Venue::new(venue_part) - .map_err(|e| anyhow!(err_message(s, e.to_string())))?, + .map_err(|e| anyhow::anyhow!(err_message(s, e.to_string())))?, }), None => { - bail!(err_message( + anyhow::bail!(err_message( s, "Missing '.' separator between symbol and venue components".to_string() )) diff --git a/nautilus_core/model/src/identifiers/order_list_id.rs b/nautilus_core/model/src/identifiers/order_list_id.rs index 856979e87dfd..6165204f5197 100644 --- a/nautilus_core/model/src/identifiers/order_list_id.rs +++ b/nautilus_core/model/src/identifiers/order_list_id.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use nautilus_core::correctness::check_valid_string; use ustr::Ustr; @@ -35,11 +34,11 @@ pub struct OrderListId { } impl OrderListId { - pub fn new(s: &str) -> Result { - check_valid_string(s, "`OrderListId` value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } } diff --git a/nautilus_core/model/src/identifiers/position_id.rs b/nautilus_core/model/src/identifiers/position_id.rs index 38f72a8ac070..954ac04a2cf3 100644 --- a/nautilus_core/model/src/identifiers/position_id.rs +++ b/nautilus_core/model/src/identifiers/position_id.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use nautilus_core::correctness::check_valid_string; use ustr::Ustr; @@ -35,11 +34,11 @@ pub struct PositionId { } impl PositionId { - pub fn new(s: &str) -> Result { - check_valid_string(s, "`PositionId` value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } } diff --git a/nautilus_core/model/src/identifiers/strategy_id.rs b/nautilus_core/model/src/identifiers/strategy_id.rs index d933fd3dece7..ee218db4503d 100644 --- a/nautilus_core/model/src/identifiers/strategy_id.rs +++ b/nautilus_core/model/src/identifiers/strategy_id.rs @@ -15,10 +15,12 @@ use std::fmt::{Debug, Display, Formatter}; -use anyhow::Result; use nautilus_core::correctness::{check_string_contains, check_valid_string}; use ustr::Ustr; +/// The identifier for all 'external' strategy IDs (not local to this system instance). +const EXTERNAL_STRATEGY_ID: &str = "EXTERNAL"; + /// Represents a valid strategy ID. /// /// Must be correctly formatted with two valid strings either side of a hyphen. @@ -41,17 +43,29 @@ pub struct StrategyId { } impl StrategyId { - pub fn new(s: &str) -> Result { - check_valid_string(s, "`StrategyId` value")?; - if s != "EXTERNAL" { - check_string_contains(s, "-", "`StrategyId` value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; + if value != EXTERNAL_STRATEGY_ID { + check_string_contains(value, "-", stringify!(value))?; } Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } + #[must_use] + pub fn external() -> Self { + Self { + value: Ustr::from(EXTERNAL_STRATEGY_ID), + } + } + + #[must_use] + pub fn is_external(&self) -> bool { + self.value == EXTERNAL_STRATEGY_ID + } + #[must_use] pub fn get_tag(&self) -> &str { // SAFETY: Unwrap safe as value previously validated @@ -101,6 +115,16 @@ mod tests { assert_eq!(format!("{strategy_id_ema_cross}"), "EMACross-001"); } + #[rstest] + fn test_get_external() { + assert_eq!(StrategyId::external().value, "EXTERNAL"); + } + + #[rstest] + fn test_is_external() { + assert!(StrategyId::external().is_external()); + } + #[rstest] fn test_get_tag(strategy_id_ema_cross: StrategyId) { assert_eq!(strategy_id_ema_cross.get_tag(), "001"); diff --git a/nautilus_core/model/src/identifiers/symbol.rs b/nautilus_core/model/src/identifiers/symbol.rs index 475fa3a878d2..62118daf5419 100644 --- a/nautilus_core/model/src/identifiers/symbol.rs +++ b/nautilus_core/model/src/identifiers/symbol.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use nautilus_core::correctness::check_valid_string; use ustr::Ustr; @@ -35,11 +34,11 @@ pub struct Symbol { } impl Symbol { - pub fn new(s: &str) -> Result { - check_valid_string(s, "`Symbol` value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } diff --git a/nautilus_core/model/src/identifiers/trade_id.rs b/nautilus_core/model/src/identifiers/trade_id.rs index 06d1262c2bcb..797d3cf48169 100644 --- a/nautilus_core/model/src/identifiers/trade_id.rs +++ b/nautilus_core/model/src/identifiers/trade_id.rs @@ -19,17 +19,20 @@ use std::{ hash::Hash, }; -use anyhow::{bail, Result}; -use nautilus_core::correctness::check_valid_string; +use nautilus_core::correctness::{check_in_range_inclusive_usize, check_valid_string}; use serde::{Deserialize, Deserializer, Serialize}; +/// The maximum length of ASCII characters for a `TradeId` string value (including null terminator). +const TRADE_ID_LEN: usize = 37; + /// Represents a valid trade match ID (assigned by a trading venue). /// /// Maximum length is 36 characters. -/// Can correspond to the `TradeID <1003> field` of the FIX protocol. /// /// The unique ID assigned to the trade entity once it is received or matched by /// the exchange or central counterparty. +/// +/// Can correspond to the `TradeID <1003> field` of the FIX protocol. #[repr(C)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] #[cfg_attr( @@ -37,27 +40,22 @@ use serde::{Deserialize, Deserializer, Serialize}; pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") )] pub struct TradeId { - /// The trade match ID C string value as a fixed-length byte array. - pub(crate) value: [u8; 37], + /// The trade match ID value as a fixed-length C string byte array (includes null terminator). + pub(crate) value: [u8; 37], // cbindgen issue using the constant in the array } impl TradeId { - pub fn new(s: &str) -> Result { - let cstr = CString::new(s).expect("`CString` conversion failed"); - + pub fn new(value: &str) -> anyhow::Result { + let cstr = CString::new(value).expect("`CString` conversion failed"); Self::from_cstr(cstr) } - pub fn from_cstr(cstr: CString) -> Result { - check_valid_string(cstr.to_str()?, "`TradeId` value")?; - - // TODO: Temporarily make this 65 to accommodate Betfair trade IDs - // TODO: Extract this to single function + pub fn from_cstr(cstr: CString) -> anyhow::Result { let bytes = cstr.as_bytes_with_nul(); - if bytes.len() > 37 { - bail!("Condition failed: value exceeds maximum trade ID length of 36"); - } - let mut value = [0; 37]; + check_valid_string(cstr.to_str()?, stringify!(cstr))?; + check_in_range_inclusive_usize(bytes.len(), 2, TRADE_ID_LEN, stringify!(cstr))?; + + let mut value = [0; TRADE_ID_LEN]; value[..bytes.len()].copy_from_slice(bytes); Ok(Self { value }) diff --git a/nautilus_core/model/src/identifiers/trader_id.rs b/nautilus_core/model/src/identifiers/trader_id.rs index 226108fdf24d..d71bfb1af5e1 100644 --- a/nautilus_core/model/src/identifiers/trader_id.rs +++ b/nautilus_core/model/src/identifiers/trader_id.rs @@ -15,7 +15,6 @@ use std::fmt::{Debug, Display, Formatter}; -use anyhow::Result; use nautilus_core::correctness::{check_string_contains, check_valid_string}; use ustr::Ustr; @@ -41,12 +40,12 @@ pub struct TraderId { } impl TraderId { - pub fn new(s: &str) -> Result { - check_valid_string(s, "`TraderId` value")?; - check_string_contains(s, "-", "`TraderId` value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; + check_string_contains(value, "-", stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } diff --git a/nautilus_core/model/src/identifiers/venue.rs b/nautilus_core/model/src/identifiers/venue.rs index c1cc32b1fc3c..544aa98bd816 100644 --- a/nautilus_core/model/src/identifiers/venue.rs +++ b/nautilus_core/model/src/identifiers/venue.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::{anyhow, Result}; use nautilus_core::correctness::check_valid_string; use ustr::Ustr; @@ -39,11 +38,11 @@ pub struct Venue { } impl Venue { - pub fn new(s: &str) -> Result { - check_valid_string(s, "`Venue` value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } @@ -54,14 +53,14 @@ impl Venue { } } - pub fn from_code(code: &str) -> Result { + pub fn from_code(code: &str) -> anyhow::Result { let map_guard = VENUE_MAP .lock() - .map_err(|e| anyhow!("Failed to acquire lock on `VENUE_MAP`: {e}"))?; + .map_err(|e| anyhow::anyhow!("Error acquiring lock on `VENUE_MAP`: {e}"))?; map_guard .get(code) .copied() - .ok_or_else(|| anyhow!("Unknown venue code: {code}")) + .ok_or_else(|| anyhow::anyhow!("Unknown venue code: {code}")) } #[must_use] diff --git a/nautilus_core/model/src/identifiers/venue_order_id.rs b/nautilus_core/model/src/identifiers/venue_order_id.rs index 17105d0568ae..e347fdb89b07 100644 --- a/nautilus_core/model/src/identifiers/venue_order_id.rs +++ b/nautilus_core/model/src/identifiers/venue_order_id.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use nautilus_core::correctness::check_valid_string; use ustr::Ustr; @@ -35,11 +34,11 @@ pub struct VenueOrderId { } impl VenueOrderId { - pub fn new(s: &str) -> Result { - check_valid_string(s, "`VenueOrderId` value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } } diff --git a/nautilus_core/model/src/instruments/crypto_future.rs b/nautilus_core/model/src/instruments/crypto_future.rs index 227038ade7de..bd3e5f9f480e 100644 --- a/nautilus_core/model/src/instruments/crypto_future.rs +++ b/nautilus_core/model/src/instruments/crypto_future.rs @@ -18,8 +18,10 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::Result; -use nautilus_core::time::UnixNanos; +use nautilus_core::{ + correctness::{check_equal_u8, check_positive_i64, check_positive_u64}, + time::UnixNanos, +}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -91,7 +93,22 @@ impl CryptoFuture { min_price: Option, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { + check_equal_u8( + price_precision, + price_increment.precision, + stringify!(price_precision), + stringify!(price_increment.precision), + )?; + check_equal_u8( + size_precision, + size_increment.precision, + stringify!(size_precision), + stringify!(size_increment.precision), + )?; + check_positive_i64(price_increment.raw, stringify!(price_increment.raw))?; + check_positive_u64(size_increment.raw, stringify!(size_increment.raw))?; + Ok(Self { id, raw_symbol, diff --git a/nautilus_core/model/src/instruments/crypto_perpetual.rs b/nautilus_core/model/src/instruments/crypto_perpetual.rs index bef84460858d..bef7d05075ec 100644 --- a/nautilus_core/model/src/instruments/crypto_perpetual.rs +++ b/nautilus_core/model/src/instruments/crypto_perpetual.rs @@ -18,8 +18,10 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::Result; -use nautilus_core::time::UnixNanos; +use nautilus_core::{ + correctness::{check_equal_u8, check_positive_i64, check_positive_u64}, + time::UnixNanos, +}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -89,7 +91,22 @@ impl CryptoPerpetual { min_price: Option, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { + check_equal_u8( + price_precision, + price_increment.precision, + stringify!(price_precision), + stringify!(price_increment.precision), + )?; + check_equal_u8( + size_precision, + size_increment.precision, + stringify!(size_precision), + stringify!(size_increment.precision), + )?; + check_positive_i64(price_increment.raw, stringify!(price_increment.raw))?; + check_positive_u64(size_increment.raw, stringify!(size_increment.raw))?; + Ok(Self { id, raw_symbol, diff --git a/nautilus_core/model/src/instruments/currency_pair.rs b/nautilus_core/model/src/instruments/currency_pair.rs index 188982667bbb..afc89b8c51fc 100644 --- a/nautilus_core/model/src/instruments/currency_pair.rs +++ b/nautilus_core/model/src/instruments/currency_pair.rs @@ -18,8 +18,10 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::Result; -use nautilus_core::time::UnixNanos; +use nautilus_core::{ + correctness::{check_equal_u8, check_positive_i64, check_positive_u64}, + time::UnixNanos, +}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -85,7 +87,22 @@ impl CurrencyPair { min_price: Option, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { + check_equal_u8( + price_precision, + price_increment.precision, + stringify!(price_precision), + stringify!(price_increment.precision), + )?; + check_equal_u8( + size_precision, + size_increment.precision, + stringify!(size_precision), + stringify!(size_increment.precision), + )?; + check_positive_i64(price_increment.raw, stringify!(price_increment.raw))?; + check_positive_u64(size_increment.raw, stringify!(size_increment.raw))?; + Ok(Self { id, raw_symbol, diff --git a/nautilus_core/model/src/instruments/equity.rs b/nautilus_core/model/src/instruments/equity.rs index 5010444c4ad1..796274223557 100644 --- a/nautilus_core/model/src/instruments/equity.rs +++ b/nautilus_core/model/src/instruments/equity.rs @@ -18,8 +18,10 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::Result; -use nautilus_core::time::UnixNanos; +use nautilus_core::{ + correctness::{check_equal_u8, check_positive_i64, check_valid_string_optional}, + time::UnixNanos, +}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -79,7 +81,16 @@ impl Equity { min_price: Option, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { + check_valid_string_optional(isin.map(|u| u.as_str()), stringify!(isin))?; + check_equal_u8( + price_precision, + price_increment.precision, + stringify!(price_precision), + stringify!(price_increment.precision), + )?; + check_positive_i64(price_increment.raw, stringify!(price_increment.raw))?; + Ok(Self { id, raw_symbol, diff --git a/nautilus_core/model/src/instruments/futures_contract.rs b/nautilus_core/model/src/instruments/futures_contract.rs index 7003983f9398..3a33311b2c85 100644 --- a/nautilus_core/model/src/instruments/futures_contract.rs +++ b/nautilus_core/model/src/instruments/futures_contract.rs @@ -18,8 +18,13 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::Result; -use nautilus_core::time::UnixNanos; +use nautilus_core::{ + correctness::{ + check_equal_u8, check_positive_i64, check_valid_string, check_valid_string_optional, + }, + time::UnixNanos, +}; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -49,8 +54,12 @@ pub struct FuturesContract { pub currency: Currency, pub price_precision: u8, pub price_increment: Price, + pub size_increment: Quantity, + pub size_precision: u8, pub multiplier: Quantity, pub lot_size: Quantity, + pub margin_init: Decimal, + pub margin_maint: Decimal, pub max_quantity: Option, pub min_quantity: Option, pub max_price: Option, @@ -78,9 +87,21 @@ impl FuturesContract { min_quantity: Option, max_price: Option, min_price: Option, + margin_init: Option, + margin_maint: Option, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { + check_valid_string_optional(exchange.map(|u| u.as_str()), stringify!(isin))?; + check_valid_string(underlying.as_str(), stringify!(underlying))?; + check_equal_u8( + price_precision, + price_increment.precision, + stringify!(price_precision), + stringify!(price_increment.precision), + )?; + check_positive_i64(price_increment.raw, stringify!(price_increment.raw))?; + Ok(Self { id, raw_symbol, @@ -92,10 +113,14 @@ impl FuturesContract { currency, price_precision, price_increment, + size_precision: 0, + size_increment: Quantity::from("1"), multiplier, lot_size, + margin_init: margin_init.unwrap_or(0.into()), + margin_maint: margin_maint.unwrap_or(0.into()), max_quantity, - min_quantity, + min_quantity: Some(min_quantity.unwrap_or(1.into())), max_price, min_price, ts_event, diff --git a/nautilus_core/model/src/instruments/futures_spread.rs b/nautilus_core/model/src/instruments/futures_spread.rs index a8ba3fcb2390..5bf71d87dfd2 100644 --- a/nautilus_core/model/src/instruments/futures_spread.rs +++ b/nautilus_core/model/src/instruments/futures_spread.rs @@ -18,8 +18,13 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::Result; -use nautilus_core::time::UnixNanos; +use nautilus_core::{ + correctness::{ + check_equal_u8, check_positive_i64, check_valid_string, check_valid_string_optional, + }, + time::UnixNanos, +}; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -50,8 +55,12 @@ pub struct FuturesSpread { pub currency: Currency, pub price_precision: u8, pub price_increment: Price, + pub size_increment: Quantity, + pub size_precision: u8, pub multiplier: Quantity, pub lot_size: Quantity, + pub margin_init: Decimal, + pub margin_maint: Decimal, pub max_quantity: Option, pub min_quantity: Option, pub max_price: Option, @@ -80,9 +89,22 @@ impl FuturesSpread { min_quantity: Option, max_price: Option, min_price: Option, + margin_init: Option, + margin_maint: Option, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { + check_valid_string_optional(exchange.map(|u| u.as_str()), stringify!(isin))?; + check_valid_string(underlying.as_str(), stringify!(underlying))?; + check_valid_string(strategy_type.as_str(), stringify!(strategy_type))?; + check_equal_u8( + price_precision, + price_increment.precision, + stringify!(price_precision), + stringify!(price_increment.precision), + )?; + check_positive_i64(price_increment.raw, stringify!(price_increment.raw))?; + Ok(Self { id, raw_symbol, @@ -95,10 +117,14 @@ impl FuturesSpread { currency, price_precision, price_increment, + size_precision: 0, + size_increment: Quantity::from("1"), multiplier, lot_size, + margin_init: margin_init.unwrap_or(0.into()), + margin_maint: margin_maint.unwrap_or(0.into()), max_quantity, - min_quantity, + min_quantity: Some(min_quantity.unwrap_or(1.into())), max_price, min_price, ts_event, diff --git a/nautilus_core/model/src/instruments/mod.rs b/nautilus_core/model/src/instruments/mod.rs index a5ab72b37d3c..813e5c47d63a 100644 --- a/nautilus_core/model/src/instruments/mod.rs +++ b/nautilus_core/model/src/instruments/mod.rs @@ -27,7 +27,6 @@ pub mod synthetic; #[cfg(feature = "stubs")] pub mod stubs; -use anyhow::Result; use nautilus_core::time::UnixNanos; use rust_decimal::Decimal; use rust_decimal_macros::dec; @@ -99,12 +98,12 @@ pub trait Instrument: Any + 'static + Send { fn ts_init(&self) -> UnixNanos; /// Creates a new price from the given `value` with the correct price precision for the instrument. - fn make_price(&self, value: f64) -> Result { + fn make_price(&self, value: f64) -> anyhow::Result { Price::new(value, self.price_precision()) } /// Creates a new quantity from the given `value` with the correct size precision for the instrument. - fn make_qty(&self, value: f64) -> Result { + fn make_qty(&self, value: f64) -> anyhow::Result { Quantity::new(value, self.size_precision()) } diff --git a/nautilus_core/model/src/instruments/options_contract.rs b/nautilus_core/model/src/instruments/options_contract.rs index d3307d6de831..ea1f0a55cad3 100644 --- a/nautilus_core/model/src/instruments/options_contract.rs +++ b/nautilus_core/model/src/instruments/options_contract.rs @@ -18,8 +18,13 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::Result; -use nautilus_core::time::UnixNanos; +use nautilus_core::{ + correctness::{ + check_equal_u8, check_positive_i64, check_valid_string, check_valid_string_optional, + }, + time::UnixNanos, +}; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -51,8 +56,12 @@ pub struct OptionsContract { pub currency: Currency, pub price_precision: u8, pub price_increment: Price, + pub size_increment: Quantity, + pub size_precision: u8, pub multiplier: Quantity, pub lot_size: Quantity, + pub margin_init: Decimal, + pub margin_maint: Decimal, pub max_quantity: Option, pub min_quantity: Option, pub max_price: Option, @@ -82,9 +91,21 @@ impl OptionsContract { min_quantity: Option, max_price: Option, min_price: Option, + margin_init: Option, + margin_maint: Option, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { + check_valid_string_optional(exchange.map(|u| u.as_str()), stringify!(isin))?; + check_valid_string(underlying.as_str(), stringify!(underlying))?; + check_equal_u8( + price_precision, + price_increment.precision, + stringify!(price_precision), + stringify!(price_increment.precision), + )?; + check_positive_i64(price_increment.raw, stringify!(price_increment.raw))?; + Ok(Self { id, raw_symbol, @@ -98,12 +119,16 @@ impl OptionsContract { currency, price_precision, price_increment, + size_precision: 0, + size_increment: Quantity::from("1"), multiplier, lot_size, max_quantity, - min_quantity, + min_quantity: Some(min_quantity.unwrap_or(1.into())), max_price, min_price, + margin_init: margin_init.unwrap_or(0.into()), + margin_maint: margin_maint.unwrap_or(0.into()), ts_event, ts_init, }) diff --git a/nautilus_core/model/src/instruments/options_spread.rs b/nautilus_core/model/src/instruments/options_spread.rs index 2a4522644d78..4aad434cfeb2 100644 --- a/nautilus_core/model/src/instruments/options_spread.rs +++ b/nautilus_core/model/src/instruments/options_spread.rs @@ -18,8 +18,13 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::Result; -use nautilus_core::time::UnixNanos; +use nautilus_core::{ + correctness::{ + check_equal_u8, check_positive_i64, check_valid_string, check_valid_string_optional, + }, + time::UnixNanos, +}; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -50,8 +55,12 @@ pub struct OptionsSpread { pub currency: Currency, pub price_precision: u8, pub price_increment: Price, + pub size_increment: Quantity, + pub size_precision: u8, pub multiplier: Quantity, pub lot_size: Quantity, + pub margin_init: Decimal, + pub margin_maint: Decimal, pub max_quantity: Option, pub min_quantity: Option, pub max_price: Option, @@ -76,13 +85,26 @@ impl OptionsSpread { price_increment: Price, multiplier: Quantity, lot_size: Quantity, + margin_init: Option, + margin_maint: Option, max_quantity: Option, min_quantity: Option, max_price: Option, min_price: Option, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { + check_valid_string_optional(exchange.map(|u| u.as_str()), stringify!(isin))?; + check_valid_string(underlying.as_str(), stringify!(underlying))?; + check_valid_string(strategy_type.as_str(), stringify!(strategy_type))?; + check_equal_u8( + price_precision, + price_increment.precision, + stringify!(price_precision), + stringify!(price_increment.precision), + )?; + check_positive_i64(price_increment.raw, stringify!(price_increment.raw))?; + Ok(Self { id, raw_symbol, @@ -95,10 +117,14 @@ impl OptionsSpread { currency, price_precision, price_increment, + size_precision: 0, + size_increment: Quantity::from("1"), multiplier, lot_size, + margin_init: margin_init.unwrap_or(0.into()), + margin_maint: margin_maint.unwrap_or(0.into()), max_quantity, - min_quantity, + min_quantity: Some(min_quantity.unwrap_or(1.into())), max_price, min_price, ts_event, diff --git a/nautilus_core/model/src/instruments/stubs.rs b/nautilus_core/model/src/instruments/stubs.rs index 584e3f83114a..12a41667de31 100644 --- a/nautilus_core/model/src/instruments/stubs.rs +++ b/nautilus_core/model/src/instruments/stubs.rs @@ -82,7 +82,7 @@ pub fn crypto_perpetual_ethusdt() -> CryptoPerpetual { Currency::from("USDT"), false, 2, - 0, + 3, Price::from("0.01"), Quantity::from("0.001"), dec!(0.0002), @@ -318,6 +318,8 @@ pub fn futures_contract_es() -> FuturesContract { None, None, None, + None, + None, 0, 0, ) @@ -350,6 +352,8 @@ pub fn futures_spread_es() -> FuturesSpread { None, None, None, + None, + None, 0, 0, ) @@ -383,6 +387,8 @@ pub fn options_contract_appl() -> OptionsContract { None, None, None, + None, + None, 0, 0, ) @@ -415,6 +421,8 @@ pub fn options_spread() -> OptionsSpread { None, None, None, + None, + None, 0, 0, ) diff --git a/nautilus_core/model/src/instruments/synthetic.rs b/nautilus_core/model/src/instruments/synthetic.rs index 75fbd9c2e636..fe645e194c75 100644 --- a/nautilus_core/model/src/instruments/synthetic.rs +++ b/nautilus_core/model/src/instruments/synthetic.rs @@ -18,7 +18,6 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::{anyhow, Result}; use evalexpr::{ContextWithMutableVariables, HashMapContext, Node, Value}; use nautilus_core::time::UnixNanos; @@ -55,7 +54,7 @@ impl SyntheticInstrument { formula: String, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { let price_increment = Price::new(10f64.powi(-i32::from(price_precision)), price_precision)?; // Extract variables from the component instruments @@ -85,7 +84,7 @@ impl SyntheticInstrument { evalexpr::build_operator_tree(formula).is_ok() } - pub fn change_formula(&mut self, formula: String) -> Result<(), anyhow::Error> { + pub fn change_formula(&mut self, formula: String) -> anyhow::Result<()> { let operator_tree = evalexpr::build_operator_tree(&formula)?; self.formula = formula; self.operator_tree = operator_tree; @@ -95,7 +94,7 @@ impl SyntheticInstrument { /// Calculates the price of the synthetic instrument based on the given component input prices /// provided as a map. #[allow(dead_code)] - pub fn calculate_from_map(&mut self, inputs: &HashMap) -> Result { + pub fn calculate_from_map(&mut self, inputs: &HashMap) -> anyhow::Result { let mut input_values = Vec::new(); for variable in &self.variables { @@ -113,9 +112,9 @@ impl SyntheticInstrument { /// Calculates the price of the synthetic instrument based on the given component input prices /// provided as an array of `f64` values. - pub fn calculate(&mut self, inputs: &[f64]) -> Result { + pub fn calculate(&mut self, inputs: &[f64]) -> anyhow::Result { if inputs.len() != self.variables.len() { - return Err(anyhow!("Invalid number of input values")); + return Err(anyhow::anyhow!("Invalid number of input values")); } for (variable, input) in self.variables.iter().zip(inputs) { @@ -127,7 +126,7 @@ impl SyntheticInstrument { match result { Value::Float(price) => Price::new(price, self.price_precision), - _ => Err(anyhow!( + _ => Err(anyhow::anyhow!( "Failed to evaluate formula to a floating point number" )), } diff --git a/nautilus_core/model/src/orders/base.rs b/nautilus_core/model/src/orders/base.rs index 8926af432c64..9bbd052f1577 100644 --- a/nautilus_core/model/src/orders/base.rs +++ b/nautilus_core/model/src/orders/base.rs @@ -93,6 +93,7 @@ fn order_side_to_fixed(side: OrderSide) -> OrderSideFixed { } } +#[derive(Clone, Debug)] pub enum PassiveOrderType { Limit(LimitOrderType), Stop(StopOrderType), @@ -107,6 +108,7 @@ impl PartialEq for PassiveOrderType { } } +#[derive(Clone, Debug)] pub enum LimitOrderType { Limit(LimitOrder), MarketToLimit(MarketToLimitOrder), @@ -125,6 +127,7 @@ impl PartialEq for LimitOrderType { } } +#[derive(Clone, Debug)] pub enum StopOrderType { StopMarket(StopMarketOrder), StopLimit(StopLimitOrder), @@ -134,6 +137,19 @@ pub enum StopOrderType { TrailingStopLimit(TrailingStopLimitOrder), } +impl PartialEq for StopOrderType { + fn eq(&self, rhs: &Self) -> bool { + match self { + Self::StopMarket(o) => o.client_order_id == rhs.get_client_order_id(), + Self::StopLimit(o) => o.client_order_id == rhs.get_client_order_id(), + Self::MarketIfTouched(o) => o.client_order_id == rhs.get_client_order_id(), + Self::LimitIfTouched(o) => o.client_order_id == rhs.get_client_order_id(), + Self::TrailingStopMarket(o) => o.client_order_id == rhs.get_client_order_id(), + Self::TrailingStopLimit(o) => o.client_order_id == rhs.get_client_order_id(), + } + } +} + pub trait GetClientOrderId { fn get_client_order_id(&self) -> ClientOrderId; } @@ -484,6 +500,11 @@ pub trait Order { fn is_pending_cancel(&self) -> bool { self.status() == OrderStatus::PendingCancel } + + fn is_spawned(&self) -> bool { + self.exec_algorithm_id().is_some() + && self.exec_spawn_id().unwrap() != self.client_order_id() + } } impl From<&T> for OrderInitialized @@ -615,11 +636,11 @@ impl OrderCore { order_type, quantity, time_in_force, - liquidity_side: None, + liquidity_side: Some(LiquiditySide::NoLiquiditySide), is_reduce_only: reduce_only, is_quote_quantity: quote_quantity, - emulation_trigger, - contingency_type, + emulation_trigger: emulation_trigger.or(Some(TriggerType::NoTrigger)), + contingency_type: contingency_type.or(Some(ContingencyType::NoContingency)), order_list_id, linked_order_ids, parent_order_id, @@ -822,6 +843,11 @@ impl OrderCore { pub fn commissions(&self) -> HashMap { self.commissions.clone() } + + #[must_use] + pub fn init_event(&self) -> Option<&OrderEvent> { + self.events.first() + } } //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/orders/default.rs b/nautilus_core/model/src/orders/default.rs index 9f564ae04f57..0dfe0f39b24a 100644 --- a/nautilus_core/model/src/orders/default.rs +++ b/nautilus_core/model/src/orders/default.rs @@ -60,6 +60,7 @@ impl Default for LimitOrder { UUID4::default(), 0, ) + .unwrap() // SAFETY: Valid default values are used } } @@ -95,6 +96,7 @@ impl Default for LimitIfTouchedOrder { UUID4::default(), 0, ) + .unwrap() // SAFETY: Valid default values are used } } @@ -122,7 +124,7 @@ impl Default for MarketOrder { None, None, ) - .unwrap() + .unwrap() // SAFETY: Valid default values are used } } @@ -156,6 +158,7 @@ impl Default for MarketIfTouchedOrder { UUID4::default(), 0, ) + .unwrap() // SAFETY: Valid default values are used } } @@ -186,6 +189,7 @@ impl Default for MarketToLimitOrder { UUID4::default(), 0, ) + .unwrap() // SAFETY: Valid default values are used } } @@ -221,6 +225,7 @@ impl Default for StopLimitOrder { UUID4::default(), 0, ) + .unwrap() // SAFETY: Valid default values are used } } @@ -254,6 +259,7 @@ impl Default for StopMarketOrder { UUID4::default(), 0, ) + .unwrap() // SAFETY: Valid default values are used } } @@ -292,6 +298,7 @@ impl Default for TrailingStopLimitOrder { UUID4::default(), 0, ) + .unwrap() // SAFETY: Valid default values are used } } @@ -327,5 +334,6 @@ impl Default for TrailingStopMarketOrder { UUID4::default(), 0, ) + .unwrap() // SAFETY: Valid default values are used } } diff --git a/nautilus_core/model/src/orders/limit.rs b/nautilus_core/model/src/orders/limit.rs index 6e3ad588490c..36b9f6d1deda 100644 --- a/nautilus_core/model/src/orders/limit.rs +++ b/nautilus_core/model/src/orders/limit.rs @@ -15,10 +15,12 @@ use std::{ collections::HashMap, + fmt::Display, ops::{Deref, DerefMut}, }; use nautilus_core::{time::UnixNanos, uuid::UUID4}; +use serde::{Deserialize, Serialize}; use ustr::Ustr; use super::base::{Order, OrderCore}; @@ -35,9 +37,13 @@ use crate::{ venue::Venue, venue_order_id::VenueOrderId, }, orders::base::OrderError, - types::{price::Price, quantity::Quantity}, + types::{ + price::Price, + quantity::{check_quantity_positive, Quantity}, + }, }; +#[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -52,7 +58,6 @@ pub struct LimitOrder { } impl LimitOrder { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, @@ -80,8 +85,20 @@ impl LimitOrder { tags: Option, init_id: UUID4, ts_init: UnixNanos, - ) -> Self { - Self { + ) -> anyhow::Result { + check_quantity_positive(quantity)?; + if time_in_force == TimeInForce::Gtd { + if expire_time.is_none() { + anyhow::bail!("Condition failed: `expire_time` is required for `GTD` order") + } + if let Some(time) = expire_time { + if time == 0 { + anyhow::bail!("`expire_time` for `GTD` Limit order should be higher then 0") + } + } + } + + Ok(Self { core: OrderCore::new( trader_id, strategy_id, @@ -106,11 +123,11 @@ impl LimitOrder { ts_init, ), price, - expire_time, + expire_time: expire_time.or(Some(0)), is_post_only: post_only, display_qty, trigger_instrument_id, - } + }) } } @@ -128,6 +145,12 @@ impl DerefMut for LimitOrder { } } +impl PartialEq for LimitOrder { + fn eq(&self, other: &Self) -> bool { + self.client_order_id == other.client_order_id + } +} + impl Order for LimitOrder { fn status(&self) -> OrderStatus { self.status @@ -348,6 +371,45 @@ impl Order for LimitOrder { } } +impl Display for LimitOrder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "LimitOrder(\ + {} {} {} {} @ {} {}, \ + status={}, \ + client_order_id={}, \ + venue_order_id={}, \ + position_id={}, \ + exec_algorithm_id={}, \ + exec_spawn_id={}, \ + tags={:?}\ + )", + self.side, + self.quantity.to_formatted_string(), + self.instrument_id, + self.order_type, + self.price, + self.time_in_force, + self.status, + self.client_order_id, + self.venue_order_id.map_or_else( + || "None".to_string(), + |venue_order_id| format!("{venue_order_id}") + ), + self.position_id.map_or_else( + || "None".to_string(), + |position_id| format!("{position_id}") + ), + self.exec_algorithm_id + .map_or_else(|| "None".to_string(), |id| format!("{id}")), + self.exec_spawn_id + .map_or_else(|| "None".to_string(), |id| format!("{id}")), + self.tags + ) + } +} + impl From for LimitOrder { fn from(event: OrderInitialized) -> Self { Self::new( @@ -379,5 +441,66 @@ impl From for LimitOrder { event.event_id, event.ts_event, ) + .unwrap() + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod tests { + use rstest::rstest; + + use crate::{ + enums::{OrderSide, TimeInForce}, + instruments::{currency_pair::CurrencyPair, stubs::*}, + orders::stubs::TestOrderStubs, + types::{price::Price, quantity::Quantity}, + }; + + #[rstest] + fn test_display(audusd_sim: CurrencyPair) { + let order = TestOrderStubs::limit_order( + audusd_sim.id, + OrderSide::Buy, + Price::from("1.00000"), + Quantity::from(100_000), + None, + None, + ); + assert_eq!( + order.to_string(), + "LimitOrder(BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, \ + status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, \ + venue_order_id=None, position_id=None, exec_algorithm_id=None, \ + exec_spawn_id=O-19700101-0000-000-001-1, tags=None)" + ); + } + + #[rstest] + #[should_panic(expected = "Condition failed: invalid `Quantity`, should be positive and was 0")] + fn test_positive_quantity_condition(audusd_sim: CurrencyPair) { + let _ = TestOrderStubs::limit_order( + audusd_sim.id, + OrderSide::Buy, + Price::from("0.8"), + Quantity::from(0), + None, + None, + ); + } + + #[rstest] + #[should_panic(expected = "Condition failed: `expire_time` is required for `GTD` order")] + fn test_correct_expiration_with_time_in_force_gtd(audusd_sim: CurrencyPair) { + let _ = TestOrderStubs::limit_order( + audusd_sim.id, + OrderSide::Buy, + Price::from("0.8"), + Quantity::from(1), + None, + Some(TimeInForce::Gtd), + ); } } diff --git a/nautilus_core/model/src/orders/limit_if_touched.rs b/nautilus_core/model/src/orders/limit_if_touched.rs index 710b4e9c1292..d98ea11ba84f 100644 --- a/nautilus_core/model/src/orders/limit_if_touched.rs +++ b/nautilus_core/model/src/orders/limit_if_touched.rs @@ -37,6 +37,7 @@ use crate::{ types::{price::Price, quantity::Quantity}, }; +#[derive(Clone, Debug)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -55,7 +56,6 @@ pub struct LimitIfTouchedOrder { } impl LimitIfTouchedOrder { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, @@ -85,8 +85,8 @@ impl LimitIfTouchedOrder { tags: Option, init_id: UUID4, ts_init: UnixNanos, - ) -> Self { - Self { + ) -> anyhow::Result { + Ok(Self { core: OrderCore::new( trader_id, strategy_id, @@ -119,7 +119,7 @@ impl LimitIfTouchedOrder { trigger_instrument_id, is_triggered: false, ts_triggered: None, - } + }) } } @@ -394,5 +394,6 @@ impl From for LimitIfTouchedOrder { event.event_id, event.ts_event, ) + .unwrap() // SAFETY: From can panic } } diff --git a/nautilus_core/model/src/orders/market.rs b/nautilus_core/model/src/orders/market.rs index 10a8045c605f..dec7a10ef6b7 100644 --- a/nautilus_core/model/src/orders/market.rs +++ b/nautilus_core/model/src/orders/market.rs @@ -15,11 +15,12 @@ use std::{ collections::HashMap, + fmt::Display, ops::{Deref, DerefMut}, }; -use anyhow::{bail, Result}; use nautilus_core::{time::UnixNanos, uuid::UUID4}; +use serde::{Deserialize, Serialize}; use ustr::Ustr; use super::base::{Order, OrderCore}; @@ -42,6 +43,7 @@ use crate::{ }, }; +#[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -72,10 +74,10 @@ impl MarketOrder { exec_algorithm_params: Option>, exec_spawn_id: Option, tags: Option, - ) -> Result { + ) -> anyhow::Result { check_quantity_positive(quantity)?; if time_in_force == TimeInForce::Gtd { - bail!("{}", "GTD not supported for Market orders"); + anyhow::bail!("{}", "GTD not supported for Market orders"); } Ok(Self { @@ -120,6 +122,12 @@ impl DerefMut for MarketOrder { } } +impl PartialEq for MarketOrder { + fn eq(&self, other: &Self) -> bool { + self.client_order_id == other.client_order_id + } +} + impl Order for MarketOrder { fn status(&self) -> OrderStatus { self.status @@ -332,6 +340,44 @@ impl Order for MarketOrder { } } +impl Display for MarketOrder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "MarketOrder(\ + {} {} {} @ {} {}, \ + status={}, \ + client_order_id={}, \ + venue_order_id={}, \ + position_id={}, \ + exec_algorithm_id={}, \ + exec_spawn_id={}, \ + tags={:?}\ + )", + self.side, + self.quantity.to_formatted_string(), + self.instrument_id, + self.order_type, + self.time_in_force, + self.status, + self.client_order_id, + self.venue_order_id.map_or_else( + || "None".to_string(), + |venue_order_id| format!("{venue_order_id}") + ), + self.position_id.map_or_else( + || "None".to_string(), + |position_id| format!("{position_id}") + ), + self.exec_algorithm_id + .map_or_else(|| "None".to_string(), |id| format!("{id}")), + self.exec_spawn_id + .map_or_else(|| "None".to_string(), |id| format!("{id}")), + self.tags + ) + } +} + impl From for MarketOrder { fn from(event: OrderInitialized) -> Self { Self::new( @@ -355,7 +401,7 @@ impl From for MarketOrder { event.exec_spawn_id, event.tags, ) - .unwrap() + .unwrap() // SAFETY: From can panic } } diff --git a/nautilus_core/model/src/orders/market_if_touched.rs b/nautilus_core/model/src/orders/market_if_touched.rs index c44ebd331cd2..8ed0cdf49dda 100644 --- a/nautilus_core/model/src/orders/market_if_touched.rs +++ b/nautilus_core/model/src/orders/market_if_touched.rs @@ -37,6 +37,7 @@ use crate::{ types::{price::Price, quantity::Quantity}, }; +#[derive(Clone, Debug)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -53,7 +54,6 @@ pub struct MarketIfTouchedOrder { } impl MarketIfTouchedOrder { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, @@ -81,8 +81,8 @@ impl MarketIfTouchedOrder { tags: Option, init_id: UUID4, ts_init: UnixNanos, - ) -> Self { - Self { + ) -> anyhow::Result { + Ok(Self { core: OrderCore::new( trader_id, strategy_id, @@ -113,7 +113,7 @@ impl MarketIfTouchedOrder { trigger_instrument_id, is_triggered: false, ts_triggered: None, - } + }) } } @@ -381,6 +381,6 @@ impl From for MarketIfTouchedOrder { event.tags, event.event_id, event.ts_event, - ) + ).unwrap() // SAFETY: From can panic } } diff --git a/nautilus_core/model/src/orders/market_to_limit.rs b/nautilus_core/model/src/orders/market_to_limit.rs index a5eb5baea726..b32ac9ef8bd3 100644 --- a/nautilus_core/model/src/orders/market_to_limit.rs +++ b/nautilus_core/model/src/orders/market_to_limit.rs @@ -38,6 +38,7 @@ use crate::{ types::{price::Price, quantity::Quantity}, }; +#[derive(Clone, Debug)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -51,7 +52,6 @@ pub struct MarketToLimitOrder { } impl MarketToLimitOrder { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, @@ -76,8 +76,8 @@ impl MarketToLimitOrder { tags: Option, init_id: UUID4, ts_init: UnixNanos, - ) -> Self { - Self { + ) -> anyhow::Result { + Ok(Self { core: OrderCore::new( trader_id, strategy_id, @@ -105,7 +105,7 @@ impl MarketToLimitOrder { expire_time, is_post_only: post_only, display_qty, - } + }) } } @@ -369,5 +369,6 @@ impl From for MarketToLimitOrder { event.event_id, event.ts_event, ) + .unwrap() // SAFETY: From can panic } } diff --git a/nautilus_core/model/src/orders/stop_limit.rs b/nautilus_core/model/src/orders/stop_limit.rs index 7c0e8060ef90..4624ddac0e8f 100644 --- a/nautilus_core/model/src/orders/stop_limit.rs +++ b/nautilus_core/model/src/orders/stop_limit.rs @@ -37,6 +37,7 @@ use crate::{ types::{price::Price, quantity::Quantity}, }; +#[derive(Clone, Debug)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -55,7 +56,6 @@ pub struct StopLimitOrder { } impl StopLimitOrder { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, @@ -85,8 +85,8 @@ impl StopLimitOrder { tags: Option, init_id: UUID4, ts_init: UnixNanos, - ) -> Self { - Self { + ) -> anyhow::Result { + Ok(Self { core: OrderCore::new( trader_id, strategy_id, @@ -119,7 +119,7 @@ impl StopLimitOrder { trigger_instrument_id, is_triggered: false, ts_triggered: None, - } + }) } } @@ -394,5 +394,6 @@ impl From for StopLimitOrder { event.event_id, event.ts_event, ) + .unwrap() // SAFETY: From can panic } } diff --git a/nautilus_core/model/src/orders/stop_market.rs b/nautilus_core/model/src/orders/stop_market.rs index b6e9279990a4..efdaaa0028e8 100644 --- a/nautilus_core/model/src/orders/stop_market.rs +++ b/nautilus_core/model/src/orders/stop_market.rs @@ -38,6 +38,7 @@ use crate::{ types::{price::Price, quantity::Quantity}, }; +#[derive(Clone, Debug)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -54,7 +55,6 @@ pub struct StopMarketOrder { } impl StopMarketOrder { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, @@ -82,8 +82,8 @@ impl StopMarketOrder { tags: Option, init_id: UUID4, ts_init: UnixNanos, - ) -> Self { - Self { + ) -> anyhow::Result { + Ok(Self { core: OrderCore::new( trader_id, strategy_id, @@ -114,7 +114,7 @@ impl StopMarketOrder { trigger_instrument_id, is_triggered: false, ts_triggered: None, - } + }) } } @@ -383,5 +383,6 @@ impl From for StopMarketOrder { event.event_id, event.ts_event, ) + .unwrap() // SAFETY: From can panic } } diff --git a/nautilus_core/model/src/orders/stubs.rs b/nautilus_core/model/src/orders/stubs.rs index 63996d02b2ad..250963637a1e 100644 --- a/nautilus_core/model/src/orders/stubs.rs +++ b/nautilus_core/model/src/orders/stubs.rs @@ -17,9 +17,9 @@ use std::str::FromStr; use nautilus_core::uuid::UUID4; -use super::limit::LimitOrder; +use super::{limit::LimitOrder, stop_market::StopMarketOrder}; use crate::{ - enums::{LiquiditySide, OrderSide, TimeInForce}, + enums::{LiquiditySide, OrderSide, TimeInForce, TriggerType}, events::order::filled::OrderFilled, identifiers::{ account_id::AccountId, @@ -149,7 +149,7 @@ impl TestOrderStubs { let trader = trader_id(); let strategy = strategy_id_ema_cross(); let client_order_id = - client_order_id.unwrap_or(ClientOrderId::from("O-20200814-102234-001-001-1")); + client_order_id.unwrap_or(ClientOrderId::from("O-19700101-0000-000-001-1")); let time_in_force = time_in_force.unwrap_or(TimeInForce::Gtc); LimitOrder::new( trader, @@ -173,10 +173,56 @@ impl TestOrderStubs { None, None, None, + Some(client_order_id), + None, + UUID4::new(), + 12_321_312_321_312, + ) + .unwrap() + } + + #[must_use] + pub fn stop_market_order( + instrument_id: InstrumentId, + order_side: OrderSide, + trigger_price: Price, + quantity: Quantity, + trigger_type: Option, + client_order_id: Option, + time_in_force: Option, + ) -> StopMarketOrder { + let trader = trader_id(); + let strategy = strategy_id_ema_cross(); + let client_order_id = + client_order_id.unwrap_or(ClientOrderId::from("O-19700101-010000-001-001-1")); + let time_in_force = time_in_force.unwrap_or(TimeInForce::Gtc); + StopMarketOrder::new( + trader, + strategy, + instrument_id, + client_order_id, + order_side, + quantity, + trigger_price, + trigger_type.unwrap_or(TriggerType::BidAsk), + time_in_force, + None, + false, + false, + None, + None, + None, + None, + None, + None, + None, + None, + None, None, None, UUID4::new(), 12_321_312_321_312, ) + .unwrap() } } diff --git a/nautilus_core/model/src/orders/trailing_stop_limit.rs b/nautilus_core/model/src/orders/trailing_stop_limit.rs index df58d146842b..d838a190a7ca 100644 --- a/nautilus_core/model/src/orders/trailing_stop_limit.rs +++ b/nautilus_core/model/src/orders/trailing_stop_limit.rs @@ -37,6 +37,7 @@ use crate::{ types::{price::Price, quantity::Quantity}, }; +#[derive(Clone, Debug)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -58,7 +59,6 @@ pub struct TrailingStopLimitOrder { } impl TrailingStopLimitOrder { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, @@ -91,8 +91,8 @@ impl TrailingStopLimitOrder { tags: Option, init_id: UUID4, ts_init: UnixNanos, - ) -> Self { - Self { + ) -> anyhow::Result { + Ok(Self { core: OrderCore::new( trader_id, strategy_id, @@ -128,7 +128,7 @@ impl TrailingStopLimitOrder { trigger_instrument_id, is_triggered: false, ts_triggered: None, - } + }) } } @@ -405,6 +405,6 @@ impl From for TrailingStopLimitOrder { event.tags, event.event_id, event.ts_event, - ) + ).unwrap() // SAFETY: From can panic } } diff --git a/nautilus_core/model/src/orders/trailing_stop_market.rs b/nautilus_core/model/src/orders/trailing_stop_market.rs index 539b96052f74..de3a00c005cc 100644 --- a/nautilus_core/model/src/orders/trailing_stop_market.rs +++ b/nautilus_core/model/src/orders/trailing_stop_market.rs @@ -38,6 +38,7 @@ use crate::{ types::{price::Price, quantity::Quantity}, }; +#[derive(Clone, Debug)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -56,7 +57,6 @@ pub struct TrailingStopMarketOrder { } impl TrailingStopMarketOrder { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, @@ -86,8 +86,8 @@ impl TrailingStopMarketOrder { tags: Option, init_id: UUID4, ts_init: UnixNanos, - ) -> Self { - Self { + ) -> anyhow::Result { + Ok(Self { core: OrderCore::new( trader_id, strategy_id, @@ -120,7 +120,7 @@ impl TrailingStopMarketOrder { trigger_instrument_id, is_triggered: false, ts_triggered: None, - } + }) } } @@ -390,6 +390,6 @@ impl From for TrailingStopMarketOrder { event.tags, event.event_id, event.ts_event, - ) + ).unwrap() // SAFETY: From can panic } } diff --git a/nautilus_core/model/src/position.rs b/nautilus_core/model/src/position.rs index 722b89c71356..e941d724fb52 100644 --- a/nautilus_core/model/src/position.rs +++ b/nautilus_core/model/src/position.rs @@ -19,7 +19,6 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::Result; use nautilus_core::time::UnixNanos; use serde::{Deserialize, Serialize}; @@ -82,7 +81,7 @@ pub struct Position { } impl Position { - pub fn new(instrument: T, fill: OrderFilled) -> Result { + pub fn new(instrument: T, fill: OrderFilled) -> anyhow::Result { assert_eq!(instrument.id(), fill.instrument_id); assert!(fill.position_id.is_some()); assert_ne!(fill.order_side, OrderSide::NoOrderSide); diff --git a/nautilus_core/model/src/python/events/account/state.rs b/nautilus_core/model/src/python/events/account/state.rs index 7c458dfda3d6..618238c9b575 100644 --- a/nautilus_core/model/src/python/events/account/state.rs +++ b/nautilus_core/model/src/python/events/account/state.rs @@ -124,7 +124,6 @@ impl AccountState { #[staticmethod] #[pyo3(name = "from_dict")] pub fn py_from_dict(py: Python<'_>, values: Py) -> PyResult { - // from_dict_pyo3(py, values) let dict = values.as_ref(py); let account_id: &str = dict.get_item("account_id")?.unwrap().extract()?; let account_type: &str = dict.get_item("account_type")?.unwrap().extract::<&str>()?; @@ -164,7 +163,7 @@ impl AccountState { UUID4::from_str(event_id).unwrap(), ts_event, ts_init, - Some(Currency::from_str(base_currency)?), + Some(Currency::from_str(base_currency).map_err(to_pyvalue_err)?), ) .unwrap(); Ok(account) diff --git a/nautilus_core/model/src/python/instruments/futures_contract.rs b/nautilus_core/model/src/python/instruments/futures_contract.rs index 248237f2481c..0a022067dad3 100644 --- a/nautilus_core/model/src/python/instruments/futures_contract.rs +++ b/nautilus_core/model/src/python/instruments/futures_contract.rs @@ -23,7 +23,7 @@ use nautilus_core::{ time::UnixNanos, }; use pyo3::{basic::CompareOp, prelude::*, types::PyDict}; -use rust_decimal::prelude::ToPrimitive; +use rust_decimal::{prelude::ToPrimitive, Decimal}; use ustr::Ustr; use crate::{ @@ -51,6 +51,8 @@ impl FuturesContract { lot_size: Quantity, ts_event: UnixNanos, ts_init: UnixNanos, + margin_init: Option, + margin_maint: Option, max_quantity: Option, min_quantity: Option, max_price: Option, @@ -74,6 +76,8 @@ impl FuturesContract { min_quantity, max_price, min_price, + margin_init, + margin_maint, ts_event, ts_init, ) @@ -195,6 +199,18 @@ impl FuturesContract { self.min_price } + #[getter] + #[pyo3(name = "size_increment")] + fn py_size_increment(&self) -> Quantity { + self.size_increment + } + + #[getter] + #[pyo3(name = "size_precision")] + fn py_size_precision(&self) -> u8 { + self.size_precision + } + #[getter] #[pyo3(name = "ts_event")] fn py_ts_event(&self) -> UnixNanos { @@ -207,6 +223,24 @@ impl FuturesContract { self.ts_init } + #[getter] + #[pyo3(name = "margin_init")] + fn py_margin_init(&self) -> Decimal { + self.margin_init + } + + #[getter] + #[pyo3(name = "margin_maint")] + fn py_margin_maint(&self) -> Decimal { + self.margin_maint + } + + #[getter] + #[pyo3(name = "info")] + fn py_info(&self, py: Python<'_>) -> PyResult { + Ok(PyDict::new(py).into()) + } + #[staticmethod] #[pyo3(name = "from_dict")] fn py_from_dict(py: Python<'_>, values: Py) -> PyResult { @@ -226,8 +260,13 @@ impl FuturesContract { dict.set_item("currency", self.currency.code.to_string())?; dict.set_item("price_precision", self.price_precision)?; dict.set_item("price_increment", self.price_increment.to_string())?; + dict.set_item("size_increment", self.size_increment.to_string())?; + dict.set_item("size_precision", self.size_precision)?; dict.set_item("multiplier", self.multiplier.to_string())?; - dict.set_item("lot_size", self.multiplier.to_string())?; + dict.set_item("lot_size", self.lot_size.to_string())?; + dict.set_item("margin_init", self.margin_init.to_string())?; + dict.set_item("margin_maint", self.margin_maint.to_string())?; + dict.set_item("info", PyDict::new(py))?; dict.set_item("ts_event", self.ts_event)?; dict.set_item("ts_init", self.ts_init)?; match self.max_quantity { diff --git a/nautilus_core/model/src/python/instruments/futures_spread.rs b/nautilus_core/model/src/python/instruments/futures_spread.rs index 770f1111ff8e..8ad1b04ba87f 100644 --- a/nautilus_core/model/src/python/instruments/futures_spread.rs +++ b/nautilus_core/model/src/python/instruments/futures_spread.rs @@ -23,7 +23,7 @@ use nautilus_core::{ time::UnixNanos, }; use pyo3::{basic::CompareOp, prelude::*, types::PyDict}; -use rust_decimal::prelude::ToPrimitive; +use rust_decimal::{prelude::ToPrimitive, Decimal}; use ustr::Ustr; use crate::{ @@ -52,6 +52,8 @@ impl FuturesSpread { lot_size: Quantity, ts_event: UnixNanos, ts_init: UnixNanos, + margin_init: Option, + margin_maint: Option, max_quantity: Option, min_quantity: Option, max_price: Option, @@ -76,6 +78,8 @@ impl FuturesSpread { min_quantity, max_price, min_price, + margin_init, + margin_maint, ts_event, ts_init, ) @@ -167,6 +171,18 @@ impl FuturesSpread { self.price_increment } + #[getter] + #[pyo3(name = "size_increment")] + fn py_size_increment(&self) -> Quantity { + self.size_increment + } + + #[getter] + #[pyo3(name = "size_precision")] + fn py_size_precision(&self) -> u8 { + self.size_precision + } + #[getter] #[pyo3(name = "multiplier")] fn py_multiplier(&self) -> Quantity { @@ -203,6 +219,24 @@ impl FuturesSpread { self.min_price } + #[getter] + #[pyo3(name = "margin_init")] + fn py_margin_init(&self) -> Decimal { + self.margin_init + } + + #[getter] + #[pyo3(name = "margin_maint")] + fn py_margin_maint(&self) -> Decimal { + self.margin_maint + } + + #[getter] + #[pyo3(name = "info")] + fn py_info(&self, py: Python<'_>) -> PyResult { + Ok(PyDict::new(py).into()) + } + #[getter] #[pyo3(name = "ts_event")] fn py_ts_event(&self) -> UnixNanos { @@ -235,8 +269,13 @@ impl FuturesSpread { dict.set_item("currency", self.currency.code.to_string())?; dict.set_item("price_precision", self.price_precision)?; dict.set_item("price_increment", self.price_increment.to_string())?; + dict.set_item("size_increment", self.size_increment.to_string())?; + dict.set_item("size_precision", self.size_precision)?; dict.set_item("multiplier", self.multiplier.to_string())?; - dict.set_item("lot_size", self.multiplier.to_string())?; + dict.set_item("lot_size", self.lot_size.to_string())?; + dict.set_item("margin_init", self.margin_init.to_string())?; + dict.set_item("margin_maint", self.margin_maint.to_string())?; + dict.set_item("info", PyDict::new(py))?; dict.set_item("ts_event", self.ts_event)?; dict.set_item("ts_init", self.ts_init)?; match self.max_quantity { diff --git a/nautilus_core/model/src/python/instruments/options_contract.rs b/nautilus_core/model/src/python/instruments/options_contract.rs index 9fca9890e8b2..dfb2ce90de95 100644 --- a/nautilus_core/model/src/python/instruments/options_contract.rs +++ b/nautilus_core/model/src/python/instruments/options_contract.rs @@ -23,7 +23,7 @@ use nautilus_core::{ time::UnixNanos, }; use pyo3::{basic::CompareOp, prelude::*, types::PyDict}; -use rust_decimal::prelude::ToPrimitive; +use rust_decimal::{prelude::ToPrimitive, Decimal}; use ustr::Ustr; use crate::{ @@ -53,6 +53,8 @@ impl OptionsContract { lot_size: Quantity, ts_event: UnixNanos, ts_init: UnixNanos, + margin_init: Option, + margin_maint: Option, max_quantity: Option, min_quantity: Option, max_price: Option, @@ -78,6 +80,8 @@ impl OptionsContract { min_quantity, max_price, min_price, + margin_init, + margin_maint, ts_event, ts_init, ) @@ -175,6 +179,18 @@ impl OptionsContract { self.price_increment } + #[getter] + #[pyo3(name = "size_increment")] + fn py_size_increment(&self) -> Quantity { + self.size_increment + } + + #[getter] + #[pyo3(name = "size_precision")] + fn py_size_precision(&self) -> u8 { + self.size_precision + } + #[getter] #[pyo3(name = "multiplier")] fn py_multiplier(&self) -> Quantity { @@ -211,6 +227,24 @@ impl OptionsContract { self.min_price } + #[getter] + #[pyo3(name = "margin_init")] + fn py_margin_init(&self) -> Decimal { + self.margin_init + } + + #[getter] + #[pyo3(name = "margin_maint")] + fn py_margin_maint(&self) -> Decimal { + self.margin_maint + } + + #[getter] + #[pyo3(name = "info")] + fn py_info(&self, py: Python<'_>) -> PyResult { + Ok(PyDict::new(py).into()) + } + #[getter] #[pyo3(name = "ts_event")] fn py_ts_event(&self) -> UnixNanos { @@ -244,8 +278,13 @@ impl OptionsContract { dict.set_item("currency", self.currency.code.to_string())?; dict.set_item("price_precision", self.price_precision)?; dict.set_item("price_increment", self.price_increment.to_string())?; + dict.set_item("size_increment", self.size_increment.to_string())?; + dict.set_item("size_precision", self.size_precision)?; dict.set_item("multiplier", self.multiplier.to_string())?; - dict.set_item("lot_size", self.multiplier.to_string())?; + dict.set_item("lot_size", self.lot_size.to_string())?; + dict.set_item("margin_init", self.margin_init.to_string())?; + dict.set_item("margin_maint", self.margin_maint.to_string())?; + dict.set_item("info", PyDict::new(py))?; dict.set_item("ts_event", self.ts_event)?; dict.set_item("ts_init", self.ts_init)?; match self.max_quantity { diff --git a/nautilus_core/model/src/python/instruments/options_spread.rs b/nautilus_core/model/src/python/instruments/options_spread.rs index 0b53fa5c79bd..cc8585fa2d63 100644 --- a/nautilus_core/model/src/python/instruments/options_spread.rs +++ b/nautilus_core/model/src/python/instruments/options_spread.rs @@ -23,7 +23,7 @@ use nautilus_core::{ time::UnixNanos, }; use pyo3::{basic::CompareOp, prelude::*, types::PyDict}; -use rust_decimal::prelude::ToPrimitive; +use rust_decimal::{prelude::ToPrimitive, Decimal}; use ustr::Ustr; use crate::{ @@ -52,6 +52,8 @@ impl OptionsSpread { lot_size: Quantity, ts_event: UnixNanos, ts_init: UnixNanos, + margin_init: Option, + margin_maint: Option, max_quantity: Option, min_quantity: Option, max_price: Option, @@ -72,6 +74,8 @@ impl OptionsSpread { price_increment, multiplier, lot_size, + margin_init, + margin_maint, max_quantity, min_quantity, max_price, @@ -167,6 +171,18 @@ impl OptionsSpread { self.price_increment } + #[getter] + #[pyo3(name = "size_increment")] + fn py_size_increment(&self) -> Quantity { + self.size_increment + } + + #[getter] + #[pyo3(name = "size_precision")] + fn py_size_precision(&self) -> u8 { + self.size_precision + } + #[getter] #[pyo3(name = "multiplier")] fn py_multiplier(&self) -> Quantity { @@ -203,6 +219,24 @@ impl OptionsSpread { self.min_price } + #[getter] + #[pyo3(name = "margin_init")] + fn py_margin_init(&self) -> Decimal { + self.margin_init + } + + #[getter] + #[pyo3(name = "margin_maint")] + fn py_margin_maint(&self) -> Decimal { + self.margin_maint + } + + #[getter] + #[pyo3(name = "info")] + fn py_info(&self, py: Python<'_>) -> PyResult { + Ok(PyDict::new(py).into()) + } + #[getter] #[pyo3(name = "ts_event")] fn py_ts_event(&self) -> UnixNanos { @@ -235,8 +269,13 @@ impl OptionsSpread { dict.set_item("currency", self.currency.code.to_string())?; dict.set_item("price_precision", self.price_precision)?; dict.set_item("price_increment", self.price_increment.to_string())?; + dict.set_item("size_increment", self.size_increment.to_string())?; + dict.set_item("size_precision", self.size_precision)?; dict.set_item("multiplier", self.multiplier.to_string())?; - dict.set_item("lot_size", self.multiplier.to_string())?; + dict.set_item("lot_size", self.lot_size.to_string())?; + dict.set_item("margin_init", self.margin_init.to_string())?; + dict.set_item("margin_maint", self.margin_maint.to_string())?; + dict.set_item("info", PyDict::new(py))?; dict.set_item("ts_event", self.ts_event)?; dict.set_item("ts_init", self.ts_init)?; match self.max_quantity { diff --git a/nautilus_core/model/src/python/orders/limit.rs b/nautilus_core/model/src/python/orders/limit.rs new file mode 100644 index 000000000000..65c735ab666b --- /dev/null +++ b/nautilus_core/model/src/python/orders/limit.rs @@ -0,0 +1,659 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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. +// ------------------------------------------------------------------------------------------------- + +use std::collections::HashMap; + +use nautilus_core::{time::UnixNanos, uuid::UUID4}; +use pyo3::{ + basic::CompareOp, + prelude::*, + types::{PyDict, PyList}, +}; +use ustr::Ustr; + +use crate::{ + enums::{ + ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide, + TimeInForce, TriggerType, + }, + identifiers::{ + client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + trader_id::TraderId, + }, + orders::{ + base::{str_hashmap_to_ustr, Order, OrderCore}, + limit::LimitOrder, + }, + types::{price::Price, quantity::Quantity}, +}; + +#[pymethods] +impl LimitOrder { + #[new] + #[allow(clippy::too_many_arguments)] + fn py_new( + trader_id: TraderId, + strategy_id: StrategyId, + instrument_id: InstrumentId, + client_order_id: ClientOrderId, + order_side: OrderSide, + quantity: Quantity, + price: Price, + time_in_force: TimeInForce, + post_only: bool, + reduce_only: bool, + quote_quantity: bool, + init_id: UUID4, + ts_init: UnixNanos, + expire_time: Option, + display_qty: Option, + emulation_trigger: Option, + trigger_instrument_id: Option, + contingency_type: Option, + order_list_id: Option, + linked_order_ids: Option>, + parent_order_id: Option, + exec_algorithm_id: Option, + exec_algorithm_params: Option>, + exec_spawn_id: Option, + tags: Option, + ) -> PyResult { + let exec_algorithm_params = exec_algorithm_params.map(str_hashmap_to_ustr); + Ok(Self::new( + trader_id, + strategy_id, + instrument_id, + client_order_id, + order_side, + quantity, + price, + time_in_force, + expire_time, + post_only, + reduce_only, + quote_quantity, + display_qty, + emulation_trigger, + trigger_instrument_id, + contingency_type, + order_list_id, + linked_order_ids, + parent_order_id, + exec_algorithm_id, + exec_algorithm_params, + exec_spawn_id, + tags.map(|s| Ustr::from(&s)), + init_id, + ts_init, + ) + .unwrap()) + } + + fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py { + match op { + CompareOp::Eq => self.eq(other).into_py(py), + CompareOp::Ne => self.ne(other).into_py(py), + _ => py.NotImplemented(), + } + } + + fn __str__(&self) -> String { + self.to_string() + } + + fn __repr__(&self) -> String { + self.to_string() + } + + #[getter] + #[pyo3(name = "trader_id")] + fn py_trader_id(&self) -> TraderId { + self.trader_id + } + + #[getter] + #[pyo3(name = "strategy_id")] + fn py_strategy_id(&self) -> StrategyId { + self.strategy_id + } + + #[getter] + #[pyo3(name = "instrument_id")] + fn py_instrument_id(&self) -> InstrumentId { + self.instrument_id + } + + #[getter] + #[pyo3(name = "client_order_id")] + fn py_client_order_id(&self) -> ClientOrderId { + self.client_order_id + } + + #[getter] + #[pyo3(name = "order_type")] + fn py_order_type(&self) -> OrderType { + self.order_type + } + + #[getter] + #[pyo3(name = "side")] + fn py_side(&self) -> OrderSide { + self.side + } + + #[getter] + #[pyo3(name = "quantity")] + fn py_quantity(&self) -> Quantity { + self.quantity + } + + #[getter] + #[pyo3(name = "price")] + fn py_price(&self) -> Price { + self.price + } + + #[getter] + #[pyo3(name = "expire_time")] + fn py_expire_time(&self) -> Option { + self.expire_time + } + + #[getter] + #[pyo3(name = "status")] + fn py_status(&self) -> OrderStatus { + self.status + } + + #[getter] + #[pyo3(name = "time_in_force")] + fn py_time_in_force(&self) -> TimeInForce { + self.time_in_force + } + + #[getter] + #[pyo3(name = "is_post_only")] + fn py_is_post_only(&self) -> bool { + self.is_post_only + } + + #[getter] + #[pyo3(name = "is_reduce_only")] + fn py_is_reduce_only(&self) -> bool { + self.is_reduce_only + } + + #[getter] + #[pyo3(name = "is_quote_quantity")] + fn py_is_quote_quantity(&self) -> bool { + self.is_quote_quantity + } + + #[getter] + #[pyo3(name = "has_price")] + fn py_has_price(&self) -> bool { + true + } + + #[getter] + #[pyo3(name = "has_trigger_price")] + fn py_trigger_price(&self) -> bool { + false + } + + #[getter] + #[pyo3(name = "is_passive")] + fn py_is_passive(&self) -> bool { + true + } + + #[getter] + #[pyo3(name = "is_open")] + fn py_is_open(&self) -> bool { + self.is_open() + } + + #[getter] + #[pyo3(name = "is_closed")] + fn py_is_closed(&self) -> bool { + self.is_closed() + } + + #[getter] + #[pyo3(name = "is_aggressive")] + fn py_is_aggressive(&self) -> bool { + self.is_aggressive() + } + + #[getter] + #[pyo3(name = "is_emulated")] + fn py_is_emulated(&self) -> bool { + self.is_emulated() + } + + #[getter] + #[pyo3(name = "is_active_local")] + fn py_is_active_local(&self) -> bool { + self.is_active_local() + } + + #[getter] + #[pyo3(name = "is_primary")] + fn py_is_primary(&self) -> bool { + self.is_primary() + } + + #[getter] + #[pyo3(name = "is_spawned")] + fn py_is_spawned(&self) -> bool { + self.is_spawned() + } + + #[getter] + #[pyo3(name = "liquidity_side")] + fn py_liquidity_side(&self) -> Option { + self.liquidity_side + } + + #[getter] + #[pyo3(name = "filled_qty")] + fn py_venue_order_id(&self) -> Quantity { + self.filled_qty + } + + #[getter] + #[pyo3(name = "trigger_instrument_id")] + fn py_trigger_instrument_id(&self) -> Option { + self.trigger_instrument_id + } + + #[getter] + #[pyo3(name = "contingency_type")] + fn py_contingency_type(&self) -> Option { + self.contingency_type + } + + #[getter] + #[pyo3(name = "order_list_id")] + fn py_order_list_id(&self) -> Option { + self.order_list_id + } + + #[getter] + #[pyo3(name = "linked_order_ids")] + fn py_linked_order_ids(&self) -> Option> { + self.linked_order_ids.clone() + } + + #[getter] + #[pyo3(name = "parent_order_id")] + fn py_parent_order_id(&self) -> Option { + self.parent_order_id + } + + #[getter] + #[pyo3(name = "exec_algorithm_id")] + fn py_exec_algorithm_id(&self) -> Option { + self.exec_algorithm_id + } + + #[getter] + #[pyo3(name = "exec_algorithm_params")] + fn py_exec_algorithm_params(&self) -> Option> { + self.exec_algorithm_params.clone().map(|x| { + x.into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + }) + } + + #[getter] + #[pyo3(name = "tags")] + fn py_tags(&self) -> Option { + self.tags.map(|x| x.to_string()) + } + + #[getter] + #[pyo3(name = "emulation_trigger")] + fn py_emulation_trigger(&self) -> Option { + self.emulation_trigger + } + + #[getter] + #[pyo3(name = "expire_time_ns")] + fn py_expire_time_ns(&self) -> Option { + self.expire_time + } + + #[getter] + #[pyo3(name = "exec_spawn_id")] + fn py_exec_spawn_id(&self) -> Option { + self.exec_spawn_id + } + + #[getter] + #[pyo3(name = "init_id")] + fn py_init_id(&self) -> UUID4 { + self.init_id + } + + #[getter] + #[pyo3(name = "display_qty")] + fn py_display_qty(&self) -> Option { + self.display_qty + } + + #[getter] + #[pyo3(name = "ts_init")] + fn py_ts_init(&self) -> UnixNanos { + self.ts_init + } + + #[staticmethod] + #[pyo3(name = "opposite_side")] + fn py_opposite_side(side: OrderSide) -> OrderSide { + OrderCore::opposite_side(side) + } + + #[staticmethod] + #[pyo3(name = "closing_side")] + fn py_closing_side(side: PositionSide) -> OrderSide { + OrderCore::closing_side(side) + } + + #[staticmethod] + #[pyo3(name = "from_dict")] + fn py_from_dict(py: Python<'_>, values: Py) -> PyResult { + let dict = values.as_ref(py); + let trader_id = TraderId::from(dict.get_item("trader_id")?.unwrap().extract::<&str>()?); + let strategy_id = + StrategyId::from(dict.get_item("strategy_id")?.unwrap().extract::<&str>()?); + let instrument_id = + InstrumentId::from(dict.get_item("instrument_id")?.unwrap().extract::<&str>()?); + let client_order_id = ClientOrderId::from( + dict.get_item("client_order_id")? + .unwrap() + .extract::<&str>()?, + ); + let order_side = dict + .get_item("side")? + .unwrap() + .extract::<&str>()? + .parse::() + .unwrap(); + let quantity = Quantity::from(dict.get_item("quantity")?.unwrap().extract::<&str>()?); + let price = Price::from(dict.get_item("price")?.unwrap().extract::<&str>()?); + let time_in_force = dict + .get_item("time_in_force")? + .unwrap() + .extract::<&str>()? + .parse::() + .unwrap(); + let expire_time_ns = dict + .get_item("expire_time_ns") + .map(|x| x.and_then(|inner| inner.extract::().ok()))?; + let is_post_only = dict.get_item("is_post_only")?.unwrap().extract::()?; + let is_reduce_only = dict + .get_item("is_reduce_only")? + .unwrap() + .extract::()?; + let is_quote_quantity = dict + .get_item("is_quote_quantity")? + .unwrap() + .extract::()?; + let display_qty = dict + .get_item("display_qty")? + .unwrap() + .extract::>()?; + let emulation_trigger = dict.get_item("emulation_trigger").map(|x| { + x.and_then(|inner| inner.extract::<&str>().unwrap().parse::().ok()) + })?; + let trigger_instrument_id = dict.get_item("trigger_instrument_id").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => item.parse::().ok(), + Err(_) => None, + } + }) + })?; + let contingency_type = dict.get_item("contingency_type").map(|x| { + x.and_then(|inner| { + inner + .extract::<&str>() + .unwrap() + .parse::() + .ok() + }) + })?; + let order_list_id = dict.get_item("order_list_id").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => item.parse::().ok(), + Err(_) => None, + } + }) + })?; + let linked_order_ids = dict.get_item("linked_order_ids").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::>(); + match extracted_str { + Ok(item) => Some( + item.iter() + .map(|x| x.parse::().unwrap()) + .collect(), + ), + Err(_) => None, + } + }) + })?; + let parent_order_id = dict.get_item("parent_order_id").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => item.parse::().ok(), + Err(_) => None, + } + }) + })?; + let exec_algorithm_id = dict.get_item("exec_algorithm_id").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => item.parse::().ok(), + Err(_) => None, + } + }) + })?; + let exec_algorithm_params = dict.get_item("exec_algorithm_params").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::>(); + match extracted_str { + Ok(item) => Some(str_hashmap_to_ustr(item)), + Err(_) => None, + } + }) + })?; + let exec_spawn_id = dict.get_item("exec_spawn_id").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => item.parse::().ok(), + Err(_) => None, + } + }) + })?; + let tags = dict.get_item("tags").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => Some(Ustr::from(item)), + Err(_) => None, + } + }) + })?; + let init_id = dict + .get_item("init_id") + .map(|x| x.and_then(|inner| inner.extract::<&str>().unwrap().parse::().ok()))? + .unwrap(); + let ts_init = dict.get_item("ts_init")?.unwrap().extract::()?; + let limit_order = Self::new( + trader_id, + strategy_id, + instrument_id, + client_order_id, + order_side, + quantity, + price, + time_in_force, + expire_time_ns, + is_post_only, + is_reduce_only, + is_quote_quantity, + display_qty, + emulation_trigger, + trigger_instrument_id, + contingency_type, + order_list_id, + linked_order_ids, + parent_order_id, + exec_algorithm_id, + exec_algorithm_params, + exec_spawn_id, + tags, + init_id, + ts_init, + ) + .unwrap(); + Ok(limit_order) + } + + #[pyo3(name = "to_dict")] + fn py_to_dict(&self, py: Python<'_>) -> PyResult { + let dict = PyDict::new(py); + dict.set_item("trader_id", self.trader_id.to_string())?; + dict.set_item("strategy_id", self.strategy_id.to_string())?; + dict.set_item("instrument_id", self.instrument_id.to_string())?; + dict.set_item("client_order_id", self.client_order_id.to_string())?; + dict.set_item("side", self.side.to_string())?; + dict.set_item("type", self.order_type.to_string())?; + dict.set_item("quantity", self.quantity.to_string())?; + dict.set_item("price", self.price.to_string())?; + dict.set_item("status", self.status.to_string())?; + dict.set_item("time_in_force", self.time_in_force.to_string())?; + dict.set_item("expire_time_ns", self.expire_time)?; + dict.set_item("is_post_only", self.is_post_only)?; + dict.set_item("is_reduce_only", self.is_reduce_only)?; + dict.set_item("is_quote_quantity", self.is_quote_quantity)?; + dict.set_item("filled_qty", self.filled_qty.to_string())?; + dict.set_item("init_id", self.init_id.to_string())?; + dict.set_item("ts_init", self.ts_init)?; + dict.set_item("ts_last", self.ts_last)?; + let commissions_dict = PyDict::new(py); + for (key, value) in &self.commissions { + commissions_dict.set_item(key.code.to_string(), value.to_string())?; + } + dict.set_item("commissions", commissions_dict)?; + self.venue_order_id.map_or_else( + || dict.set_item("venue_order_id", py.None()), + |x| dict.set_item("venue_order_id", x.to_string()), + )?; + self.display_qty.map_or_else( + || dict.set_item("display_qty", py.None()), + |x| dict.set_item("display_qty", x.to_string()), + )?; + self.emulation_trigger.map_or_else( + || dict.set_item("emulation_trigger", py.None()), + |x| dict.set_item("emulation_trigger", x.to_string()), + )?; + self.trigger_instrument_id.map_or_else( + || dict.set_item("trigger_instrument_id", py.None()), + |x| dict.set_item("trigger_instrument_id", x.to_string()), + )?; + self.contingency_type.map_or_else( + || dict.set_item("contingency_type", py.None()), + |x| dict.set_item("contingency_type", x.to_string()), + )?; + self.order_list_id.map_or_else( + || dict.set_item("order_list_id", py.None()), + |x| dict.set_item("order_list_id", x.to_string()), + )?; + self.linked_order_ids.clone().map_or_else( + || dict.set_item("linked_order_ids", py.None()), + |linked_order_ids| { + let linked_order_ids_list = PyList::new( + py, + linked_order_ids + .iter() + .map(std::string::ToString::to_string), + ); + dict.set_item("linked_order_ids", linked_order_ids_list) + }, + )?; + self.parent_order_id.map_or_else( + || dict.set_item("parent_order_id", py.None()), + |x| dict.set_item("parent_order_id", x.to_string()), + )?; + self.exec_algorithm_id.map_or_else( + || dict.set_item("exec_algorithm_id", py.None()), + |x| dict.set_item("exec_algorithm_id", x.to_string()), + )?; + match &self.exec_algorithm_params { + Some(exec_algorithm_params) => { + let py_exec_algorithm_params = PyDict::new(py); + for (key, value) in exec_algorithm_params { + py_exec_algorithm_params.set_item(key.to_string(), value.to_string())?; + } + dict.set_item("exec_algorithm_params", py_exec_algorithm_params)?; + } + None => dict.set_item("exec_algorithm_params", py.None())?, + } + self.exec_spawn_id.map_or_else( + || dict.set_item("exec_spawn_id", py.None()), + |x| dict.set_item("exec_spawn_id", x.to_string()), + )?; + self.tags.map_or_else( + || dict.set_item("tags", py.None()), + |x| dict.set_item("tags", x.to_string()), + )?; + self.account_id.map_or_else( + || dict.set_item("account_id", py.None()), + |x| dict.set_item("account_id", x.to_string()), + )?; + self.slippage.map_or_else( + || dict.set_item("slippage", py.None()), + |x| dict.set_item("slippage", x.to_string()), + )?; + self.position_id.map_or_else( + || dict.set_item("position_id", py.None()), + |x| dict.set_item("position_id", x.to_string()), + )?; + self.liquidity_side.map_or_else( + || dict.set_item("liquidity_side", py.None()), + |x| dict.set_item("liquidity_side", x.to_string()), + )?; + self.last_trade_id.map_or_else( + || dict.set_item("last_trade_id", py.None()), + |x| dict.set_item("last_trade_id", x.to_string()), + )?; + self.avg_px.map_or_else( + || dict.set_item("avg_px", py.None()), + |x| dict.set_item("avg_px", x.to_string()), + )?; + Ok(dict.into()) + } +} diff --git a/nautilus_core/model/src/python/orders/market.rs b/nautilus_core/model/src/python/orders/market.rs index 502b621932c7..968b9da11af0 100644 --- a/nautilus_core/model/src/python/orders/market.rs +++ b/nautilus_core/model/src/python/orders/market.rs @@ -16,7 +16,12 @@ use std::collections::HashMap; use nautilus_core::{python::to_pyvalue_err, time::UnixNanos, uuid::UUID4}; -use pyo3::{pymethods, PyResult}; +use pyo3::{ + basic::CompareOp, + pymethods, + types::{PyDict, PyList}, + IntoPy, Py, PyAny, PyObject, PyResult, Python, +}; use rust_decimal::Decimal; use ustr::Ustr; @@ -37,27 +42,6 @@ use crate::{ #[pymethods] impl MarketOrder { #[new] - #[pyo3(signature = ( - trader_id, - strategy_id, - instrument_id, - client_order_id, - order_side, - quantity, - init_id, - ts_init, - time_in_force=TimeInForce::Gtd, - reduce_only=false, - quote_quantity=false, - contingency_type=None, - order_list_id=None, - linked_order_ids=None, - parent_order_id=None, - exec_algorithm_id=None, - exec_algorithm_params=None, - exec_spawn_id=None, - tags=None, - ))] #[allow(clippy::too_many_arguments)] fn py_new( trader_id: TraderId, @@ -104,16 +88,20 @@ impl MarketOrder { .map_err(to_pyvalue_err) } - #[staticmethod] - #[pyo3(name = "opposite_side")] - fn py_opposite_side(side: OrderSide) -> OrderSide { - OrderCore::opposite_side(side) + fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py { + match op { + CompareOp::Eq => self.eq(other).into_py(py), + CompareOp::Ne => self.ne(other).into_py(py), + _ => py.NotImplemented(), + } } - #[staticmethod] - #[pyo3(name = "closing_side")] - fn py_closing_side(side: PositionSide) -> OrderSide { - OrderCore::closing_side(side) + fn __str__(&self) -> String { + self.to_string() + } + + fn __repr__(&self) -> String { + self.to_string() } #[pyo3(name = "signed_decimal_qty")] @@ -135,37 +123,397 @@ impl MarketOrder { fn py_commissions(&self) -> HashMap { self.commissions() } + #[getter] - fn account_id(&self) -> Option { + #[pyo3(name = "account_id")] + fn py_account_id(&self) -> Option { self.account_id } + #[getter] - fn instrument_id(&self) -> InstrumentId { + #[pyo3(name = "instrument_id")] + fn py_instrument_id(&self) -> InstrumentId { self.instrument_id } + #[getter] - fn trader_id(&self) -> TraderId { + #[pyo3(name = "trader_id")] + fn py_trader_id(&self) -> TraderId { self.trader_id } #[getter] - fn client_order_id(&self) -> ClientOrderId { + #[pyo3(name = "strategy_id")] + fn py_strategy_id(&self) -> StrategyId { + self.strategy_id + } + + #[getter] + #[pyo3(name = "init_id")] + fn py_init_id(&self) -> UUID4 { + self.init_id + } + + #[getter] + #[pyo3(name = "ts_init")] + fn py_ts_init(&self) -> UnixNanos { + self.ts_init + } + + #[getter] + #[pyo3(name = "client_order_id")] + fn py_client_order_id(&self) -> ClientOrderId { self.client_order_id } + + #[getter] + #[pyo3(name = "order_list_id")] + fn py_order_list_id(&self) -> Option { + self.order_list_id + } + + #[getter] + #[pyo3(name = "linked_order_ids")] + fn py_linked_order_ids(&self) -> Option> { + self.linked_order_ids.clone() + } + + #[getter] + #[pyo3(name = "parent_order_id")] + fn py_parent_order_id(&self) -> Option { + self.parent_order_id + } + + #[getter] + #[pyo3(name = "exec_algorithm_id")] + fn py_exec_algorithm_id(&self) -> Option { + self.exec_algorithm_id + } + + #[getter] + #[pyo3(name = "exec_algorithm_params")] + fn py_exec_algorithm_params(&self) -> Option> { + self.exec_algorithm_params.clone().map(|x| { + x.into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + }) + } + + #[getter] + #[pyo3(name = "exec_spawn_id")] + fn py_exec_spawn_id(&self) -> Option { + self.exec_spawn_id + } + + #[getter] + #[pyo3(name = "is_reduce_only")] + fn py_is_reduce_only(&self) -> bool { + self.is_reduce_only + } + #[getter] - fn quantity(&self) -> Quantity { + #[pyo3(name = "is_quote_quantity")] + fn py_is_quote_quantity(&self) -> bool { + self.is_quote_quantity + } + + #[getter] + #[pyo3(name = "contingency_type")] + fn py_contingency_type(&self) -> Option { + self.contingency_type + } + + #[getter] + #[pyo3(name = "quantity")] + fn py_quantity(&self) -> Quantity { self.quantity } + #[getter] - fn side(&self) -> OrderSide { + #[pyo3(name = "side")] + fn py_side(&self) -> OrderSide { self.side } + #[getter] - fn order_type(&self) -> OrderType { + #[pyo3(name = "order_type")] + fn py_order_type(&self) -> OrderType { self.order_type } + #[getter] - fn strategy_id(&self) -> StrategyId { - self.strategy_id + #[pyo3(name = "emulation_trigger")] + fn py_emulation_trigger(&self) -> Option { + self.emulation_trigger.map(|x| x.to_string()) + } + + #[getter] + #[pyo3(name = "time_in_force")] + fn py_time_in_force(&self) -> TimeInForce { + self.time_in_force + } + + #[getter] + #[pyo3(name = "tags")] + fn py_tags(&self) -> Option { + self.tags.map(|x| x.to_string()) + } + + #[staticmethod] + #[pyo3(name = "opposite_side")] + fn py_opposite_side(side: OrderSide) -> OrderSide { + OrderCore::opposite_side(side) + } + + #[staticmethod] + #[pyo3(name = "closing_side")] + fn py_closing_side(side: PositionSide) -> OrderSide { + OrderCore::closing_side(side) + } + + #[pyo3(name = "to_dict")] + fn py_to_dict(&self, py: Python<'_>) -> PyResult { + let dict = PyDict::new(py); + dict.set_item("trader_id", self.trader_id.to_string())?; + dict.set_item("strategy_id", self.strategy_id.to_string())?; + dict.set_item("instrument_id", self.instrument_id.to_string())?; + dict.set_item("client_order_id", self.client_order_id.to_string())?; + dict.set_item("side", self.side.to_string())?; + dict.set_item("type", self.order_type.to_string())?; + dict.set_item("quantity", self.quantity.to_string())?; + dict.set_item("status", self.status.to_string())?; + dict.set_item("time_in_force", self.time_in_force.to_string())?; + dict.set_item("is_reduce_only", self.is_reduce_only)?; + dict.set_item("is_quote_quantity", self.is_quote_quantity)?; + dict.set_item("filled_qty", self.filled_qty.to_string())?; + dict.set_item("init_id", self.init_id.to_string())?; + dict.set_item("ts_init", self.ts_init)?; + dict.set_item("ts_last", self.ts_last)?; + let commissions_dict = PyDict::new(py); + for (key, value) in &self.commissions { + commissions_dict.set_item(key.code.to_string(), value.to_string())?; + } + dict.set_item("commissions", commissions_dict)?; + self.venue_order_id.map_or_else( + || dict.set_item("venue_order_id", py.None()), + |x| dict.set_item("venue_order_id", x.to_string()), + )?; + self.emulation_trigger.map_or_else( + || dict.set_item("emulation_trigger", py.None()), + |x| dict.set_item("emulation_trigger", x.to_string()), + )?; + self.contingency_type.map_or_else( + || dict.set_item("contingency_type", py.None()), + |x| dict.set_item("contingency_type", x.to_string()), + )?; + self.order_list_id.map_or_else( + || dict.set_item("order_list_id", py.None()), + |x| dict.set_item("order_list_id", x.to_string()), + )?; + self.linked_order_ids.clone().map_or_else( + || dict.set_item("linked_order_ids", py.None()), + |linked_order_ids| { + let linked_order_ids_list = PyList::new( + py, + linked_order_ids + .iter() + .map(std::string::ToString::to_string), + ); + dict.set_item("linked_order_ids", linked_order_ids_list) + }, + )?; + self.parent_order_id.map_or_else( + || dict.set_item("parent_order_id", py.None()), + |x| dict.set_item("parent_order_id", x.to_string()), + )?; + self.exec_algorithm_id.map_or_else( + || dict.set_item("exec_algorithm_id", py.None()), + |x| dict.set_item("exec_algorithm_id", x.to_string()), + )?; + match &self.exec_algorithm_params { + Some(exec_algorithm_params) => { + let py_exec_algorithm_params = PyDict::new(py); + for (key, value) in exec_algorithm_params { + py_exec_algorithm_params.set_item(key.to_string(), value.to_string())?; + } + dict.set_item("exec_algorithm_params", py_exec_algorithm_params)?; + } + None => dict.set_item("exec_algorithm_params", py.None())?, + } + self.exec_spawn_id.map_or_else( + || dict.set_item("exec_spawn_id", py.None()), + |x| dict.set_item("exec_spawn_id", x.to_string()), + )?; + self.tags.map_or_else( + || dict.set_item("tags", py.None()), + |x| dict.set_item("tags", x.to_string()), + )?; + self.account_id.map_or_else( + || dict.set_item("account_id", py.None()), + |x| dict.set_item("account_id", x.to_string()), + )?; + self.slippage.map_or_else( + || dict.set_item("slippage", py.None()), + |x| dict.set_item("slippage", x.to_string()), + )?; + self.position_id.map_or_else( + || dict.set_item("position_id", py.None()), + |x| dict.set_item("position_id", x.to_string()), + )?; + self.liquidity_side.map_or_else( + || dict.set_item("liquidity_side", py.None()), + |x| dict.set_item("liquidity_side", x.to_string()), + )?; + self.last_trade_id.map_or_else( + || dict.set_item("last_trade_id", py.None()), + |x| dict.set_item("last_trade_id", x.to_string()), + )?; + self.avg_px.map_or_else( + || dict.set_item("avg_px", py.None()), + |x| dict.set_item("avg_px", x.to_string()), + )?; + Ok(dict.into()) + } + + #[staticmethod] + #[pyo3(name = "from_dict")] + fn py_from_dict(py: Python<'_>, values: Py) -> PyResult { + let dict = values.as_ref(py); + let trader_id = TraderId::from(dict.get_item("trader_id")?.unwrap().extract::<&str>()?); + let strategy_id = + StrategyId::from(dict.get_item("strategy_id")?.unwrap().extract::<&str>()?); + let instrument_id = + InstrumentId::from(dict.get_item("instrument_id")?.unwrap().extract::<&str>()?); + let client_order_id = ClientOrderId::from( + dict.get_item("client_order_id")? + .unwrap() + .extract::<&str>()?, + ); + let order_side = dict + .get_item("side")? + .unwrap() + .extract::<&str>()? + .parse::() + .unwrap(); + let quantity = Quantity::from(dict.get_item("quantity")?.unwrap().extract::<&str>()?); + let time_in_force = dict + .get_item("time_in_force")? + .unwrap() + .extract::<&str>()? + .parse::() + .unwrap(); + let init_id = dict + .get_item("init_id") + .map(|x| x.and_then(|inner| inner.extract::<&str>().unwrap().parse::().ok()))? + .unwrap(); + let ts_init = dict.get_item("ts_init")?.unwrap().extract::()?; + let is_reduce_only = dict + .get_item("is_reduce_only")? + .unwrap() + .extract::()?; + let is_quote_quantity = dict + .get_item("is_quote_quantity")? + .unwrap() + .extract::()?; + let contingency_type = dict.get_item("contingency_type").map(|x| { + x.and_then(|inner| { + inner + .extract::<&str>() + .unwrap() + .parse::() + .ok() + }) + })?; + let order_list_id = dict.get_item("order_list_id").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => item.parse::().ok(), + Err(_) => None, + } + }) + })?; + let linked_order_ids = dict.get_item("linked_order_ids").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::>(); + match extracted_str { + Ok(item) => Some( + item.iter() + .map(|x| x.parse::().unwrap()) + .collect(), + ), + Err(_) => None, + } + }) + })?; + let parent_order_id = dict.get_item("parent_order_id").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => item.parse::().ok(), + Err(_) => None, + } + }) + })?; + let exec_algorithm_id = dict.get_item("exec_algorithm_id").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => item.parse::().ok(), + Err(_) => None, + } + }) + })?; + let exec_algorithm_params = dict.get_item("exec_algorithm_params").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::>(); + match extracted_str { + Ok(item) => Some(str_hashmap_to_ustr(item)), + Err(_) => None, + } + }) + })?; + let exec_spawn_id = dict.get_item("exec_spawn_id").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => item.parse::().ok(), + Err(_) => None, + } + }) + })?; + let tags = dict.get_item("tags").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => Some(Ustr::from(item)), + Err(_) => None, + } + }) + })?; + let market_order = Self::new( + trader_id, + strategy_id, + instrument_id, + client_order_id, + order_side, + quantity, + time_in_force, + init_id, + ts_init, + is_reduce_only, + is_quote_quantity, + contingency_type, + order_list_id, + linked_order_ids, + parent_order_id, + exec_algorithm_id, + exec_algorithm_params, + exec_spawn_id, + tags, + ) + .unwrap(); + Ok(market_order) } } diff --git a/nautilus_core/model/src/python/orders/mod.rs b/nautilus_core/model/src/python/orders/mod.rs index 3ffe64791279..932a30dadb5e 100644 --- a/nautilus_core/model/src/python/orders/mod.rs +++ b/nautilus_core/model/src/python/orders/mod.rs @@ -13,4 +13,5 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +pub mod limit; pub mod market; diff --git a/nautilus_core/model/src/python/types/balance.rs b/nautilus_core/model/src/python/types/balance.rs index 26ed3b8bb93a..9f9337f843cc 100644 --- a/nautilus_core/model/src/python/types/balance.rs +++ b/nautilus_core/model/src/python/types/balance.rs @@ -73,11 +73,11 @@ impl AccountBalance { let free: f64 = free_str.parse::().unwrap(); let locked_str: &str = dict.get_item("locked")?.unwrap().extract()?; let locked: f64 = locked_str.parse::().unwrap(); - let currency = Currency::from_str(currency)?; + let currency = Currency::from_str(currency).map_err(to_pyvalue_err)?; let account_balance = Self::new( - Money::new(total, currency)?, - Money::new(locked, currency)?, - Money::new(free, currency)?, + Money::new(total, currency).map_err(to_pyvalue_err)?, + Money::new(locked, currency).map_err(to_pyvalue_err)?, + Money::new(free, currency).map_err(to_pyvalue_err)?, ) .unwrap(); Ok(account_balance) @@ -160,10 +160,10 @@ impl MarginBalance { let maintenance_str: &str = dict.get_item("maintenance")?.unwrap().extract()?; let maintenance: f64 = maintenance_str.parse::().unwrap(); let instrument_id_str: &str = dict.get_item("instrument_id")?.unwrap().extract()?; - let currency = Currency::from_str(currency)?; + let currency = Currency::from_str(currency).map_err(to_pyvalue_err)?; let account_balance = Self::new( - Money::new(initial, currency)?, - Money::new(maintenance, currency)?, + Money::new(initial, currency).map_err(to_pyvalue_err)?, + Money::new(maintenance, currency).map_err(to_pyvalue_err)?, InstrumentId::from_str(instrument_id_str).unwrap(), ) .unwrap(); diff --git a/nautilus_core/model/src/stubs.rs b/nautilus_core/model/src/stubs.rs index b71abb032446..0d146381343b 100644 --- a/nautilus_core/model/src/stubs.rs +++ b/nautilus_core/model/src/stubs.rs @@ -13,7 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use anyhow::Result; use rstest::fixture; use rust_decimal::prelude::ToPrimitive; @@ -37,7 +36,7 @@ pub fn calculate_commission( last_qty: Quantity, last_px: Price, use_quote_for_inverse: Option, -) -> Result { +) -> anyhow::Result { let liquidity_side = LiquiditySide::Taker; assert_ne!( liquidity_side, diff --git a/nautilus_core/model/src/types/balance.rs b/nautilus_core/model/src/types/balance.rs index f4edefe0dd27..b661a7c9737d 100644 --- a/nautilus_core/model/src/types/balance.rs +++ b/nautilus_core/model/src/types/balance.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use serde::{Deserialize, Serialize}; use crate::{ @@ -36,7 +35,7 @@ pub struct AccountBalance { } impl AccountBalance { - pub fn new(total: Money, locked: Money, free: Money) -> Result { + pub fn new(total: Money, locked: Money, free: Money) -> anyhow::Result { assert!(total == locked + free, "Total balance is not equal to the sum of locked and free balances: {total} != {locked} + {free}" ); @@ -78,7 +77,11 @@ pub struct MarginBalance { } impl MarginBalance { - pub fn new(initial: Money, maintenance: Money, instrument_id: InstrumentId) -> Result { + pub fn new( + initial: Money, + maintenance: Money, + instrument_id: InstrumentId, + ) -> anyhow::Result { Ok(Self { initial, maintenance, diff --git a/nautilus_core/model/src/types/currency.rs b/nautilus_core/model/src/types/currency.rs index c9a1cf9a0670..28d0576e3870 100644 --- a/nautilus_core/model/src/types/currency.rs +++ b/nautilus_core/model/src/types/currency.rs @@ -18,7 +18,6 @@ use std::{ str::FromStr, }; -use anyhow::{anyhow, Result}; use nautilus_core::correctness::check_valid_string; use serde::{Deserialize, Serialize, Serializer}; use ustr::Ustr; @@ -47,9 +46,9 @@ impl Currency { iso4217: u16, name: &str, currency_type: CurrencyType, - ) -> Result { - check_valid_string(code, "`Currency` code")?; - check_valid_string(name, "`Currency` name")?; + ) -> anyhow::Result { + check_valid_string(code, "code")?; + check_valid_string(name, "name")?; check_fixed_precision(precision)?; Ok(Self { @@ -61,8 +60,10 @@ impl Currency { }) } - pub fn register(currency: Self, overwrite: bool) -> Result<()> { - let mut map = CURRENCY_MAP.lock().map_err(|e| anyhow!(e.to_string()))?; + pub fn register(currency: Self, overwrite: bool) -> anyhow::Result<()> { + let mut map = CURRENCY_MAP + .lock() + .map_err(|e| anyhow::anyhow!(e.to_string()))?; if !overwrite && map.contains_key(currency.code.as_str()) { // If overwrite is false and the currency already exists, simply return @@ -74,17 +75,17 @@ impl Currency { Ok(()) } - pub fn is_fiat(code: &str) -> Result { + pub fn is_fiat(code: &str) -> anyhow::Result { let currency = Self::from_str(code)?; Ok(currency.currency_type == CurrencyType::Fiat) } - pub fn is_crypto(code: &str) -> Result { + pub fn is_crypto(code: &str) -> anyhow::Result { let currency = Self::from_str(code)?; Ok(currency.currency_type == CurrencyType::Crypto) } - pub fn is_commodity_backed(code: &str) -> Result { + pub fn is_commodity_backed(code: &str) -> anyhow::Result { let currency = Self::from_str(code)?; Ok(currency.currency_type == CurrencyType::CommodityBacked) } @@ -105,14 +106,14 @@ impl Hash for Currency { impl FromStr for Currency { type Err = anyhow::Error; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> anyhow::Result { let map_guard = CURRENCY_MAP .lock() - .map_err(|e| anyhow!("Failed to acquire lock on `CURRENCY_MAP`: {e}"))?; + .map_err(|e| anyhow::anyhow!("Failed to acquire lock on `CURRENCY_MAP`: {e}"))?; map_guard .get(s) .copied() - .ok_or_else(|| anyhow!("Unknown currency: {s}")) + .ok_or_else(|| anyhow::anyhow!("Unknown currency: {s}")) } } @@ -151,7 +152,7 @@ mod tests { use crate::{enums::CurrencyType, types::currency::Currency}; #[rstest] - #[should_panic(expected = "`Currency` code")] + #[should_panic(expected = "code")] fn test_invalid_currency_code() { let _ = Currency::new("", 2, 840, "United States dollar", CurrencyType::Fiat).unwrap(); } diff --git a/nautilus_core/model/src/types/fixed.rs b/nautilus_core/model/src/types/fixed.rs index 21d4be2be6f3..ff7f5d2fbb74 100644 --- a/nautilus_core/model/src/types/fixed.rs +++ b/nautilus_core/model/src/types/fixed.rs @@ -13,14 +13,12 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use anyhow::{bail, Result}; - pub const FIXED_PRECISION: u8 = 9; pub const FIXED_SCALAR: f64 = 1_000_000_000.0; // 10.0**FIXED_PRECISION -pub fn check_fixed_precision(precision: u8) -> Result<()> { +pub fn check_fixed_precision(precision: u8) -> anyhow::Result<()> { if precision > FIXED_PRECISION { - bail!("Condition failed: `precision` was greater than the maximum `FIXED_PRECISION` (9), was {precision}") + anyhow::bail!("Condition failed: `precision` was greater than the maximum `FIXED_PRECISION` (9), was {precision}") } Ok(()) } diff --git a/nautilus_core/model/src/types/money.rs b/nautilus_core/model/src/types/money.rs index 50048eeff7c5..56b54a8f030b 100644 --- a/nautilus_core/model/src/types/money.rs +++ b/nautilus_core/model/src/types/money.rs @@ -21,8 +21,7 @@ use std::{ str::FromStr, }; -use anyhow::Result; -use nautilus_core::correctness::check_f64_in_range_inclusive; +use nautilus_core::correctness::check_in_range_inclusive_f64; use rust_decimal::Decimal; use serde::{Deserialize, Deserializer, Serialize}; use thousands::Separable; @@ -48,8 +47,8 @@ pub struct Money { } impl Money { - pub fn new(amount: f64, currency: Currency) -> Result { - check_f64_in_range_inclusive(amount, MONEY_MIN, MONEY_MAX, "`Money` amount")?; + pub fn new(amount: f64, currency: Currency) -> anyhow::Result { + check_in_range_inclusive_f64(amount, MONEY_MIN, MONEY_MAX, "amount")?; Ok(Self { raw: f64_to_fixed_i64(amount, currency.precision), diff --git a/nautilus_core/model/src/types/price.rs b/nautilus_core/model/src/types/price.rs index 116ca2d5a381..f6173658af76 100644 --- a/nautilus_core/model/src/types/price.rs +++ b/nautilus_core/model/src/types/price.rs @@ -21,8 +21,7 @@ use std::{ str::FromStr, }; -use anyhow::Result; -use nautilus_core::{correctness::check_f64_in_range_inclusive, parsing::precision_from_str}; +use nautilus_core::{correctness::check_in_range_inclusive_f64, parsing::precision_from_str}; use rust_decimal::Decimal; use serde::{Deserialize, Deserializer, Serialize}; use thousands::Separable; @@ -51,8 +50,8 @@ pub struct Price { } impl Price { - pub fn new(value: f64, precision: u8) -> Result { - check_f64_in_range_inclusive(value, PRICE_MIN, PRICE_MAX, "`Price` value")?; + pub fn new(value: f64, precision: u8) -> anyhow::Result { + check_in_range_inclusive_f64(value, PRICE_MIN, PRICE_MAX, "value")?; check_fixed_precision(precision)?; Ok(Self { @@ -61,7 +60,7 @@ impl Price { }) } - pub fn from_raw(raw: i64, precision: u8) -> Result { + pub fn from_raw(raw: i64, precision: u8) -> anyhow::Result { check_fixed_precision(precision)?; Ok(Self { raw, precision }) } diff --git a/nautilus_core/model/src/types/quantity.rs b/nautilus_core/model/src/types/quantity.rs index 545dd740a0fd..0f45df8d5031 100644 --- a/nautilus_core/model/src/types/quantity.rs +++ b/nautilus_core/model/src/types/quantity.rs @@ -21,8 +21,7 @@ use std::{ str::FromStr, }; -use anyhow::{bail, Result}; -use nautilus_core::{correctness::check_f64_in_range_inclusive, parsing::precision_from_str}; +use nautilus_core::{correctness::check_in_range_inclusive_f64, parsing::precision_from_str}; use rust_decimal::Decimal; use serde::{Deserialize, Deserializer, Serialize}; use thousands::Separable; @@ -45,8 +44,8 @@ pub struct Quantity { } impl Quantity { - pub fn new(value: f64, precision: u8) -> Result { - check_f64_in_range_inclusive(value, QUANTITY_MIN, QUANTITY_MAX, "`Quantity` value")?; + pub fn new(value: f64, precision: u8) -> anyhow::Result { + check_in_range_inclusive_f64(value, QUANTITY_MIN, QUANTITY_MAX, "value")?; check_fixed_precision(precision)?; Ok(Self { @@ -55,7 +54,7 @@ impl Quantity { }) } - pub fn from_raw(raw: u64, precision: u8) -> Result { + pub fn from_raw(raw: u64, precision: u8) -> anyhow::Result { check_fixed_precision(precision)?; Ok(Self { raw, precision }) } @@ -278,9 +277,9 @@ impl<'de> Deserialize<'de> for Quantity { } } -pub fn check_quantity_positive(value: Quantity) -> Result<()> { +pub fn check_quantity_positive(value: Quantity) -> anyhow::Result<()> { if !value.is_positive() { - bail!("Condition failed: invalid `Quantity`, should be positive and was {value}") + anyhow::bail!("Condition failed: invalid `Quantity`, should be positive and was {value}") } Ok(()) } diff --git a/nautilus_core/network/Cargo.toml b/nautilus_core/network/Cargo.toml index 38082c589211..ef7832fbf1a7 100644 --- a/nautilus_core/network/Cargo.toml +++ b/nautilus_core/network/Cargo.toml @@ -23,7 +23,7 @@ futures-util = "0.3.30" http = "1.1.0" hyper = "1.2.0" nonzero_ext = "0.3.0" -reqwest = "0.11.26" +reqwest = "0.11.27" tokio-tungstenite = { path = "./tokio-tungstenite", features = ["rustls-tls-native-roots"] } [dev-dependencies] @@ -34,9 +34,9 @@ axum = "0.7.4" tracing-test = "0.2.4" [features] +default = ["python"] extension-module = [ "pyo3/extension-module", "nautilus-core/extension-module", ] python = ["pyo3", "pyo3-asyncio"] -default = ["python"] diff --git a/nautilus_core/persistence/Cargo.toml b/nautilus_core/persistence/Cargo.toml index b5211c278091..b1f7d2abba58 100644 --- a/nautilus_core/persistence/Cargo.toml +++ b/nautilus_core/persistence/Cargo.toml @@ -34,6 +34,7 @@ quickcheck_macros = "1" procfs = "0.16.0" [features] +default = ["ffi", "python"] extension-module = [ "pyo3/extension-module", "nautilus-core/extension-module", @@ -41,7 +42,6 @@ extension-module = [ ] ffi = ["nautilus-core/ffi", "nautilus-model/ffi"] python = ["pyo3", "nautilus-core/python", "nautilus-model/python"] -default = ["ffi", "python"] [[bench]] name = "bench_persistence" diff --git a/nautilus_core/persistence/src/db/database.rs b/nautilus_core/persistence/src/db/database.rs index 3a917c072944..43630b1bd370 100644 --- a/nautilus_core/persistence/src/db/database.rs +++ b/nautilus_core/persistence/src/db/database.rs @@ -15,7 +15,6 @@ use std::{path::Path, str::FromStr}; -use anyhow::Result; use sqlx::{ any::{install_default_drivers, AnyConnectOptions}, sqlite::SqliteConnectOptions, @@ -108,7 +107,7 @@ impl Database { } } -pub async fn init_db_schema(db: &Database, schema_dir: &str) -> Result<()> { +pub async fn init_db_schema(db: &Database, schema_dir: &str) -> anyhow::Result<()> { // scan all the files in the current directory let mut sql_files = std::fs::read_dir(schema_dir)?.collect::, std::io::Error>>()?; diff --git a/nautilus_core/pyo3/Cargo.toml b/nautilus_core/pyo3/Cargo.toml index 19ddeeb377d4..7be3033864da 100644 --- a/nautilus_core/pyo3/Cargo.toml +++ b/nautilus_core/pyo3/Cargo.toml @@ -23,6 +23,7 @@ nautilus-persistence = { path = "../persistence" , features = ["python"] } pyo3 = { workspace = true } [features] +default = [] extension-module = [ "pyo3/extension-module", "nautilus-accounting/extension-module", @@ -41,4 +42,3 @@ ffi = [ "nautilus-model/ffi", "nautilus-persistence/ffi", ] -default = [] diff --git a/nautilus_core/rust-toolchain.toml b/nautilus_core/rust-toolchain.toml index 8299b1d792e7..2a1ad66785f1 100644 --- a/nautilus_core/rust-toolchain.toml +++ b/nautilus_core/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -version = "1.76.0" +version = "1.77.0" channel = "stable" diff --git a/nautilus_trader/adapters/binance/common/execution.py b/nautilus_trader/adapters/binance/common/execution.py index 6682acfbcf90..3f8423a9b6f9 100644 --- a/nautilus_trader/adapters/binance/common/execution.py +++ b/nautilus_trader/adapters/binance/common/execution.py @@ -30,6 +30,7 @@ from nautilus_trader.adapters.binance.config import BinanceExecClientConfig from nautilus_trader.adapters.binance.http.account import BinanceAccountHttpAPI from nautilus_trader.adapters.binance.http.client import BinanceHttpClient +from nautilus_trader.adapters.binance.http.error import BinanceClientError from nautilus_trader.adapters.binance.http.error import BinanceError from nautilus_trader.adapters.binance.http.market import BinanceMarketHttpAPI from nautilus_trader.adapters.binance.http.user import BinanceUserDataHttpAPI @@ -283,19 +284,23 @@ async def _ping_listen_keys(self) -> None: while True: self._log.debug( f"Scheduled `ping_listen_keys` to run in " - f"{self._ping_listen_keys_interval}s.", + f"{self._ping_listen_keys_interval}s", ) await asyncio.sleep(self._ping_listen_keys_interval) if self._listen_key: - self._log.debug(f"Pinging WebSocket listen key {self._listen_key}...") - await self._http_user.keepalive_listen_key(listen_key=self._listen_key) + self._log.debug(f"Pinging WebSocket listen key {self._listen_key}") + try: + await self._http_user.keepalive_listen_key(listen_key=self._listen_key) + except BinanceClientError as ex: + # We may see this if an old listen key was used for the ping + self._log.error(f"Error pinging listen key: {ex}") except asyncio.CancelledError: - self._log.debug("Canceled `ping_listen_keys` task.") + self._log.debug("Canceled `ping_listen_keys` task") async def _disconnect(self) -> None: # Cancel tasks if self._ping_listen_keys_task: - self._log.debug("Canceling `ping_listen_keys` task...") + self._log.debug("Canceling `ping_listen_keys` task") self._ping_listen_keys_task.cancel() self._ping_listen_keys_task = None diff --git a/nautilus_trader/adapters/databento/data.py b/nautilus_trader/adapters/databento/data.py index e00bfbebc478..4b8285160d89 100644 --- a/nautilus_trader/adapters/databento/data.py +++ b/nautilus_trader/adapters/databento/data.py @@ -387,7 +387,7 @@ async def _subscribe_imbalance(self, data_type: DataType) -> None: live_client = self._get_live_client(dataset) live_client.subscribe( schema=DatabentoSchema.IMBALANCE.value, - symbols=instrument_id.symbol.value, + symbols=[instrument_id.symbol.value], ) await self._check_live_client_started(dataset, live_client) except asyncio.CancelledError: @@ -401,7 +401,7 @@ async def _subscribe_statistics(self, data_type: DataType) -> None: live_client = self._get_live_client(dataset) live_client.subscribe( schema=DatabentoSchema.STATISTICS.value, - symbols=instrument_id.symbol.value, + symbols=[instrument_id.symbol.value], ) await self._check_live_client_started(dataset, live_client) except asyncio.CancelledError: @@ -417,7 +417,7 @@ async def _subscribe_instrument(self, instrument_id: InstrumentId) -> None: live_client = self._get_live_client(dataset) live_client.subscribe( schema=DatabentoSchema.DEFINITION.value, - symbols=instrument_id.symbol.value, + symbols=[instrument_id.symbol.value], ) await self._check_live_client_started(dataset, live_client) except asyncio.CancelledError: @@ -432,7 +432,7 @@ async def _subscribe_parent_symbols( live_client = self._get_live_client(dataset) live_client.subscribe( schema=DatabentoSchema.DEFINITION.value, - symbols=",".join(sorted(parent_symbols)), + symbols=sorted(parent_symbols), stype_in="parent", ) await self._check_live_client_started(dataset, live_client) @@ -448,7 +448,7 @@ async def _subscribe_instrument_ids( live_client = self._get_live_client(dataset) live_client.subscribe( schema=DatabentoSchema.DEFINITION.value, - symbols=",".join(sorted([i.symbol.value for i in instrument_ids])), + symbols=[i.symbol.value for i in instrument_ids], ) await self._check_live_client_started(dataset, live_client) except asyncio.CancelledError: @@ -533,7 +533,7 @@ async def _subscribe_order_book_deltas_batch( live_client.subscribe( schema=DatabentoSchema.MBO.value, - symbols=",".join(sorted([i.symbol.value for i in instrument_ids])), + symbols=[i.symbol.value for i in instrument_ids], start=0, # Replay from start of weekly session ) @@ -578,7 +578,7 @@ async def _subscribe_order_book_snapshots( live_client = self._get_live_client(dataset) live_client.subscribe( schema=schema, - symbols=",".join(sorted([instrument_id.symbol.value])), + symbols=[instrument_id.symbol.value], ) await self._check_live_client_started(dataset, live_client) except asyncio.CancelledError: @@ -592,7 +592,7 @@ async def _subscribe_quote_ticks(self, instrument_id: InstrumentId) -> None: live_client = self._get_live_client(dataset) live_client.subscribe( schema=DatabentoSchema.MBP_1.value, - symbols=",".join(sorted([instrument_id.symbol.value])), + symbols=[instrument_id.symbol.value], ) # Add trade tick subscriptions for instrument (MBP-1 data includes trades) @@ -613,7 +613,7 @@ async def _subscribe_trade_ticks(self, instrument_id: InstrumentId) -> None: live_client = self._get_live_client(dataset) live_client.subscribe( schema=DatabentoSchema.TRADES.value, - symbols=instrument_id.symbol.value, + symbols=[instrument_id.symbol.value], ) await self._check_live_client_started(dataset, live_client) except asyncio.CancelledError: @@ -632,7 +632,7 @@ async def _subscribe_bars(self, bar_type: BarType) -> None: live_client = self._get_live_client(dataset) live_client.subscribe( schema=schema.value, - symbols=bar_type.instrument_id.symbol.value, + symbols=[bar_type.instrument_id.symbol.value], ) await self._check_live_client_started(dataset, live_client) except asyncio.CancelledError: @@ -713,7 +713,7 @@ async def _request_imbalance(self, data_type: DataType, correlation_id: UUID4) - pyo3_imbalances = await self._http_client.get_range_imbalance( dataset=dataset, - symbols=instrument_id.symbol.value, + symbols=[instrument_id.symbol.value], start=start.value, end=end.value, ) @@ -743,7 +743,7 @@ async def _request_statistics(self, data_type: DataType, correlation_id: UUID4) pyo3_statistics = await self._http_client.get_range_statistics( dataset=dataset, - symbols=instrument_id.symbol.value, + symbols=[instrument_id.symbol.value], start=start.value, end=end.value, ) @@ -775,7 +775,7 @@ async def _request_instrument( pyo3_instruments = await self._http_client.get_range_instruments( dataset=dataset, - symbols=ALL_SYMBOLS, + symbols=[instrument_id.symbol.value], start=start.value, end=end.value, ) @@ -808,7 +808,7 @@ async def _request_instruments( pyo3_instruments = await self._http_client.get_range_instruments( dataset=dataset, - symbols=ALL_SYMBOLS, + symbols=[ALL_SYMBOLS], start=start.value, end=end.value, ) @@ -848,7 +848,7 @@ async def _request_quote_ticks( pyo3_quotes = await self._http_client.get_range_quotes( dataset=dataset, - symbols=instrument_id.symbol.value, + symbols=[instrument_id.symbol.value], start=start.value, end=end.value, ) @@ -888,7 +888,7 @@ async def _request_trade_ticks( pyo3_trades = await self._http_client.get_range_trades( dataset=dataset, - symbols=instrument_id.symbol.value, + symbols=[instrument_id.symbol.value], start=(start or available_end - pd.Timedelta(days=1)).value, end=(end or available_end).value, ) @@ -928,7 +928,7 @@ async def _request_bars( pyo3_bars = await self._http_client.get_range_bars( dataset=dataset, - symbols=bar_type.instrument_id.symbol.value, + symbols=[bar_type.instrument_id.symbol.value], aggregation=nautilus_pyo3.BarAggregation( bar_aggregation_to_str(bar_type.spec.aggregation), ), diff --git a/nautilus_trader/adapters/databento/loaders.py b/nautilus_trader/adapters/databento/loaders.py index cff1127e5039..91b6e54dece8 100644 --- a/nautilus_trader/adapters/databento/loaders.py +++ b/nautilus_trader/adapters/databento/loaders.py @@ -47,7 +47,7 @@ class DatabentoDataLoader: References ---------- - https://docs.databento.com/knowledge-base/new-users/dbn-encoding + https://databento.com/docs/knowledge-base/new-users/dbn-encoding """ diff --git a/nautilus_trader/adapters/databento/providers.py b/nautilus_trader/adapters/databento/providers.py index 583939692d29..caff1c893ad5 100644 --- a/nautilus_trader/adapters/databento/providers.py +++ b/nautilus_trader/adapters/databento/providers.py @@ -137,7 +137,7 @@ def receive_instruments(pyo3_instrument: Any) -> None: live_client.subscribe( schema=DatabentoSchema.DEFINITION.value, - symbols=",".join(sorted([i.symbol.value for i in instrument_ids])), + symbols=sorted([i.symbol.value for i in instrument_ids]), start=0, # From start of current week (latest definitions) ) @@ -146,7 +146,7 @@ def receive_instruments(pyo3_instrument: Any) -> None: live_client.subscribe( schema=DatabentoSchema.DEFINITION.value, stype_in="parent", - symbols=",".join(parent_symbols), + symbols=parent_symbols, start=0, # From start of current week (latest definitions) ) @@ -236,7 +236,7 @@ async def get_range( pyo3_instruments = await self._http_client.get_range_instruments( dataset=dataset, - symbols=ALL_SYMBOLS, + symbols=[ALL_SYMBOLS], start=pd.Timestamp(start, tz=pytz.utc).value, end=pd.Timestamp(end, tz=pytz.utc).value if end is not None else None, ) diff --git a/nautilus_trader/adapters/databento/publishers.json b/nautilus_trader/adapters/databento/publishers.json index f9f45ff0c837..cbd2492e6c9b 100644 --- a/nautilus_trader/adapters/databento/publishers.json +++ b/nautilus_trader/adapters/databento/publishers.json @@ -364,5 +364,119 @@ "dataset": "OPRA.PILLAR", "venue": "SPHR", "description": "OPRA - MIAX Sapphire" + }, + { + "publisher_id": 62, + "dataset": "DBEQ.MAX", + "venue": "XCHI", + "description": "DBEQ Max - NYSE Chicago" + }, + { + "publisher_id": 63, + "dataset": "DBEQ.MAX", + "venue": "XCIS", + "description": "DBEQ Max - NYSE National" + }, + { + "publisher_id": 64, + "dataset": "DBEQ.MAX", + "venue": "IEXG", + "description": "DBEQ Max - IEX" + }, + { + "publisher_id": 65, + "dataset": "DBEQ.MAX", + "venue": "EPRL", + "description": "DBEQ Max - MIAX Pearl" + }, + { + "publisher_id": 66, + "dataset": "DBEQ.MAX", + "venue": "XNAS", + "description": "DBEQ Max - Nasdaq" + }, + { + "publisher_id": 67, + "dataset": "DBEQ.MAX", + "venue": "XNYS", + "description": "DBEQ Max - NYSE" + }, + { + "publisher_id": 68, + "dataset": "DBEQ.MAX", + "venue": "FINN", + "description": "DBEQ Max - FINRA/NYSE TRF" + }, + { + "publisher_id": 69, + "dataset": "DBEQ.MAX", + "venue": "FINY", + "description": "DBEQ Max - FINRA/Nasdaq TRF Carteret" + }, + { + "publisher_id": 70, + "dataset": "DBEQ.MAX", + "venue": "FINC", + "description": "DBEQ Max - FINRA/Nasdaq TRF Chicago" + }, + { + "publisher_id": 71, + "dataset": "DBEQ.MAX", + "venue": "BATS", + "description": "DBEQ Max - CBOE BZX" + }, + { + "publisher_id": 72, + "dataset": "DBEQ.MAX", + "venue": "BATY", + "description": "DBEQ Max - CBOE BYX" + }, + { + "publisher_id": 73, + "dataset": "DBEQ.MAX", + "venue": "EDGA", + "description": "DBEQ Max - CBOE EDGA" + }, + { + "publisher_id": 74, + "dataset": "DBEQ.MAX", + "venue": "EDGX", + "description": "DBEQ Max - CBOE EDGX" + }, + { + "publisher_id": 75, + "dataset": "DBEQ.MAX", + "venue": "XBOS", + "description": "DBEQ Max - Nasdaq BX" + }, + { + "publisher_id": 76, + "dataset": "DBEQ.MAX", + "venue": "XPSX", + "description": "DBEQ Max - Nasdaq PSX" + }, + { + "publisher_id": 77, + "dataset": "DBEQ.MAX", + "venue": "MEMX", + "description": "DBEQ Max - MEMX" + }, + { + "publisher_id": 78, + "dataset": "DBEQ.MAX", + "venue": "XASE", + "description": "DBEQ Max - NYSE American" + }, + { + "publisher_id": 79, + "dataset": "DBEQ.MAX", + "venue": "ARCX", + "description": "DBEQ Max - NYSE Arca" + }, + { + "publisher_id": 80, + "dataset": "DBEQ.MAX", + "venue": "LTSE", + "description": "DBEQ Max - Long-Term Stock Exchange" } ] diff --git a/nautilus_trader/adapters/interactive_brokers/client/account.py b/nautilus_trader/adapters/interactive_brokers/client/account.py index f5f1bd887feb..f90084a9c37d 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/account.py +++ b/nautilus_trader/adapters/interactive_brokers/client/account.py @@ -17,7 +17,6 @@ from decimal import Decimal from ibapi.account_summary_tags import AccountSummaryTags -from ibapi.utils import current_fn_name from nautilus_trader.adapters.interactive_brokers.client.common import BaseMixin from nautilus_trader.adapters.interactive_brokers.client.common import IBPosition @@ -134,10 +133,9 @@ async def get_positions(self, account_id: str) -> list[Position] | None: positions.append(position) return positions - # -- EWrapper overrides ----------------------------------------------------------------------- - - def accountSummary( + def process_account_summary( self, + *, req_id: int, account_id: str, tag: str, @@ -147,26 +145,26 @@ def accountSummary( """ Receive account information. """ - self.logAnswer(current_fn_name(), vars()) name = f"accountSummary-{account_id}" if handler := self._event_subscriptions.get(name, None): handler(tag, value, currency) - def managedAccounts(self, accounts_list: str) -> None: + def process_managed_accounts(self, *, accounts_list: str) -> None: """ Receive a comma-separated string with the managed account ids. Occurs automatically on initial API client connection. """ - self.logAnswer(current_fn_name(), vars()) self._account_ids = {a for a in accounts_list.split(",") if a} - if self._next_valid_order_id >= 0 and not self._is_ib_ready.is_set(): - self._log.info("`is_ib_ready` set by managedAccounts", LogColor.BLUE) - self._is_ib_ready.set() + self._log.debug(f"Managed accounts set: {self._account_ids}") + if self._next_valid_order_id >= 0 and not self._is_ib_connected.is_set(): + self._log.debug("`_is_ib_connected` set by `managedAccounts`.", LogColor.BLUE) + self._is_ib_connected.set() - def position( + def process_position( self, + *, account_id: str, contract: IBContract, position: Decimal, @@ -175,14 +173,12 @@ def position( """ Provide the portfolio's open positions. """ - self.logAnswer(current_fn_name(), vars()) if request := self._requests.get(name="OpenPositions"): request.result.append(IBPosition(account_id, contract, position, avg_cost)) - def positionEnd(self) -> None: + def process_position_end(self) -> None: """ Indicate that all the positions have been transmitted. """ - self.logAnswer(current_fn_name(), vars()) if request := self._requests.get(name="OpenPositions"): self._end_request(request.req_id) diff --git a/nautilus_trader/adapters/interactive_brokers/client/client.py b/nautilus_trader/adapters/interactive_brokers/client/client.py index a29358bd0460..4d1550cd6e43 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/client.py +++ b/nautilus_trader/adapters/interactive_brokers/client/client.py @@ -15,6 +15,7 @@ import asyncio import functools +import os from collections.abc import Callable from collections.abc import Coroutine from inspect import iscoroutinefunction @@ -29,7 +30,6 @@ from ibapi.errors import BAD_LENGTH from ibapi.execution import Execution from ibapi.utils import current_fn_name -from ibapi.wrapper import EWrapper # fmt: off from nautilus_trader.adapters.interactive_brokers.client.account import InteractiveBrokersClientAccountMixin @@ -42,16 +42,14 @@ from nautilus_trader.adapters.interactive_brokers.client.error import InteractiveBrokersClientErrorMixin from nautilus_trader.adapters.interactive_brokers.client.market_data import InteractiveBrokersClientMarketDataMixin from nautilus_trader.adapters.interactive_brokers.client.order import InteractiveBrokersClientOrderMixin +from nautilus_trader.adapters.interactive_brokers.client.wrapper import InteractiveBrokersEWrapper from nautilus_trader.adapters.interactive_brokers.common import IB_VENUE -from nautilus_trader.adapters.interactive_brokers.common import IBContract -from nautilus_trader.adapters.interactive_brokers.parsing.instruments import instrument_id_to_ib_contract from nautilus_trader.cache.cache import Cache from nautilus_trader.common.component import Component from nautilus_trader.common.component import LiveClock from nautilus_trader.common.component import MessageBus from nautilus_trader.common.enums import LogColor from nautilus_trader.model.identifiers import ClientId -from nautilus_trader.model.identifiers import InstrumentId # fmt: on @@ -65,7 +63,6 @@ class InteractiveBrokersClient( InteractiveBrokersClientOrderMixin, InteractiveBrokersClientContractMixin, InteractiveBrokersClientErrorMixin, - EWrapper, ): """ A client component that interfaces with the Interactive Brokers TWS or Gateway. @@ -92,9 +89,8 @@ def __init__( component_id=ClientId(f"{IB_VENUE.value}-{client_id:03d}"), component_name=f"{type(self).__name__}-{client_id:03d}", msgbus=msgbus, - # TODO: Config needs to be fully formed earlier than this - # config={"name": f"{type(self).__name__}-{client_id:03d}", "client_id": client_id}, ) + # Config self._loop = loop self._cache = cache @@ -103,47 +99,48 @@ def __init__( self._client_id = client_id # TWS API - self._eclient: EClient = EClient(wrapper=self) + self._eclient: EClient = EClient( + wrapper=InteractiveBrokersEWrapper( + nautilus_logger=self._log, + client=self, + ), + ) + + # EClient Overrides + self._eclient.sendMsg = self.sendMsg + self._eclient.logRequest = self.logRequest # Tasks - self._watch_dog_task: asyncio.Task | None = None + self._connection_watchdog_task: asyncio.Task | None = None self._tws_incoming_msg_reader_task: asyncio.Task | None = None - self._internal_msg_queue_task: asyncio.Task | None = None + self._internal_msg_queue_processor_task: asyncio.Task | None = None self._internal_msg_queue: asyncio.Queue = asyncio.Queue() # Event flags self._is_client_ready: asyncio.Event = asyncio.Event() - self._is_ib_ready: asyncio.Event = asyncio.Event() # Connectivity between IB and TWS + self._is_ib_connected: asyncio.Event = asyncio.Event() # Hot caches self.registered_nautilus_clients: set = set() self._event_subscriptions: dict[str, Callable] = {} - # Reset - self._reset() - self._request_id_seq: int = 10000 - # Subscriptions self._requests = Requests() self._subscriptions = Subscriptions() - # Overrides for EClient - self._eclient.sendMsg = self.sendMsg - self._eclient.logRequest = self.logRequest - # AccountMixin self._account_ids: set[str] = set() # ConnectionMixin - self._connection_attempt_counter: int = 0 - self._contract_for_probe: IBContract = instrument_id_to_ib_contract( - InstrumentId.from_str("EUR/CHF.IDEALPRO"), - ) + self._reconnect_attempts: int = 0 + self._max_reconnect_attempts: int = int(os.getenv("IB_MAX_RECONNECT_ATTEMPTS", 0)) + self._indefinite_reconnect: bool = False if self._max_reconnect_attempts else True + self._reconnect_delay: int = 5 # seconds # MarketDataMixin self._bar_type_to_last_bar: dict[str, BarData | None] = {} - # OrderMixing + # OrderMixin self._exec_id_details: dict[ str, dict[str, Execution | (CommissionReport | str)], @@ -151,26 +148,85 @@ def __init__( self._order_id_to_order_ref: dict[int, AccountOrderRef] = {} self._next_valid_order_id: int = -1 + # Start client + self._request_id_seq: int = 10000 + def _start(self) -> None: """ Start the client. + + This method is called when the client is first initialized and when the client + is reset. It sets up the client and starts the connection watchdog, incoming + message reader, and internal message queue processing tasks. + """ + if not self._loop.is_running(): + self._log.warning("Started when loop is not running.") + + self._log.info(f"Starting InteractiveBrokersClient ({self._client_id})...") + self._loop.run_until_complete(self._startup()) self._is_client_ready.set() + async def _startup(self): + try: + self._log.info(f"Starting InteractiveBrokersClient ({self._client_id})...") + await self._connect() + self._start_tws_incoming_msg_reader() + self._start_internal_msg_queue_processor() + self._eclient.startApi() + # TWS/Gateway will send a managedAccounts message upon successful connection, + # which will set the `_is_ib_connected` event. This typically takes a few + # seconds, so we wait for it here. + await asyncio.wait_for(self._is_ib_connected.wait(), 15) + self._start_connection_watchdog() + self._is_client_ready.set() + except asyncio.TimeoutError: + self._log.error("Client failed to initialize. Connection timeout.") + self._stop() + except Exception as e: + self._log.exception("Unhandled exception in client startup", e) + self._stop() + + def _start_tws_incoming_msg_reader(self) -> None: + """ + Start the incoming message reader task. + """ + if self._tws_incoming_msg_reader_task: + self._tws_incoming_msg_reader_task.cancel() + self._tws_incoming_msg_reader_task = self._create_task( + self._run_tws_incoming_msg_reader(), + ) + + def _start_internal_msg_queue_processor(self) -> None: + """ + Start the internal message queue processing task. + """ + if self._internal_msg_queue_processor_task: + self._internal_msg_queue_processor_task.cancel() + self._internal_msg_queue_processor_task = self._create_task( + self._run_internal_msg_queue_processor(), + ) + + def _start_connection_watchdog(self) -> None: + """ + Start the connection watchdog task. + """ + if self._connection_watchdog_task: + self._connection_watchdog_task.cancel() + self._connection_watchdog_task = self._create_task( + self._run_connection_watchdog(), + ) + def _stop(self) -> None: """ Stop the client and cancel running tasks. """ - if self.registered_nautilus_clients != set(): - self._log.warning( - f"Any registered Clients from {self.registered_nautilus_clients} will disconnect.", - ) - + self._log.info(f"Stopping InteractiveBrokersClient ({self._client_id})...") # Cancel tasks tasks = [ - self._watch_dog_task, + self._connection_watchdog_task, self._tws_incoming_msg_reader_task, - self._internal_msg_queue_task, + self._internal_msg_queue_processor_task, ] for task in tasks: if task and not task.cancelled(): @@ -179,62 +235,47 @@ def _stop(self) -> None: self._eclient.disconnect() self._is_client_ready.clear() self._account_ids = set() + for client in self.registered_nautilus_clients: + self._log.warning(f"Client {client} disconnected.") + self.registered_nautilus_clients = set() def _reset(self) -> None: """ - Reset the client state and restart connection watchdog. + Restart the client. """ + self._log.info(f"Resetting InteractiveBrokersClient ({self._client_id})...") self._stop() - self._eclient.reset() - - # Start the Watchdog - self._watch_dog_task = self._create_task(self._run_watch_dog()) + self._start() def _resume(self) -> None: """ - Resume the client and reset the connection attempt counter. + Resume the client and resubscribe to all subscriptions. """ + self._log.info(f"Resuming InteractiveBrokersClient ({self._client_id})...") self._is_client_ready.set() - self._connection_attempt_counter = 0 def _degrade(self) -> None: """ Degrade the client when connectivity is lost. """ + self._log.info(f"Degrading InteractiveBrokersClient ({self._client_id})...") self._is_client_ready.clear() self._account_ids = set() - def _start_client_tasks_and_tws_api(self) -> None: + async def _resubscribe_all(self) -> None: """ - Start the incoming message reader and queue tasks, and initiate the start API - call to the EClient. - """ - if self._tws_incoming_msg_reader_task: - self._tws_incoming_msg_reader_task.cancel() - self._tws_incoming_msg_reader_task = self._create_task( - self._run_tws_incoming_msg_reader(), - ) - if self._internal_msg_queue_task: - self._internal_msg_queue_task.cancel() - self._internal_msg_queue_task = self._create_task( - self._run_internal_msg_queue(), - ) - self._eclient.startApi() - - async def _cancel_and_restart_subscriptions(self) -> None: - """ - Attempt to cancel and restart all subscriptions. + Cancel and restart all subscriptions. """ + self._log.debug("Resubscribing all subscriptions...") for subscription in self._subscriptions.get_all(): try: subscription.cancel() if iscoroutinefunction(subscription.handle): await subscription.handle() else: - await self._loop.run_in_executor(None, subscription.handle) + await asyncio.to_thread(subscription.handle) except Exception as e: - # The exception is handled, so won't be further raised - self._log.exception("Failed subscription", e) + self._log.exception(f"Failed to resubscribe to {subscription}", e) async def wait_until_ready(self, timeout: int = 300) -> None: """ @@ -252,7 +293,7 @@ async def wait_until_ready(self, timeout: int = 300) -> None: except asyncio.TimeoutError as e: self._log.error(f"Client is not ready. {e}") - async def _run_watch_dog(self) -> None: + async def _run_connection_watchdog(self) -> None: """ Run a watchdog to monitor and manage the health of the socket connection. @@ -264,24 +305,27 @@ async def _run_watch_dog(self) -> None: try: while True: await asyncio.sleep(1) - if not self._eclient.isConnected(): - await self._reconnect() - - if not self._is_ib_ready.is_set(): - if self.is_running: - self._degrade() - continue - await self._probe_for_connectivity() + if not self._is_ib_connected.is_set() or not self._eclient.isConnected(): + self._log.error( + "Connection watchdog detects connection lost.", + ) + await self._handle_disconnection() + except asyncio.CancelledError: + self._log.debug("Client connection watchdog task was canceled.") - if self.is_degraded: - await self._cancel_and_restart_subscriptions() - self._resume() + async def _handle_disconnection(self) -> None: + """ + Handle the disconnection of the client from TWS/Gateway. + """ + if self.is_running: + self._degrade() + if not self._is_ib_connected.is_set(): + self._log.debug("`_is_ib_connected` unset by `_handle_disconnection`.", LogColor.BLUE) + self._is_ib_connected.clear() + await asyncio.sleep(5) + await self._handle_reconnect() - if self.is_initialized and not self.is_running: - self._start() - except asyncio.CancelledError: - # The exception is handled, so won't be further raised - self._log.debug("Client `watch_dog` task was canceled.") + self._resume() def _create_task( self, @@ -405,7 +449,11 @@ async def _await_request(self, request: Request, timeout: int) -> Any | None: try: return await asyncio.wait_for(request.future, timeout) except asyncio.TimeoutError as e: - self._log.info(f"Request timed out for {request}") + self._log.warning(f"Request timed out for {request}. Ending request.") + self._end_request(request.req_id, success=False, exception=e) + return None + except ConnectionError as e: + self._log.error(f"Connection error during {request}. Ending request.") self._end_request(request.req_id, success=False, exception=e) return None @@ -445,11 +493,11 @@ async def _run_tws_incoming_msg_reader(self) -> None: Continuously read messages from TWS/Gateway and then put them in the internal message queue for processing. """ - self._log.debug("Client TWS incoming message reader starting...") + self._log.debug("Client TWS incoming message reader started.") buf = b"" try: while self._eclient.conn and self._eclient.conn.isConnected(): - data = await self._loop.run_in_executor(None, self._eclient.conn.recvMsg) + data = await asyncio.to_thread(self._eclient.conn.recvMsg) buf += data while buf: _, msg, buf = comm.read_msg(buf) @@ -465,14 +513,20 @@ async def _run_tws_incoming_msg_reader(self) -> None: except Exception as e: self._log.exception("Unhandled exception in Client TWS incoming message reader", e) finally: + if self._is_ib_connected.is_set() and not self.is_disposed: + self._log.debug( + "`_is_ib_connected` unset by `_run_tws_incoming_msg_reader`.", + LogColor.BLUE, + ) + self._is_ib_connected.clear() self._log.debug("Client TWS incoming message reader stopped.") - async def _run_internal_msg_queue(self) -> None: + async def _run_internal_msg_queue_processor(self) -> None: """ Continuously process messages from the internal incoming message queue. """ self._log.debug( - "Client internal message queue starting...", + "Client internal message queue processor started.", ) try: while ( @@ -494,7 +548,7 @@ async def _run_internal_msg_queue(self) -> None: ) ) finally: - self._eclient.disconnect() + self._log.debug("Internal message queue processor stopped.") def _process_message(self, msg: str) -> bool: """ @@ -561,16 +615,3 @@ def logRequest(self, fnName, fnParams): else: prms = fnParams self._log.debug(f"TWS API prepared request: function={fnName} data={prms}") - - # -- EWrapper overrides ----------------------------------------------------------------------- - - def logAnswer(self, fnName, fnParams): - """ - Override the logging for EWrapper.logAnswer. - """ - if "self" in fnParams: - prms = dict(fnParams) - del prms["self"] - else: - prms = fnParams - self._log.debug(f"Msg handled: function={fnName} data={prms}") diff --git a/nautilus_trader/adapters/interactive_brokers/client/common.py b/nautilus_trader/adapters/interactive_brokers/client/common.py index 7b6235fdd514..1104a65a0d22 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/common.py +++ b/nautilus_trader/adapters/interactive_brokers/client/common.py @@ -493,22 +493,28 @@ class BaseMixin: _subscriptions: Subscriptions _event_subscriptions: dict[str, Callable] _eclient: EClient - _is_ib_ready: asyncio.Event + _is_ib_connected: asyncio.Event + _start: Callable + _startup: Callable + _reset: Callable + _stop: Callable + _resume: Callable _degrade: Callable _end_request: Callable _await_request: Callable _next_req_id: Callable - logAnswer: Callable - _reset: Callable + _resubscribe_all: Callable _create_task: Callable - _start_client_tasks_and_tws_api: Callable + logAnswer: Callable # Account accounts: Callable # Connection - _connection_attempt_counter: int - _contract_for_probe: IBContract + _reconnect_attempts: int + _reconnect_delay: int + _max_reconnect_attempts: int + _indefinite_reconnect: bool # MarketData _bar_type_to_last_bar: dict[str, BarData | None] diff --git a/nautilus_trader/adapters/interactive_brokers/client/connection.py b/nautilus_trader/adapters/interactive_brokers/client/connection.py index ed485ae67c39..f76d1852f9a0 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/connection.py +++ b/nautilus_trader/adapters/interactive_brokers/client/connection.py @@ -24,9 +24,9 @@ from ibapi.errors import CONNECT_FAIL from ibapi.server_versions import MAX_CLIENT_VER from ibapi.server_versions import MIN_CLIENT_VER -from ibapi.utils import current_fn_name from nautilus_trader.adapters.interactive_brokers.client.common import BaseMixin +from nautilus_trader.common.enums import LogColor class InteractiveBrokersClientConnectionMixin(BaseMixin): @@ -35,26 +35,23 @@ class InteractiveBrokersClientConnectionMixin(BaseMixin): This class is responsible for establishing and maintaining the socket connection, handling server communication, monitoring the connection's health, and managing - reconnections. + reconnections. When a connection is established and the client finishes initializing, + the `_is_ib_connected` event is set, and if the connection is lost, the + `_is_ib_connected` event is cleared. """ - async def _establish_socket_connection(self) -> None: + async def _connect(self) -> None: """ - Establish the socket connection with TWS/Gateway. It initializes the connection, - connects the socket, sends and receives version information, and then sets up - the client. + Establish the socket connection with TWS/Gateway. - Raises - ------ - OSError - If an OSError occurs during the connection process. - Exception - For any other unexpected errors during the connection. + This initializes the connection, connects the socket, sends and receives version + information, and then sets a flag that the connection has been successfully + established. """ - self._initialize_connection_params() try: + self._initialize_connection_params() await self._connect_socket() self._eclient.setConnState(EClient.CONNECTING) await self._send_version_info() @@ -64,12 +61,56 @@ async def _establish_socket_connection(self) -> None: ) await self._receive_server_info() self._eclient.setConnState(EClient.CONNECTED) - self._start_client_tasks_and_tws_api() - self._log.debug("TWS API connection established successfully.") - except OSError as e: - self._handle_connection_error(e) + self._log.info( + f"Connected to Interactive Brokers (v{self._eclient.serverVersion_}) " + f"at {self._eclient.connTime.decode()} from {self._host}:{self._port} " + f"with client id: {self._client_id}.", + ) + except asyncio.CancelledError: + self._log.info("Connection cancelled.") + await self._disconnect() + except Exception as e: + self._log.error(f"Connection failed: {e}") + if self._eclient.wrapper: + self._eclient.wrapper.error(NO_VALID_ID, CONNECT_FAIL.code(), CONNECT_FAIL.msg()) + await self._handle_reconnect() + + async def _disconnect(self) -> None: + """ + Disconnect from TWS/Gateway and clear the `_is_ib_connected` flag. + """ + try: + self._eclient.disconnect() + if self._is_ib_connected.is_set(): + self._log.debug("`_is_ib_connected` unset by `_disconnect`.", LogColor.BLUE) + self._is_ib_connected.clear() + self._log.info("Disconnected from Interactive Brokers API.") except Exception as e: - self._log.exception("Unexpected error during connection", e) + self._log.error(f"Disconnection failed: {e}") + + async def _handle_reconnect(self) -> None: + """ + Attempt to reconnect to TWS/Gateway. + """ + while not self._is_ib_connected.is_set(): + if ( + not self._indefinite_reconnect + and self._reconnect_attempts > self._max_reconnect_attempts + ): + self._log.error("Max reconnection attempts reached. Connection failed.") + self._stop() + break + self._reconnect_attempts += 1 + self._log.info( + f"Attempt {self._reconnect_attempts}: Attempting to reconnect in {self._reconnect_delay} seconds...", + ) + await asyncio.sleep(self._reconnect_delay) + await self._startup() + + self._log.info("Reconnection successful.") + self._reconnect_attempts = 0 + await self._resubscribe_all() + self._resume() def _initialize_connection_params(self) -> None: """ @@ -79,14 +120,10 @@ def _initialize_connection_params(self) -> None: the connection attempt counter. Logs the attempt information. """ + self._eclient.reset() self._eclient._host = self._host self._eclient._port = self._port self._eclient.clientId = self._client_id - self._connection_attempt_counter += 1 - self._log.info( - f"Attempt {self._connection_attempt_counter}: " - f"Connecting to {self._host}:{self._port} w/ id:{self._client_id}", - ) async def _connect_socket(self) -> None: """ @@ -97,7 +134,10 @@ async def _connect_socket(self) -> None: """ self._eclient.conn = Connection(self._host, self._port) - await self._loop.run_in_executor(None, self._eclient.conn.connect) + self._log.info( + f"Connecting to {self._host}:{self._port} with client id: {self._client_id}", + ) + await asyncio.to_thread(self._eclient.conn.connect) async def _send_version_info(self) -> None: """ @@ -114,10 +154,7 @@ async def _send_version_info(self) -> None: v100version += f" {self._eclient.connectionOptions}" msg = comm.make_msg(v100version) msg2 = str.encode(v100prefix, "ascii") + msg - await self._loop.run_in_executor( - None, - functools.partial(self._eclient.conn.sendMsg, msg2), - ) + await asyncio.to_thread(functools.partial(self._eclient.conn.sendMsg, msg2)) async def _receive_server_info(self) -> None: """ @@ -132,49 +169,33 @@ async def _receive_server_info(self) -> None: If the server version information is not received within the allotted retries. """ - connection_retries_remaining = 5 + retries_remaining = 5 fields: list[str] = [] - while len(fields) != 2 and connection_retries_remaining > 0: + while retries_remaining > 0: + buf = await asyncio.to_thread(self._eclient.conn.recvMsg) + if len(buf) > 0: + _, msg, _ = comm.read_msg(buf) + fields.extend(comm.read_fields(msg)) + else: + self._log.debug("Received empty buffer.") + + if len(fields) == 2: + self._process_server_version(fields) + break + + retries_remaining -= 1 + self._log.warning( + "Failed to receive server version information. " + f"Retries remaining: {retries_remaining}.", + ) await asyncio.sleep(1) - buf = await self._loop.run_in_executor(None, self._eclient.conn.recvMsg) - self._process_received_buffer(buf, connection_retries_remaining, fields) - - if len(fields) == 2: - self._process_server_version(fields) - else: - raise ConnectionError("Failed to receive server version information.") - - def _process_received_buffer( - self, - buf: bytes, - retries_remaining: int, - fields: list[str], - ) -> None: - """ - Process the received buffer from TWS API. Reads the received message and - extracts fields from it. Handles situations where the connection might be lost - or the received buffer is empty. - - Parameters - ---------- - buf : bytes - The received buffer from the server. - retries_remaining : int - The number of remaining retries for receiving the message. - fields : list[str] - The list to which the extracted fields will be appended. - """ - if not self._eclient.conn.isConnected() or retries_remaining <= 0: - self._log.warning("Disconnected. Resetting connection...") - self._reset() - return - if len(buf) > 0: - _, msg, _ = comm.read_msg(buf) - fields.extend(comm.read_fields(msg)) - else: - self._log.debug(f"Received empty buffer (retries_remaining={retries_remaining})") + if retries_remaining == 0: + raise ConnectionError( + "Max retry attempts reached. Failed to receive server version information.", + ) + self._log.info("") def _process_server_version(self, fields: list[str]) -> None: """ @@ -192,80 +213,8 @@ def _process_server_version(self, fields: list[str]) -> None: self._eclient.connTime = conn_time self._eclient.serverVersion_ = server_version self._eclient.decoder.serverVersion = server_version - self._log.debug(f"Connected to server version {server_version} at {conn_time}") - - async def _reconnect(self) -> None: - """ - Manage socket connectivity, including reconnection attempts and error handling. - Degrades the client if it's currently running and tries to re-establish the - socket connection. Waits for the Interactive Brokers readiness signal, logging - success or failure accordingly. - - Raises - ------ - asyncio.TimeoutError - If the connection attempt times out. - Exception - For general failures in re-establishing the connection. - - """ - if self.is_running: - self._degrade() - self._is_ib_ready.clear() - await asyncio.sleep(5) # Avoid too fast attempts - await self._establish_socket_connection() - try: - await asyncio.wait_for(self._is_ib_ready.wait(), 15) - self._log.info( - f"Connected to {self._host}:{self._port} w/ id:{self._client_id}", - ) - except asyncio.TimeoutError: - self._log.error( - f"Unable to connect to {self._host}:{self._port} w/ id:{self._client_id}", - ) - except Exception as e: - self._log.exception("Failed connection", e) - async def _probe_for_connectivity(self) -> None: - """ - Perform a connectivity probe to TWS using a historical data request if the - client is degraded. - """ - # Probe connectivity. Sometime restored event will not be received from TWS without this - self._eclient.reqHistoricalData( - reqId=1, - contract=self._contract_for_probe, - endDateTime="", - durationStr="30 S", - barSizeSetting="5 secs", - whatToShow="MIDPOINT", - useRTH=False, - formatDate=2, - keepUpToDate=False, - chartOptions=[], - ) - await asyncio.sleep(15) - self._eclient.cancelHistoricalData(1) - - def _handle_connection_error(self, e): - """ - Handle any connection errors that occur during the connection setup. Logs the - error, notifies the wrapper of the connection failure, and disconnects the - client. - - Parameters - ---------- - e : Exception - The exception that occurred during the connection process. - - """ - if self._eclient.wrapper: - self._eclient.wrapper.error(NO_VALID_ID, CONNECT_FAIL.code(), CONNECT_FAIL.msg()) - self._eclient.disconnect() - self._log.error(f"Connection failed: {e}") - - # -- EWrapper overrides ----------------------------------------------------------------------- - def connectionClosed(self) -> None: + def process_connection_closed(self) -> None: """ Indicate the API connection has closed. @@ -273,8 +222,9 @@ def connectionClosed(self) -> None: automatically but must be triggered by API client code. """ - self.logAnswer(current_fn_name(), vars()) for future in self._requests.get_futures(): if not future.done(): - future.set_exception(ConnectionError("Socket disconnect")) - self._eclient.reset() + future.set_exception(ConnectionError("Socket disconnected.")) + if self._is_ib_connected.is_set(): + self._log.debug("`_is_ib_connected` unset by `connectionClosed`.", LogColor.BLUE) + self._is_ib_connected.clear() diff --git a/nautilus_trader/adapters/interactive_brokers/client/contract.py b/nautilus_trader/adapters/interactive_brokers/client/contract.py index 9c284a9f9113..acfd98ec842e 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/contract.py +++ b/nautilus_trader/adapters/interactive_brokers/client/contract.py @@ -19,7 +19,6 @@ from ibapi.common import SetOfFloat from ibapi.common import SetOfString from ibapi.contract import ContractDetails -from ibapi.utils import current_fn_name from nautilus_trader.adapters.interactive_brokers.client.common import BaseMixin from nautilus_trader.adapters.interactive_brokers.common import IBContract @@ -140,9 +139,9 @@ async def get_option_chains(self, underlying: IBContract) -> Any | None: self._log.info(f"Request already exist for {request}") return None - # -- EWrapper overrides ----------------------------------------------------------------------- - def contractDetails( + def process_contract_details( self, + *, req_id: int, contract_details: ContractDetails, ) -> None: @@ -151,21 +150,20 @@ def contractDetails( contracts matching the requested via EClientSocket::reqContractDetails. For example, one can obtain the whole option chain with it. """ - self.logAnswer(current_fn_name(), vars()) if not (request := self._requests.get(req_id=req_id)): return request.result.append(contract_details) - def contractDetailsEnd(self, req_id: int) -> None: + def process_contract_details_end(self, *, req_id: int) -> None: """ After all contracts matching the request were returned, this method will mark the end of their reception. """ - self.logAnswer(current_fn_name(), vars()) self._end_request(req_id) - def securityDefinitionOptionParameter( + def process_security_definition_option_parameter( self, + *, req_id: int, exchange: str, underlying_con_id: int, @@ -180,27 +178,24 @@ def securityDefinitionOptionParameter( securityDefinitionOptionParameter if multiple exchanges are specified in reqSecDefOptParams. """ - self.logAnswer(current_fn_name(), vars()) if request := self._requests.get(req_id=req_id): request.result.append((exchange, expirations)) - def securityDefinitionOptionParameterEnd(self, req_id: int) -> None: + def process_security_definition_option_parameter_end(self, *, req_id: int) -> None: """ Call when all callbacks to securityDefinitionOptionParameter are complete. """ - self.logAnswer(current_fn_name(), vars()) self._end_request(req_id) - def symbolSamples( + def process_symbol_samples( self, + *, req_id: int, contract_descriptions: list, ) -> None: """ Return an array of sample contract descriptions. """ - self.logAnswer(current_fn_name(), vars()) - if request := self._requests.get(req_id=req_id): for contract_description in contract_descriptions: request.result.append(IBContract(**contract_description.contract.__dict__)) diff --git a/nautilus_trader/adapters/interactive_brokers/client/error.py b/nautilus_trader/adapters/interactive_brokers/client/error.py index 94fa54d20d26..5e780b24770e 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/error.py +++ b/nautilus_trader/adapters/interactive_brokers/client/error.py @@ -14,8 +14,10 @@ # ------------------------------------------------------------------------------------------------- from inspect import iscoroutinefunction +from typing import Final from nautilus_trader.adapters.interactive_brokers.client.common import BaseMixin +from nautilus_trader.common.enums import LogColor class InteractiveBrokersClientErrorMixin(BaseMixin): @@ -31,11 +33,11 @@ class InteractiveBrokersClientErrorMixin(BaseMixin): """ - WARNING_CODES = {1101, 1102, 110, 165, 202, 399, 404, 434, 492, 10167} - CLIENT_ERRORS = {502, 503, 504, 10038, 10182, 1100, 2110} - CONNECTIVITY_LOST_CODES = {1100, 1300, 2110} - CONNECTIVITY_RESTORED_CODES = {1101, 1102} - ORDER_REJECTION_CODES = {201, 203, 321, 10289, 10293} + WARNING_CODES: Final[set[int]] = {1101, 1102, 110, 165, 202, 399, 404, 434, 492, 10167} + CLIENT_ERRORS: Final[set[int]] = {502, 503, 504, 10038, 10182, 1100, 2110} + CONNECTIVITY_LOST_CODES: Final[set[int]] = {1100, 1300, 2110} + CONNECTIVITY_RESTORED_CODES: Final[set[int]] = {1101, 1102} + ORDER_REJECTION_CODES: Final[set[int]] = {201, 203, 321, 10289, 10293} def _log_message( self, @@ -59,14 +61,17 @@ def _log_message( Indicates whether the message is a warning or an error. """ - msg_type = "Warning" if is_warning else "Error" - msg = f"{msg_type} {error_code} {req_id=}: {error_string}" - if is_warning: - self._log.info(msg) - else: - self._log.error(msg) + msg = f"{error_string} (code: {error_code}, {req_id=})." + self._log.warning(msg) if is_warning else self._log.error(msg) - def _process_error(self, req_id: int, error_code: int, error_string: str) -> None: + def process_error( + self, + *, + req_id: int, + error_code: int, + error_string: str, + advanced_order_reject_json: str = "", + ) -> None: """ Process an error based on its code, request ID, and message. Depending on the error code, this method delegates to specific error handlers or performs general @@ -80,9 +85,12 @@ def _process_error(self, req_id: int, error_code: int, error_string: str) -> Non The error code. error_string : str The error message string. + advanced_order_reject_json : str + The JSON string for advanced order rejection. """ is_warning = error_code in self.WARNING_CODES or 2100 <= error_code < 2200 + error_string = error_string.replace("\n", " ") self._log_message(error_code, req_id, error_string, is_warning) if req_id != -1: @@ -95,12 +103,19 @@ def _process_error(self, req_id: int, error_code: int, error_string: str) -> Non else: self._log.warning(f"Unhandled error: {error_code} for req_id {req_id}") elif error_code in self.CLIENT_ERRORS or error_code in self.CONNECTIVITY_LOST_CODES: - self._log.warning(f"Client or Connectivity Lost Error: {error_string}") - if self._is_ib_ready.is_set(): - self._is_ib_ready.clear() + if self._is_ib_connected.is_set(): + self._log.debug( + f"`_is_ib_connected` unset by code {error_code} in `_process_error`.", + LogColor.BLUE, + ) + self._is_ib_connected.clear() elif error_code in self.CONNECTIVITY_RESTORED_CODES: - if not self._is_ib_ready.is_set(): - self._is_ib_ready.set() + if not self._is_ib_connected.is_set(): + self._log.debug( + f"`_is_ib_connected` set by code {error_code} in `_process_error`.", + LogColor.BLUE, + ) + self._is_ib_connected.set() def _handle_subscription_error(self, req_id: int, error_code: int, error_string: str) -> None: """ @@ -132,9 +147,11 @@ def _handle_subscription_error(self, req_id: int, error_code: int, error_string: elif error_code == 10182: # Handle disconnection error self._log.warning(f"{error_code}: {error_string}") - if self._is_ib_ready.is_set(): - self._log.info(f"`is_ib_ready` cleared by {subscription.name}") - self._is_ib_ready.clear() + if self._is_ib_connected.is_set(): + self._log.info( + f"`_is_ib_connected` unset by {subscription.name} in `_handle_subscription_error`.", + ) + self._is_ib_connected.clear() else: # Log unknown subscription errors self._log.warning( @@ -197,17 +214,3 @@ def _handle_order_error(self, req_id: int, error_code: int, error_string: str) - f"Unhandled order warning or error code: {error_code} (req_id {req_id}) - " f"{error_string}", ) - - # -- EWrapper overrides ----------------------------------------------------------------------- - - def error( - self, - req_id: int, - error_code: int, - error_string: str, - advanced_order_reject_json: str = "", - ) -> None: - """ - Errors sent by TWS API are received here. - """ - self._process_error(req_id, error_code, error_string) diff --git a/nautilus_trader/adapters/interactive_brokers/client/market_data.py b/nautilus_trader/adapters/interactive_brokers/client/market_data.py index a3ecdb4cbd78..0fc1b4bde804 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/market_data.py +++ b/nautilus_trader/adapters/interactive_brokers/client/market_data.py @@ -16,6 +16,7 @@ import functools from collections.abc import Callable from decimal import Decimal +from inspect import iscoroutinefunction from typing import Any import pandas as pd @@ -25,7 +26,6 @@ from ibapi.common import MarketDataTypeEnum from ibapi.common import TickAttribBidAsk from ibapi.common import TickAttribLast -from ibapi.utils import current_fn_name # fmt: off from nautilus_trader.adapters.interactive_brokers.client.common import BaseMixin @@ -36,7 +36,6 @@ from nautilus_trader.adapters.interactive_brokers.parsing.data import timedelta_to_duration_str from nautilus_trader.adapters.interactive_brokers.parsing.data import what_to_show from nautilus_trader.adapters.interactive_brokers.parsing.instruments import ib_contract_to_instrument_id -from nautilus_trader.common.enums import LogColor from nautilus_trader.core.data import Data from nautilus_trader.model.data import Bar from nautilus_trader.model.data import BarType @@ -125,7 +124,10 @@ async def _subscribe( ) if not subscription: return None - subscription.handle() + if iscoroutinefunction(subscription.handle): + await subscription.handle() + else: + subscription.handle() return subscription else: self._log.info(f"Subscription already exists for {subscription}") @@ -646,21 +648,20 @@ def _handle_data(self, data: Data) -> None: """ self._msgbus.send(endpoint="DataEngine.process", msg=data) - # -- EWrapper overrides ----------------------------------------------------------------------- - def marketDataType(self, req_id: int, market_data_type: int) -> None: + def process_market_data_type(self, *, req_id: int, market_data_type: int) -> None: """ Return the market data type (real-time, frozen, delayed, delayed-frozen) of ticker sent by EClientSocket::reqMktData when TWS switches from real-time to frozen and back and from delayed to delayed-frozen and back. """ - self.logAnswer(current_fn_name(), vars()) if market_data_type == MarketDataTypeEnum.REALTIME: self._log.debug(f"Market DataType is {MarketDataTypeEnum.to_str(market_data_type)}") else: self._log.warning(f"Market DataType is {MarketDataTypeEnum.to_str(market_data_type)}") - def tickByTickBidAsk( + def process_tick_by_tick_bid_ask( self, + *, req_id: int, time: int, bid_price: float, @@ -672,7 +673,6 @@ def tickByTickBidAsk( """ Return "BidAsk" tick-by-tick real-time tick data. """ - self.logAnswer(current_fn_name(), vars()) if not (subscription := self._subscriptions.get(req_id=req_id)): return @@ -692,8 +692,9 @@ def tickByTickBidAsk( self._handle_data(quote_tick) - def tickByTickAllLast( + def process_tick_by_tick_all_last( self, + *, req_id: int, tick_type: str, time: int, @@ -706,7 +707,6 @@ def tickByTickAllLast( """ Return "Last" or "AllLast" (trades) tick-by-tick real-time tick. """ - self.logAnswer(current_fn_name(), vars()) if not (subscription := self._subscriptions.get(req_id=req_id)): return @@ -730,8 +730,9 @@ def tickByTickAllLast( self._handle_data(trade_tick) - def realtimeBar( + def process_realtime_bar( self, + *, req_id: int, time: int, open_: float, @@ -745,7 +746,6 @@ def realtimeBar( """ Update real-time 5 second bars. """ - self.logAnswer(current_fn_name(), vars()) if not (subscription := self._subscriptions.get(req_id=req_id)): return bar_type = BarType.from_str(subscription.name) @@ -765,11 +765,10 @@ def realtimeBar( self._handle_data(bar) - def historicalData(self, req_id: int, bar: BarData) -> None: + def process_historical_data(self, *, req_id: int, bar: BarData) -> None: """ Return the requested historical data bars. """ - self.logAnswer(current_fn_name(), vars()) if request := self._requests.get(req_id=req_id): bar_type = BarType.from_str(request.name) bar = self._ib_bar_to_nautilus_bar( @@ -792,17 +791,13 @@ def historicalData(self, req_id: int, bar: BarData) -> None: self._log.debug(f"Received {bar=} on {req_id=}") return - def historicalDataEnd(self, req_id: int, start: str, end: str) -> None: + def process_historical_data_end(self, *, req_id: int, start: str, end: str) -> None: """ Mark the end of receiving historical bars. """ - self.logAnswer(current_fn_name(), vars()) self._end_request(req_id) - if req_id == 1 and not self._is_ib_ready.is_set(): # probe successful - self._log.info(f"`is_ib_ready` set by historicalDataEnd {req_id=}", LogColor.BLUE) - self._is_ib_ready.set() - def historicalDataUpdate(self, req_id: int, bar: BarData) -> None: + def process_historical_data_update(self, *, req_id: int, bar: BarData) -> None: """ Receive bars in real-time if keepUpToDate is set as True in reqHistoricalData. @@ -812,7 +807,6 @@ def historicalDataUpdate(self, req_id: int, bar: BarData) -> None: time data. """ - self.logAnswer(current_fn_name(), vars()) if not (subscription := self._subscriptions.get(req_id=req_id)): return if not isinstance(subscription.handle, functools.partial): @@ -827,8 +821,9 @@ def historicalDataUpdate(self, req_id: int, bar: BarData) -> None: else: self._handle_data(bar) - def historicalTicksBidAsk( + def process_historical_ticks_bid_ask( self, + *, req_id: int, ticks: list, done: bool, @@ -836,7 +831,6 @@ def historicalTicksBidAsk( """ Return the requested historic bid/ask ticks. """ - self.logAnswer(current_fn_name(), vars()) if not done: return if request := self._requests.get(req_id=req_id): @@ -858,20 +852,18 @@ def historicalTicksBidAsk( self._end_request(req_id) - def historicalTicksLast(self, req_id: int, ticks: list, done: bool) -> None: + def process_historical_ticks_last(self, *, req_id: int, ticks: list, done: bool) -> None: """ Return the requested historic trade ticks. """ - self.logAnswer(current_fn_name(), vars()) if not done: return self._process_trade_ticks(req_id, ticks) - def historicalTicks(self, req_id: int, ticks: list, done: bool) -> None: + def process_historical_ticks(self, *, req_id: int, ticks: list, done: bool) -> None: """ Return the requested historic ticks. """ - self.logAnswer(current_fn_name(), vars()) if not done: return self._process_trade_ticks(req_id, ticks) diff --git a/nautilus_trader/adapters/interactive_brokers/client/order.py b/nautilus_trader/adapters/interactive_brokers/client/order.py index 25961f21bead..1fd496422f5c 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/order.py +++ b/nautilus_trader/adapters/interactive_brokers/client/order.py @@ -20,7 +20,6 @@ from ibapi.execution import Execution from ibapi.order import Order as IBOrder from ibapi.order_state import OrderState as IBOrderState -from ibapi.utils import current_fn_name from nautilus_trader.adapters.interactive_brokers.client.common import AccountOrderRef from nautilus_trader.adapters.interactive_brokers.client.common import BaseMixin @@ -143,8 +142,7 @@ def next_order_id(self) -> int: self._eclient.reqIds(-1) return order_id - # -- EWrapper overrides ----------------------------------------------------------------------- - def nextValidId(self, order_id: int) -> None: + def process_next_valid_id(self, *, order_id: int) -> None: """ Receive the next valid order id. @@ -153,14 +151,14 @@ def nextValidId(self, order_id: int) -> None: Important: the next valid order ID is only valid at the time it is received. """ - self.logAnswer(current_fn_name(), vars()) self._next_valid_order_id = max(self._next_valid_order_id, order_id, 101) - if self.accounts() and not self._is_ib_ready.is_set(): - self._log.info("`is_ib_ready` set by nextValidId", LogColor.BLUE) - self._is_ib_ready.set() + if self.accounts() and not self._is_ib_connected.is_set(): + self._log.debug("`_is_ib_connected` set by `nextValidId`.", LogColor.BLUE) + self._is_ib_connected.set() - def openOrder( + def process_open_order( self, + *, order_id: int, contract: Contract, order: IBOrder, @@ -169,7 +167,6 @@ def openOrder( """ Feed in currently open orders. """ - self.logAnswer(current_fn_name(), vars()) # Handle response to on-demand request if request := self._requests.get(name="OpenOrders"): order.contract = IBContract(**contract.__dict__) @@ -201,16 +198,16 @@ def openOrder( order_state=order_state, ) - def openOrderEnd(self) -> None: + def process_open_order_end(self) -> None: """ Notifies the end of the open orders' reception. """ - self.logAnswer(current_fn_name(), vars()) if request := self._requests.get(name="OpenOrders"): self._end_request(request.req_id) - def orderStatus( + def process_order_status( self, + *, order_id: int, status: str, filled: Decimal, @@ -229,7 +226,6 @@ def orderStatus( Note: Often there are duplicate orderStatus messages. """ - self.logAnswer(current_fn_name(), vars()) order_ref = self._order_id_to_order_ref.get(order_id, None) if order_ref: name = f"orderStatus-{order_ref.account_id}" @@ -239,8 +235,9 @@ def orderStatus( order_status=status, ) - def execDetails( + def process_exec_details( self, + *, req_id: int, contract: Contract, execution: Execution, @@ -248,7 +245,6 @@ def execDetails( """ Provide the executions that happened in the prior 24 hours. """ - self.logAnswer(current_fn_name(), vars()) if not (cache := self._exec_id_details.get(execution.execId, None)): self._exec_id_details[execution.execId] = {} cache = self._exec_id_details[execution.execId] @@ -266,14 +262,14 @@ def execDetails( ) cache.pop(execution.execId, None) - def commissionReport( + def process_commission_report( self, + *, commission_report: CommissionReport, ) -> None: """ Provide the CommissionReport of an Execution. """ - self.logAnswer(current_fn_name(), vars()) if not (cache := self._exec_id_details.get(commission_report.execId, None)): self._exec_id_details[commission_report.execId] = {} cache = self._exec_id_details[commission_report.execId] diff --git a/nautilus_trader/adapters/interactive_brokers/client/wrapper.py b/nautilus_trader/adapters/interactive_brokers/client/wrapper.py new file mode 100644 index 000000000000..eac080a9a252 --- /dev/null +++ b/nautilus_trader/adapters/interactive_brokers/client/wrapper.py @@ -0,0 +1,1269 @@ +# ------------------------------------------------------------------------------------------------- +# 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 decimal import Decimal +from typing import TYPE_CHECKING + +from ibapi.commission_report import CommissionReport +from ibapi.common import BarData +from ibapi.common import FaDataType +from ibapi.common import HistogramData +from ibapi.common import ListOfContractDescription +from ibapi.common import ListOfDepthExchanges +from ibapi.common import ListOfFamilyCode +from ibapi.common import ListOfHistoricalSessions +from ibapi.common import ListOfHistoricalTick +from ibapi.common import ListOfHistoricalTickBidAsk +from ibapi.common import ListOfHistoricalTickLast +from ibapi.common import ListOfNewsProviders +from ibapi.common import ListOfPriceIncrements +from ibapi.common import OrderId +from ibapi.common import SetOfFloat +from ibapi.common import SetOfString +from ibapi.common import SmartComponentMap +from ibapi.common import TickAttrib +from ibapi.common import TickAttribBidAsk +from ibapi.common import TickAttribLast +from ibapi.common import TickerId +from ibapi.contract import Contract +from ibapi.contract import ContractDetails +from ibapi.contract import DeltaNeutralContract +from ibapi.execution import Execution +from ibapi.order import Order +from ibapi.order_state import OrderState +from ibapi.ticktype import TickType +from ibapi.utils import current_fn_name +from ibapi.wrapper import EWrapper + +from nautilus_trader.common.component import Logger + + +if TYPE_CHECKING: + from nautilus_trader.adapters.interactive_brokers.client.client import InteractiveBrokersClient + + +class InteractiveBrokersEWrapper(EWrapper): + def __init__( + self, + nautilus_logger: Logger, + client: "InteractiveBrokersClient", + ) -> None: + super().__init__() + self._log = nautilus_logger + self._client = client + + def logAnswer(self, fnName, fnParams) -> None: + """ + Override the logging for EWrapper.logAnswer. + """ + if "self" in fnParams: + prms = dict(fnParams) + del prms["self"] + else: + prms = fnParams + self._log.debug(f"Msg handled: function={fnName} data={prms}") + + def error( + self, + reqId: TickerId, + errorCode: int, + errorString: str, + advancedOrderRejectJson="", + ) -> None: + """ + Call this event in response to an error in communication or when TWS needs to + send a message to the client. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_error( + req_id=reqId, + error_code=errorCode, + error_string=errorString, + advanced_order_reject_json=advancedOrderRejectJson, + ) + + def winError(self, text: str, lastError: int) -> None: + self.logAnswer(current_fn_name(), vars()) + + def connectAck(self) -> None: + """ + Invoke this callback to signify the completion of a successful connection. + """ + self.logAnswer(current_fn_name(), vars()) + + def marketDataType(self, reqId: TickerId, marketDataType: int) -> None: + """ + Receives notification when the market data type changes. + + This method is called when TWS sends a marketDataType(type) callback to the API, + where type is set to Frozen or RealTime, to announce that market data has been + switched between frozen and real-time. This notification occurs only when market + data switches between real-time and frozen. The marketDataType() callback accepts + a reqId parameter and is sent per every subscription because different contracts + can generally trade on a different schedule. + + Parameters + ---------- + reqId : TickerId + The request's identifier. + marketDataType : int + The type of market data being received. Possible values are 1 for real-time streaming, 2 for frozen market data. + + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_market_data_type(req_id=reqId, market_data_type=marketDataType) + + def tickPrice( + self, + reqId: TickerId, + tickType: TickType, + price: float, + attrib: TickAttrib, + ) -> None: + """ + Market data tick price callback. + + Parameters + ---------- + reqId : TickerId + The request's identifier. + tickType : TickType + The type of tick being received. + price : float + The price of the tick. + attrib : TickAttrib + The tick's attributes. + + """ + self.logAnswer(current_fn_name(), vars()) + + def tickSize(self, reqId: TickerId, tickType: TickType, size: Decimal) -> None: + """ + Handle tick size-related market data. + + This method is responsible for handling all size-related ticks from the market data. + Each tick represents a change in the market size for a specific type of data. + + Parameters + ---------- + reqId : TickerId + The request's identifier. + tickType : TickType + The type of tick being received. + size : Decimal + The size of the tick. + + """ + self.logAnswer(current_fn_name(), vars()) + + def tickSnapshotEnd(self, reqId: int) -> None: + """ + When requesting market data snapshots, this market will indicate the snapshot + reception is finished. + """ + self.logAnswer(current_fn_name(), vars()) + + def tickGeneric(self, reqId: TickerId, tickType: TickType, value: float) -> None: + self.logAnswer(current_fn_name(), vars()) + + def tickString(self, reqId: TickerId, tickType: TickType, value: str) -> None: + self.logAnswer(current_fn_name(), vars()) + + def tickEFP( + self, + reqId: TickerId, + tickType: TickType, + basisPoints: float, + formattedBasisPoints: str, + totalDividends: float, + holdDays: int, + futureLastTradeDate: str, + dividendImpact: float, + dividendsToLastTradeDate: float, + ) -> None: + """ + Market data callback for Exchange for Physical. + + Parameters + ---------- + reqId : TickerId + The request's identifier. + tickType : TickType + The type of tick being received. + basisPoints : float + Annualized basis points, representative of the financing rate that can be directly be + compared to broker rates. + formattedBasisPoints : str + Annualized basis points as a formatted string depicting them in percentage form. + totalDividends : float + The total dividends. + holdDays : int + The number of hold days until the lastTradeDate of the EFP. + futureLastTradeDate : str + The expiration date of the single stock future. + dividendImpact : float + The dividend impact upon the annualized basis points interest rate. + dividendsToLastTradeDate : float + The dividends expected until the expiration of the single stock future. + + """ + self.logAnswer(current_fn_name(), vars()) + + def orderStatus( + self, + orderId: OrderId, + status: str, + filled: Decimal, + remaining: Decimal, + avgFillPrice: float, + permId: int, + parentId: int, + lastFillPrice: float, + clientId: int, + whyHeld: str, + mktCapPrice: float, + ) -> None: + """ + Call this event whenever the status of an order changes. Also, fire it after + reconnecting to TWS if the client has any open orders. + + Parameters + ---------- + orderId: OrderId + The order ID that was specified previously in the call to placeOrder(). + status: str + The order status. Possible values include: + PendingSubmit, PendingCancel, PreSubmitted, Submitted, Cancelled, Filled, Inactive. + filled: int + Specifies the number of shares that have been executed. + remaining: int + Specifies the number of shares still outstanding. + avgFillPrice: float + The average price of the shares that have been executed. + permId: int + The TWS id used to identify orders. Remains the same over TWS sessions. + parentId: int + The order ID of the parent order, used for bracket and auto trailing stop orders. + lastFillPrice: float + The last price of the shares that have been executed. + clientId: int + The ID of the client (or TWS) that placed the order. + whyHeld: str + This field is used to identify an order held when TWS is trying to locate shares for a short sell. + The value used to indicate this is 'locate'. + mktCapPrice: float + The price at which the market cap price is calculated. + + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_order_status( + order_id=orderId, + status=status, + filled=filled, + remaining=remaining, + avg_fill_price=avgFillPrice, + perm_id=permId, + parent_id=parentId, + last_fill_price=lastFillPrice, + client_id=clientId, + why_held=whyHeld, + mkt_cap_price=mktCapPrice, + ) + + def openOrder( + self, + orderId: OrderId, + contract: Contract, + order: Order, + orderState: OrderState, + ) -> None: + """ + Call this function to feed in open orders. + + Parameters + ---------- + orderId: OrderId + The order ID assigned by TWS. Use to cancel or update TWS order. + contract: Contract + The Contract class attributes describe the contract. + order: Order + The Order class gives the details of the open order. + orderState: OrderState + The orderState class includes attributes Used for both pre and post trade margin and commission data. + + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_open_order( + order_id=orderId, + contract=contract, + order=order, + order_state=orderState, + ) + + def openOrderEnd(self) -> None: + """ + Call this at the end of a given request for open orders. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_open_order_end() + + def connectionClosed(self) -> None: + """ + Call this function when TWS closes the socket connection with the ActiveX + control, or when TWS is shut down. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_connection_closed() + + def updateAccountValue( + self, + key: str, + val: str, + currency: str, + accountName: str, + ) -> None: + """ + Call this function only when ReqAccountUpdates on EEClientSocket object has been + called. + """ + self.logAnswer(current_fn_name(), vars()) + + def updatePortfolio( + self, + contract: Contract, + position: Decimal, + marketPrice: float, + marketValue: float, + averageCost: float, + unrealizedPNL: float, + realizedPNL: float, + accountName: str, + ) -> None: + """ + Call this function only when reqAccountUpdates on EEClientSocket object has been + called. + """ + self.logAnswer(current_fn_name(), vars()) + + def updateAccountTime(self, timeStamp: str) -> None: + self.logAnswer(current_fn_name(), vars()) + + def accountDownloadEnd(self, accountName: str) -> None: + """ + Call this after a batch updateAccountValue() and updatePortfolio() is sent. + """ + self.logAnswer(current_fn_name(), vars()) + + def nextValidId(self, orderId: int) -> None: + """ + Receives next valid order id. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_next_valid_id(order_id=orderId) + + def contractDetails(self, reqId: int, contractDetails: ContractDetails) -> None: + """ + Receives the full contract's definitions. + + This method will return all + contracts matching the requested via EEClientSocket::reqContractDetails. + For example, one can obtain the whole option chain with it. + + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_contract_details(req_id=reqId, contract_details=contractDetails) + + def bondContractDetails(self, reqId: int, contractDetails: ContractDetails) -> None: + """ + Call this function when the reqContractDetails function has been called for + bonds. + """ + self.logAnswer(current_fn_name(), vars()) + + def contractDetailsEnd(self, reqId: int) -> None: + """ + Call this function once all contract details for a given request are received. + + This helps to define the end of an option chain. + + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_contract_details_end(req_id=reqId) + + def execDetails(self, reqId: int, contract: Contract, execution: Execution) -> None: + """ + Fire this event when the reqExecutions() function is invoked or when an order is + filled. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_exec_details( + req_id=reqId, + contract=contract, + execution=execution, + ) + + def execDetailsEnd(self, reqId: int) -> None: + """ + Call this function once all executions have been sent to a client in response to + reqExecutions(). + """ + self.logAnswer(current_fn_name(), vars()) + + def updateMktDepth( + self, + reqId: TickerId, + position: int, + operation: int, + side: int, + price: float, + size: Decimal, + ) -> None: + """ + Return the order book. + + Parameters + ---------- + reqId : TickerId + The request's identifier. + position : int + The order book's row being updated. + operation : int + How to refresh the row: + - 0: insert (insert this new order into the row identified by 'position') + - 1: update (update the existing order in the row identified by 'position') + - 2: delete (delete the existing order at the row identified by 'position'). + side : int + 0 for ask, 1 for bid. + price : float + The order's price. + size : Decimal + The order's size. + + """ + self.logAnswer(current_fn_name(), vars()) + + def updateMktDepthL2( + self, + reqId: TickerId, + position: int, + marketMaker: str, + operation: int, + side: int, + price: float, + size: Decimal, + isSmartDepth: bool, + ) -> None: + """ + Return the order book. + + Parameters + ---------- + reqId : TickerId + The request's identifier. + position : int + The order book's row being updated. + marketMaker : str + The exchange holding the order. + operation : int + How to refresh the row: + - 0: insert (insert this new order into the row identified by 'position') + - 1: update (update the existing order in the row identified by 'position') + - 2: delete (delete the existing order at the row identified by 'position'). + side : int + 0 for ask, 1 for bid. + price : float + The order's price. + size : Decimal + The order's size. + isSmartDepth : bool + Is SMART Depth request. + + """ + self.logAnswer(current_fn_name(), vars()) + + def updateNewsBulletin( + self, + msgId: int, + msgType: int, + newsMessage: str, + originExch: str, + ) -> None: + """ + Provide IB's bulletins. + + Parameters + ---------- + msgId: int + The bulletin's identifier. + msgType: int + One of: + - 1: Regular news bulletin + - 2: Exchange no longer available for trading + - 3: Exchange is available for trading + newsMessage: str + The message. + originExch: str + The exchange where the message comes from. + + """ + self.logAnswer(current_fn_name(), vars()) + + def managedAccounts(self, accountsList: str) -> None: + """ + Receives a comma-separated string with the managed account ids. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_managed_accounts(accounts_list=accountsList) + + def receiveFA(self, faData: FaDataType, cxml: str) -> None: + """ + Receives the Financial Advisor's configuration available in the TWS. + + Parameters + ---------- + faData : str + One of the following: + - Groups: Offer traders a way to create a group of accounts and apply a single allocation method + to all accounts in the group. + - Profiles: Let you allocate shares on an account-by-account basis using a predefined calculation value. + - Account Aliases: Let you easily identify the accounts by meaningful names rather than account numbers. + cxml : str + The XML-formatted configuration. + + """ + self.logAnswer(current_fn_name(), vars()) + + def historicalData(self, reqId: int, bar: BarData) -> None: + """ + Return the requested historical data bars. + + Parameters + ---------- + reqId : int + The request's identifier. + bar : BarData + The bar's data. + + """ + self.logAnswer(current_fn_name(), vars()) + + def historicalDataEnd(self, reqId: int, start: str, end: str) -> None: + """ + Mark the end of the reception of historical bars. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_historical_data_end(req_id=reqId, start=start, end=end) + + def scannerParameters(self, xml: str) -> None: + """ + Provide the XML-formatted parameters available to create a market scanner. + + Parameters + ---------- + xml : str + The XML-formatted string with the available parameters. + + """ + self.logAnswer(current_fn_name(), vars()) + + def scannerData( + self, + reqId: int, + rank: int, + contractDetails: ContractDetails, + distance: str, + benchmark: str, + projection: str, + legsStr: str, + ) -> None: + """ + Provide the data resulting from the market scanner request. + + Parameters + ---------- + reqId : int + The request's identifier. + rank : int + The ranking within the response of this bar. + contractDetails : ContractDetails + The data's ContractDetails. + distance : str + According to query. + benchmark : str + According to query. + projection : str + According to query. + legsStr : str + Describes the combo legs when the scanner is returning EFP. + + """ + self.logAnswer(current_fn_name(), vars()) + + def scannerDataEnd(self, reqId: int) -> None: + """ + Indicate that scanner data reception has terminated. + + Parameters + ---------- + reqId : int + The request's identifier. + + """ + self.logAnswer(current_fn_name(), vars()) + + def realtimeBar( + self, + reqId: TickerId, + time: int, + open_: float, + high: float, + low: float, + close: float, + volume: Decimal, + wap: Decimal, + count: int, + ) -> None: + """ + Update real-time 5-second bars. + + Parameters + ---------- + reqId : int + The request's identifier. + time : int + Start of the bar in Unix (or 'epoch') time. + open_ : float + The bar's open value. + high : float + The bar's high value. + low : float + The bar's low value. + close : float + The bar's closing value. + volume : int + The bar's traded volume if available. + wap : float + The bar's Weighted Average Price. + count : int + The number of trades during the bar's timespan (only available for TRADES). + + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_realtime_bar( + req_id=reqId, + time=time, + open_=open_, + high=high, + low=low, + close=close, + volume=volume, + wap=wap, + count=count, + ) + + def currentTime(self, time: int) -> None: + """ + Obtain the IB server's system time by calling this method as a result of + invoking `reqCurrentTime`. + """ + self.logAnswer(current_fn_name(), vars()) + + def fundamentalData(self, reqId: TickerId, data: str) -> None: + """ + Call this function to receive fundamental market data. + + Ensure that the appropriate market data subscription is set up in Account + Management before attempting to receive this data. + + """ + self.logAnswer(current_fn_name(), vars()) + + def deltaNeutralValidation( + self, + reqId: int, + deltaNeutralContract: DeltaNeutralContract, + ) -> None: + """ + When accepting a Delta-Neutral RFQ (request for quote), the server sends a + deltaNeutralValidation() message with the DeltaNeutralContract structure. + + If the delta and price fields are empty in the original request, the + confirmation will contain the current values from the server. These values are + locked when the RFQ is processed and remain locked until the RFQ is canceled. + + """ + self.logAnswer(current_fn_name(), vars()) + + def commissionReport(self, commissionReport: CommissionReport) -> None: + """ + Trigger this callback in the following scenarios: + + - Immediately after a trade execution. + - By calling reqExecutions(). + + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_commission_report(commission_report=commissionReport) + + def position( + self, + account: str, + contract: Contract, + position: Decimal, + avgCost: float, + ) -> None: + """ + Return real-time positions for all accounts in response to the reqPositions() + method. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_position( + account_id=account, + contract=contract, + position=position, + avg_cost=avgCost, + ) + + def positionEnd(self) -> None: + """ + Call this once all position data for a given request has been received, serving + as an end marker for the position() data. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_position_end() + + def accountSummary( + self, + reqId: int, + account: str, + tag: str, + value: str, + currency: str, + ) -> None: + """ + Return the data from the TWS Account Window Summary tab in response to + reqAccountSummary(). + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_account_summary( + req_id=reqId, + account_id=account, + tag=tag, + value=value, + currency=currency, + ) + + def accountSummaryEnd(self, reqId: int) -> None: + """ + Call this method when all account summary data for a given request has been + received. + """ + self.logAnswer(current_fn_name(), vars()) + + def verifyCompleted(self, isSuccessful: bool, errorText: str) -> None: + + self.logAnswer(current_fn_name(), vars()) + + def verifyAndAuthMessageAPI(self, apiData: str, xyzChallange: str) -> None: + + self.logAnswer(current_fn_name(), vars()) + + def verifyAndAuthCompleted(self, isSuccessful: bool, errorText: str) -> None: + + self.logAnswer(current_fn_name(), vars()) + + def displayGroupList(self, reqId: int, groups: str) -> None: + """ + Receive a one-time response callback to queryDisplayGroups(). + + Parameters + ---------- + reqId : int + The requestId specified in queryDisplayGroups(). + groups : str + A list of integers representing visible group IDs separated by the '|' character, sorted by most + used group first. This list remains unchanged during the TWS session (i.e., users cannot add new + groups; sorting can change). + + """ + self.logAnswer(current_fn_name(), vars()) + + def displayGroupUpdated(self, reqId: int, contractInfo: str) -> None: + """ + Receive a notification from TWS to the API client after subscribing to group + events via subscribeToGroupEvents(). This notification will be resent if the + chosen contract in the subscribed display group changes. + + Parameters + ---------- + reqId : int + The requestId specified in subscribeToGroupEvents(). + contractInfo : str + The encoded value uniquely representing the contract in IB. Possible values include: + - 'none': Empty selection. + - 'contractID@exchange': For any non-combination contract. + Examples: '8314@SMART' for IBM SMART; '8314@ARCA' for IBM @ARCA. + - 'combo': If any combo is selected. + + """ + self.logAnswer(current_fn_name(), vars()) + + def positionMulti( + self, + reqId: int, + account: str, + modelCode: str, + contract: Contract, + pos: Decimal, + avgCost: float, + ) -> None: + """ + Retrieve the position for a specific account or model, mirroring the position() + function. + """ + self.logAnswer(current_fn_name(), vars()) + + def positionMultiEnd(self, reqId: int) -> None: + """ + Terminate the position for a specific account or model, akin to the + positionEnd() function. + """ + self.logAnswer(current_fn_name(), vars()) + + def accountUpdateMulti( + self, + reqId: int, + account: str, + modelCode: str, + key: str, + value: str, + currency: str, + ) -> None: + """ + Update the value for a specific account or model, similar to the + updateAccountValue() function. + """ + self.logAnswer(current_fn_name(), vars()) + + def accountUpdateMultiEnd(self, reqId: int) -> None: + """ + Download data for a specific account or model, resembling accountDownloadEnd() + functionality. + """ + self.logAnswer(current_fn_name(), vars()) + + def tickOptionComputation( + self, + reqId: TickerId, + tickType: TickType, + tickAttrib: int, + impliedVol: float, + delta: float, + optPrice: float, + pvDividend: float, + gamma: float, + vega: float, + theta: float, + undPrice: float, + ) -> None: + """ + Invoke this function in response to market movements in an option or its + underlier. + + Receive TWS's option model volatilities, prices, and deltas, as well as the + present value of dividends expected on the option's underlier. + + """ + self.logAnswer(current_fn_name(), vars()) + + def securityDefinitionOptionParameter( + self, + reqId: int, + exchange: str, + underlyingConId: int, + tradingClass: str, + multiplier: str, + expirations: SetOfString, + strikes: SetOfFloat, + ) -> None: + """ + Return the option chain for an underlying on a specified exchange. + + This is triggered by a call to `reqSecDefOptParams`. If multiple exchanges are specified in + `reqSecDefOptParams`, there will be multiple callbacks to `securityDefinitionOptionParameter`. + + Parameters + ---------- + reqId : int + ID of the request that initiated the callback. + exchange : str + The exchange for which the option chain is requested. + underlyingConId : int + The conID of the underlying security. + tradingClass : str + The option trading class. + multiplier : str + The option multiplier. + expirations : list[str] + A list of expiry dates for the options of this underlying on this exchange. + strikes : list[float] + A list of possible strikes for options of this underlying on this exchange. + + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_security_definition_option_parameter( + req_id=reqId, + exchange=exchange, + underlying_con_id=underlyingConId, + trading_class=tradingClass, + multiplier=multiplier, + expirations=expirations, + strikes=strikes, + ) + + def securityDefinitionOptionParameterEnd(self, reqId: int) -> None: + """ + Invoke this after all callbacks to securityDefinitionOptionParameter have been + completed. + + Parameters + ---------- + reqId : int + The ID used in the initial call to `securityDefinitionOptionParameter`. + + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_security_definition_option_parameter_end(req_id=reqId) + + def softDollarTiers(self, reqId: int, tiers: list) -> None: + """ + Invoke this upon receiving Soft Dollar Tier configuration information. + + Call this method when Soft Dollar Tier configuration details are received. + + Parameters + ---------- + reqId : int + The request ID used in the call to `EEClient::reqSoftDollarTiers`. + tiers : list[SoftDollarTier] + A list containing all Soft Dollar Tier information. + + """ + self.logAnswer(current_fn_name(), vars()) + + def familyCodes(self, familyCodes: ListOfFamilyCode) -> None: + """ + Return an array of family codes. + """ + self.logAnswer(current_fn_name(), vars()) + + def symbolSamples( + self, + reqId: int, + contractDescriptions: ListOfContractDescription, + ) -> None: + """ + Return an array of sample contract descriptions. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_symbol_samples( + req_id=reqId, + contract_descriptions=contractDescriptions, + ) + + def mktDepthExchanges(self, depthMktDataDescriptions: ListOfDepthExchanges) -> None: + """ + Return an array of exchanges that provide depth data to UpdateMktDepthL2. + """ + self.logAnswer(current_fn_name(), vars()) + + def tickNews( + self, + tickerId: int, + timeStamp: int, + providerCode: str, + articleId: str, + headline: str, + extraData: str, + ) -> None: + """ + Return news headlines. + """ + self.logAnswer(current_fn_name(), vars()) + + def smartComponents(self, reqId: int, smartComponentMap: SmartComponentMap) -> None: + """ + Return exchange component mapping. + """ + self.logAnswer(current_fn_name(), vars()) + + def tickReqParams( + self, + tickerId: int, + minTick: float, + bboExchange: str, + snapshotPermissions: int, + ) -> None: + """ + Return the exchange map for a specific contract. + """ + self.logAnswer(current_fn_name(), vars()) + + def newsProviders(self, newsProviders: ListOfNewsProviders) -> None: + """ + Return available and subscribed API news providers. + """ + self.logAnswer(current_fn_name(), vars()) + + def newsArticle(self, requestId: int, articleType: int, articleText: str) -> None: + """ + Return the body of a news article. + """ + self.logAnswer(current_fn_name(), vars()) + + def historicalNews( + self, + requestId: int, + time: str, + providerCode: str, + articleId: str, + headline: str, + ) -> None: + """ + Return historical news headlines. + """ + self.logAnswer(current_fn_name(), vars()) + + def historicalNewsEnd(self, requestId: int, hasMore: bool) -> None: + """ + Signals end of historical news. + """ + self.logAnswer(current_fn_name(), vars()) + + def headTimestamp(self, reqId: int, headTimestamp: str) -> None: + """ + Return the earliest available data for a specific type of data for a given + contract. + """ + self.logAnswer(current_fn_name(), vars()) + + def histogramData(self, reqId: int, items: HistogramData) -> None: + """ + Return histogram data for a contract. + """ + self.logAnswer(current_fn_name(), vars()) + + def historicalDataUpdate(self, reqId: int, bar: BarData) -> None: + """ + Return updates in real time when keepUpToDate is set to True. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_historical_data_update( + req_id=reqId, + bar=bar, + ) + + def rerouteMktDataReq(self, reqId: int, conId: int, exchange: str) -> None: + """ + Return rerouted CFD contract information for a market data request. + """ + self.logAnswer(current_fn_name(), vars()) + + def rerouteMktDepthReq(self, reqId: int, conId: int, exchange: str) -> None: + """ + Return rerouted CFD contract information for a market depth request. + """ + self.logAnswer(current_fn_name(), vars()) + + def marketRule(self, marketRuleId: int, priceIncrements: ListOfPriceIncrements) -> None: + """ + Return the minimum price increment structure for a specific market rule ID. + """ + self.logAnswer(current_fn_name(), vars()) + + def pnl(self, reqId: int, dailyPnL: float, unrealizedPnL: float, realizedPnL: float) -> None: + """ + Return the daily Profit and Loss (PnL) for the account. + """ + self.logAnswer(current_fn_name(), vars()) + + def pnlSingle( + self, + reqId: int, + pos: Decimal, + dailyPnL: float, + unrealizedPnL: float, + realizedPnL: float, + value: float, + ) -> None: + """ + Return the daily Profit and Loss (PnL) for a single position in the account. + """ + self.logAnswer(current_fn_name(), vars()) + + def historicalTicks(self, reqId: int, ticks: ListOfHistoricalTick, done: bool) -> None: + """ + Return historical tick data when whatToShow is set to MIDPOINT. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_historical_ticks( + req_id=reqId, + ticks=ticks, + done=done, + ) + + def historicalTicksBidAsk( + self, + reqId: int, + ticks: ListOfHistoricalTickBidAsk, + done: bool, + ) -> None: + """ + Return historical tick data when whatToShow is set to BID_ASK. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_historical_ticks_bid_ask( + req_id=reqId, + ticks=ticks, + done=done, + ) + + def historicalTicksLast(self, reqId: int, ticks: ListOfHistoricalTickLast, done: bool) -> None: + """ + Return historical tick data when whatToShow is set to TRADES. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_historical_ticks_last( + req_id=reqId, + ticks=ticks, + done=done, + ) + + def tickByTickAllLast( + self, + reqId: int, + tickType: int, + time: int, + price: float, + size: Decimal, + tickAttribLast: TickAttribLast, + exchange: str, + specialConditions: str, + ) -> None: + """ + Return tick-by-tick data for tickType set to "Last" or "AllLast". + """ + self.logAnswer(current_fn_name(), vars()) + self._process_tick_by_tick_all_last( + req_id=reqId, + tick_type=tickType, + time=time, + price=price, + size=size, + tick_attrib_last=tickAttribLast, + exchange=exchange, + special_conditions=specialConditions, + ) + + def tickByTickBidAsk( + self, + reqId: int, + time: int, + bidPrice: float, + askPrice: float, + bidSize: Decimal, + askSize: Decimal, + tickAttribBidAsk: TickAttribBidAsk, + ) -> None: + """ + Return tick-by-tick data for tickType set to "BidAsk". + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_tick_by_tick_bid_ask( + req_id=reqId, + time=time, + bid_price=bidPrice, + ask_price=askPrice, + bid_size=bidSize, + ask_size=askSize, + tick_attrib_bid_ask=tickAttribBidAsk, + ) + + def tickByTickMidPoint(self, reqId: int, time: int, midPoint: float) -> None: + """ + Return tick-by-tick data for tickType set to "MidPoint". + """ + self.logAnswer(current_fn_name(), vars()) + + def orderBound(self, reqId: int, apiClientId: int, apiOrderId: int) -> None: + """ + Return the orderBound notification. + """ + self.logAnswer(current_fn_name(), vars()) + + def completedOrder(self, contract: Contract, order: Order, orderState: OrderState) -> None: + """ + Feed in completed orders. + + Call this function to provide information on completed orders. + + Parameters + ---------- + contract : Contract + Describes the contract with attributes of the Contract class. + order : Order + Details of the completed order, as defined by the Order class. + orderState : OrderState + Includes status details of the completed order, as specified in the OrderState class. + + """ + self.logAnswer(current_fn_name(), vars()) + + def completedOrdersEnd(self) -> None: + """ + Invoke this upon completing a request for completed orders. + """ + self.logAnswer(current_fn_name(), vars()) + + def replaceFAEnd(self, reqId: int, text: str) -> None: + """ + Invoke this at the completion of a Financial Advisor (FA) replacement operation. + """ + self.logAnswer(current_fn_name(), vars()) + + def wshMetaData(self, reqId: int, dataJson: str) -> None: + self.logAnswer(current_fn_name(), vars()) + + def wshEventData(self, reqId: int, dataJson: str) -> None: + self.logAnswer(current_fn_name(), vars()) + + def historicalSchedule( + self, + reqId: int, + startDateTime: str, + endDateTime: str, + timeZone: str, + sessions: ListOfHistoricalSessions, + ) -> None: + """ + Return historical schedule for historical data request with whatToShow=SCHEDULE. + """ + self.logAnswer(current_fn_name(), vars()) + + def userInfo(self, reqId: int, whiteBrandingId: str) -> None: + """ + Return user info. + """ + self.logAnswer(current_fn_name(), vars()) diff --git a/nautilus_trader/adapters/interactive_brokers/data.py b/nautilus_trader/adapters/interactive_brokers/data.py index 3280ff737169..dd4384ced322 100644 --- a/nautilus_trader/adapters/interactive_brokers/data.py +++ b/nautilus_trader/adapters/interactive_brokers/data.py @@ -94,11 +94,7 @@ def __init__( cache=cache, clock=clock, instrument_provider=instrument_provider, - config=config, # TODO: Config needs to be fully formed earlier than this - # config={ - # "name": f"{type(self).__name__}-{ibg_client_id:03d}", - # "client_id": ibg_client_id, - # }, + config=config, ) self._client = client self._handle_revised_bars = config.handle_revised_bars diff --git a/nautilus_trader/adapters/interactive_brokers/factories.py b/nautilus_trader/adapters/interactive_brokers/factories.py index 66098b03fa71..120460855c58 100644 --- a/nautilus_trader/adapters/interactive_brokers/factories.py +++ b/nautilus_trader/adapters/interactive_brokers/factories.py @@ -103,6 +103,7 @@ def get_cached_ib_client( port=port, client_id=client_id, ) + client.start() IB_CLIENTS[client_key] = client return IB_CLIENTS[client_key] diff --git a/nautilus_trader/adapters/interactive_brokers/historic/client.py b/nautilus_trader/adapters/interactive_brokers/historic/client.py index 2770b1ff56d9..8237c00ed38c 100644 --- a/nautilus_trader/adapters/interactive_brokers/historic/client.py +++ b/nautilus_trader/adapters/interactive_brokers/historic/client.py @@ -87,7 +87,7 @@ def __init__( async def _connect(self) -> None: # Connect client - await self._client.wait_until_ready() + self._client.start() self._client.registered_nautilus_clients.add(1) # Set Market Data Type diff --git a/nautilus_trader/adapters/interactive_brokers/providers.py b/nautilus_trader/adapters/interactive_brokers/providers.py index 4f0e43e9c183..c804fc3406f7 100644 --- a/nautilus_trader/adapters/interactive_brokers/providers.py +++ b/nautilus_trader/adapters/interactive_brokers/providers.py @@ -102,6 +102,9 @@ async def get_contract_details( ) -> list[ContractDetails]: try: details = await self._client.get_contract_details(contract=contract) + if not details: + self._log.error(f"No contract details returned for {contract}.") + return [] [qualified] = details self._log.info( f"Contract qualified for {qualified.contract.localSymbol}." diff --git a/nautilus_trader/cache/database.pxd b/nautilus_trader/cache/database.pxd index 52a6a2955ff5..21cd9afbf938 100644 --- a/nautilus_trader/cache/database.pxd +++ b/nautilus_trader/cache/database.pxd @@ -18,32 +18,5 @@ from nautilus_trader.serialization.base cimport Serializer cdef class CacheDatabaseAdapter(CacheDatabaseFacade): - cdef str _key_trader - cdef str _key_general - cdef str _key_currencies - cdef str _key_instruments - cdef str _key_synthetics - cdef str _key_accounts - cdef str _key_orders - cdef str _key_positions - cdef str _key_actors - cdef str _key_strategies - - cdef str _key_index_order_ids - cdef str _key_index_order_position - cdef str _key_index_order_client - cdef str _key_index_orders - cdef str _key_index_orders_open - cdef str _key_index_orders_closed - cdef str _key_index_orders_emulated - cdef str _key_index_orders_inflight - cdef str _key_index_positions - cdef str _key_index_positions_open - cdef str _key_index_positions_closed - - cdef str _key_snapshots_orders - cdef str _key_snapshots_positions - cdef str _key_heartbeat - cdef Serializer _serializer cdef object _backing diff --git a/nautilus_trader/cache/database.pyx b/nautilus_trader/cache/database.pyx index 696fe39602c1..bed0e6c0cad5 100644 --- a/nautilus_trader/cache/database.pyx +++ b/nautilus_trader/cache/database.pyx @@ -154,34 +154,6 @@ cdef class CacheDatabaseAdapter(CacheDatabaseFacade): self._log.info(f"{config.use_trader_prefix=}", LogColor.BLUE) self._log.info(f"{config.use_instance_id=}", LogColor.BLUE) - # Database keys - self._key_trader = f"{_TRADER}-{trader_id}" # noqa - self._key_general = f"{self._key_trader}:{_GENERAL}:" # noqa - self._key_currencies = f"{self._key_trader}:{_CURRENCIES}:" # noqa - self._key_instruments = f"{self._key_trader}:{_INSTRUMENTS}:" # noqa - self._key_synthetics = f"{self._key_trader}:{_SYNTHETICS}:" # noqa - self._key_accounts = f"{self._key_trader}:{_ACCOUNTS}:" # noqa - self._key_orders = f"{self._key_trader}:{_ORDERS}:" # noqa - self._key_positions = f"{self._key_trader}:{_POSITIONS}:" # noqa - self._key_actors = f"{self._key_trader}:{_ACTORS}:" # noqa - self._key_strategies = f"{self._key_trader}:{_STRATEGIES}:" # noqa - - self._key_index_order_ids = f"{self._key_trader}:{_INDEX_ORDER_IDS}:" - self._key_index_order_position = f"{self._key_trader}:{_INDEX_ORDER_POSITION}:" - self._key_index_order_client = f"{self._key_trader}:{_INDEX_ORDER_CLIENT}:" - self._key_index_orders = f"{self._key_trader}:{_INDEX_ORDERS}" - self._key_index_orders_open = f"{self._key_trader}:{_INDEX_ORDERS_OPEN}" - self._key_index_orders_closed = f"{self._key_trader}:{_INDEX_ORDERS_CLOSED}" - self._key_index_orders_emulated = f"{self._key_trader}:{_INDEX_ORDERS_EMULATED}" - self._key_index_orders_inflight = f"{self._key_trader}:{_INDEX_ORDERS_INFLIGHT}" - self._key_index_positions = f"{self._key_trader}:{_INDEX_POSITIONS}" - self._key_index_positions_open = f"{self._key_trader}:{_INDEX_POSITIONS_OPEN}" - self._key_index_positions_closed = f"{self._key_trader}:{_INDEX_POSITIONS_CLOSED}" - - self._key_snapshots_orders = f"{self._key_trader}:{_SNAPSHOTS_ORDERS}:" - self._key_snapshots_positions = f"{self._key_trader}:{_SNAPSHOTS_POSITIONS}:" - self._key_heartbeat = f"{self._key_trader}:{_HEARTBEAT}" - self._serializer = serializer self._backing = nautilus_pyo3.RedisCacheDatabase( @@ -242,7 +214,7 @@ cdef class CacheDatabaseAdapter(CacheDatabaseFacade): """ cdef dict general = {} - cdef list general_keys = self._backing.keys(f"*:{_GENERAL}:*") + cdef list general_keys = self._backing.keys(f"{_GENERAL}:*") if not general_keys: return general @@ -271,7 +243,7 @@ cdef class CacheDatabaseAdapter(CacheDatabaseFacade): """ cdef dict currencies = {} - cdef list currency_keys = self._backing.keys(f"*:{_CURRENCIES}*") + cdef list currency_keys = self._backing.keys(f"{_CURRENCIES}*") if not currency_keys: return currencies @@ -299,7 +271,7 @@ cdef class CacheDatabaseAdapter(CacheDatabaseFacade): """ cdef dict instruments = {} - cdef list instrument_keys = self._backing.keys(f"*:{_INSTRUMENTS}*") + cdef list instrument_keys = self._backing.keys(f"{_INSTRUMENTS}*") if not instrument_keys: return instruments @@ -327,7 +299,7 @@ cdef class CacheDatabaseAdapter(CacheDatabaseFacade): """ cdef dict synthetics = {} - cdef list synthetic_keys = self._backing.keys(f"*:{_SYNTHETICS}*") + cdef list synthetic_keys = self._backing.keys(f"{_SYNTHETICS}*") if not synthetic_keys: return synthetics @@ -355,7 +327,7 @@ cdef class CacheDatabaseAdapter(CacheDatabaseFacade): """ cdef dict accounts = {} - cdef list account_keys = self._backing.keys(f"*:{_ACCOUNTS}*") + cdef list account_keys = self._backing.keys(f"{_ACCOUNTS}*") if not account_keys: return accounts @@ -384,7 +356,7 @@ cdef class CacheDatabaseAdapter(CacheDatabaseFacade): """ cdef dict orders = {} - cdef list order_keys = self._backing.keys(f"*:{_ORDERS}*") + cdef list order_keys = self._backing.keys(f"{_ORDERS}*") if not order_keys: return orders @@ -412,7 +384,7 @@ cdef class CacheDatabaseAdapter(CacheDatabaseFacade): """ cdef dict positions = {} - cdef list position_keys = self._backing.keys(f"*:{_POSITIONS}*") + cdef list position_keys = self._backing.keys(f"{_POSITIONS}*") if not position_keys: return positions diff --git a/nautilus_trader/common/component.pxd b/nautilus_trader/common/component.pxd index 09f9678c308d..6c0b57d6f97f 100644 --- a/nautilus_trader/common/component.pxd +++ b/nautilus_trader/common/component.pxd @@ -29,6 +29,7 @@ from nautilus_trader.core.rust.common cimport ComponentState from nautilus_trader.core.rust.common cimport ComponentTrigger from nautilus_trader.core.rust.common cimport LiveClock_API from nautilus_trader.core.rust.common cimport LogColor +from nautilus_trader.core.rust.common cimport LogGuard_API from nautilus_trader.core.rust.common cimport LogLevel from nautilus_trader.core.rust.common cimport MessageBus_API from nautilus_trader.core.rust.common cimport TestClock_API @@ -136,7 +137,12 @@ cpdef str log_color_to_str(LogColor value) cpdef LogLevel log_level_from_str(str value) cpdef str log_level_to_str(LogLevel value) -cpdef void init_logging( + +cdef class LogGuard: + cdef LogGuard_API _mem + + +cpdef LogGuard init_logging( TraderId trader_id=*, str machine_id=*, UUID4 instance_id=*, diff --git a/nautilus_trader/common/component.pyx b/nautilus_trader/common/component.pyx index ee5675c5120c..3023f239ee0c 100644 --- a/nautilus_trader/common/component.pyx +++ b/nautilus_trader/common/component.pyx @@ -59,6 +59,7 @@ from nautilus_trader.core.message cimport Event from nautilus_trader.core.rust.common cimport ComponentState from nautilus_trader.core.rust.common cimport ComponentTrigger from nautilus_trader.core.rust.common cimport LogColor +from nautilus_trader.core.rust.common cimport LogGuard_API from nautilus_trader.core.rust.common cimport LogLevel from nautilus_trader.core.rust.common cimport TimeEventHandler_t from nautilus_trader.core.rust.common cimport component_state_from_cstr @@ -82,7 +83,7 @@ from nautilus_trader.core.rust.common cimport log_color_from_cstr from nautilus_trader.core.rust.common cimport log_color_to_cstr from nautilus_trader.core.rust.common cimport log_level_from_cstr from nautilus_trader.core.rust.common cimport log_level_to_cstr -from nautilus_trader.core.rust.common cimport logger_flush +from nautilus_trader.core.rust.common cimport logger_drop from nautilus_trader.core.rust.common cimport logger_log from nautilus_trader.core.rust.common cimport logging_clock_set_realtime_mode from nautilus_trader.core.rust.common cimport logging_clock_set_static_mode @@ -1023,7 +1024,19 @@ cpdef str log_level_to_str(LogLevel value): return cstr_to_pystr(log_level_to_cstr(value)) -cpdef void init_logging( +cdef class LogGuard: + """ + Provides a `LogGuard` which serves as a token to signal the initialization + of the logging system. It also ensures that the global logger is flushed + of any buffered records when the instance is destroyed. + """ + + def __del__(self) -> None: + if self._mem._0 != NULL: + logger_drop(self._mem) + + +cpdef LogGuard init_logging( TraderId trader_id = None, str machine_id = None, UUID4 instance_id = None, @@ -1043,7 +1056,8 @@ cpdef void init_logging( Acts as an interface into the logging system implemented in Rust with the `log` crate. This function should only be called once per process, at the beginning of the application - run. + run. Subsequent calls will raise a `RuntimeError`, as there can only be one `LogGuard` + per initialized system. Parameters ---------- @@ -1076,6 +1090,15 @@ cpdef void init_logging( print_config : bool, default False If the core logging configuration should be printed to stdout on initialization. + Returns + ------- + LogGuard + + Raises + ------ + RuntimeError + If the logging system has already been initialized. + """ if trader_id is None: trader_id = TraderId("TRADER-000") @@ -1084,20 +1107,27 @@ cpdef void init_logging( if instance_id is None: instance_id = UUID4() - if not logging_is_initialized(): - logging_init( - trader_id._mem, - instance_id._mem, - level_stdout, - level_file, - pystr_to_cstr(directory) if directory else NULL, - pystr_to_cstr(file_name) if file_name else NULL, - pystr_to_cstr(file_format) if file_format else NULL, - pybytes_to_cstr(msgspec.json.encode(component_levels)) if component_levels else NULL, - colors, - bypass, - print_config, - ) + if logging_is_initialized(): + raise RuntimeError("Logging system already initialized") + + cdef LogGuard_API log_guard_api = logging_init( + trader_id._mem, + instance_id._mem, + level_stdout, + level_file, + pystr_to_cstr(directory) if directory else NULL, + pystr_to_cstr(file_name) if file_name else NULL, + pystr_to_cstr(file_format) if file_format else NULL, + pybytes_to_cstr(msgspec.json.encode(component_levels)) if component_levels else NULL, + colors, + bypass, + print_config, + ) + + cdef LogGuard log_guard = LogGuard.__new__(LogGuard) + log_guard._mem = log_guard_api + return log_guard + LOGGING_PYO3 = False diff --git a/nautilus_trader/common/config.py b/nautilus_trader/common/config.py index 9afc90cb6503..df7bc5a2cf05 100644 --- a/nautilus_trader/common/config.py +++ b/nautilus_trader/common/config.py @@ -247,8 +247,11 @@ class DatabaseConfig(NautilusConfig, frozen=True): The account username for the database connection. password : str, optional The account password for the database connection. + If a value is provided then it will be redacted in the string repr for this object. ssl : bool, default False If database should use an SSL enabled connection. + timeout : int, default 20 + The timeout (seconds) to wait for a new connection. Notes ----- @@ -262,6 +265,25 @@ class DatabaseConfig(NautilusConfig, frozen=True): username: str | None = None password: str | None = None ssl: bool = False + timeout: int | None = 20 + + def __repr__(self) -> str: + redacted_password = "None" + if self.password: + if len(self.password) >= 4: + redacted_password = f"{self.password[:2]}...{self.password[-2:]}" + else: + redacted_password = self.password + return ( + f"{type(self).__name__}(" + f"type={self.type}, " + f"host={self.host}, " + f"port={self.port}, " + f"username={self.username}, " + f"password={redacted_password}, " + f"ssl={self.ssl}, " + f"timeout={self.timeout})" + ) class MessageBusConfig(NautilusConfig, frozen=True): diff --git a/nautilus_trader/core/includes/common.h b/nautilus_trader/core/includes/common.h index ebaa8c58520d..b10ca55c5cab 100644 --- a/nautilus_trader/core/includes/common.h +++ b/nautilus_trader/core/includes/common.h @@ -195,6 +195,8 @@ typedef enum LogLevel { typedef struct LiveClock LiveClock; +typedef struct LogGuard LogGuard; + /** * Provides a generic message bus to facilitate various messaging patterns. * @@ -250,6 +252,20 @@ typedef struct LiveClock_API { struct LiveClock *_0; } LiveClock_API; +/** + * Provides a C compatible Foreign Function Interface (FFI) for an underlying [`LogGuard`]. + * + * This struct wraps `LogGuard` in a way that makes it compatible with C function + * calls, enabling interaction with `LogGuard` in a C environment. + * + * It implements the `Deref` trait, allowing instances of `LogGuard_API` to be + * dereferenced to `LogGuard`, providing access to `LogGuard`'s methods without + * having to manually access the underlying `LogGuard` instance. + */ +typedef struct LogGuard_API { + struct LogGuard *_0; +} LogGuard_API; + /** * Provides a C compatible Foreign Function Interface (FFI) for an underlying [`MessageBus`]. * @@ -534,17 +550,17 @@ enum LogColor log_color_from_cstr(const char *ptr); * - Assume `file_format_ptr` is either NULL or a valid C string pointer. * - Assume `component_level_ptr` is either NULL or a valid C string pointer. */ -void logging_init(TraderId_t trader_id, - UUID4_t instance_id, - enum LogLevel level_stdout, - enum LogLevel level_file, - const char *directory_ptr, - const char *file_name_ptr, - const char *file_format_ptr, - const char *component_levels_ptr, - uint8_t is_colored, - uint8_t is_bypassed, - uint8_t print_config); +struct LogGuard_API logging_init(TraderId_t trader_id, + UUID4_t instance_id, + enum LogLevel level_stdout, + enum LogLevel level_file, + const char *directory_ptr, + const char *file_name_ptr, + const char *file_format_ptr, + const char *component_levels_ptr, + uint8_t is_colored, + uint8_t is_bypassed, + uint8_t print_config); /** * Creates a new log event. @@ -582,9 +598,9 @@ void logging_log_header(TraderId_t trader_id, void logging_log_sysinfo(const char *component_ptr); /** - * Flushes global logger buffers. + * Flushes global logger buffers of any records. */ -void logger_flush(void); +void logger_drop(struct LogGuard_API log_guard); /** * # Safety diff --git a/nautilus_trader/core/includes/core.h b/nautilus_trader/core/includes/core.h index e91027c98faa..0ba5678d6758 100644 --- a/nautilus_trader/core/includes/core.h +++ b/nautilus_trader/core/includes/core.h @@ -43,7 +43,7 @@ typedef struct CVec { */ typedef struct UUID4_t { /** - * The UUID v4 C string value as a fixed-length byte array. + * The UUID v4 value as a fixed-length C string byte array (includes null terminator). */ uint8_t value[37]; } UUID4_t; diff --git a/nautilus_trader/core/includes/model.h b/nautilus_trader/core/includes/model.h index bdd87dbbc51d..ba453f410485 100644 --- a/nautilus_trader/core/includes/model.h +++ b/nautilus_trader/core/includes/model.h @@ -276,7 +276,7 @@ typedef enum InstrumentCloseType { */ typedef enum LiquiditySide { /** - * No specific liqudity side. + * No liquidity side specified. */ NO_LIQUIDITY_SIDE = 0, /** @@ -893,14 +893,15 @@ typedef struct QuoteTick_t { * Represents a valid trade match ID (assigned by a trading venue). * * Maximum length is 36 characters. - * Can correspond to the `TradeID <1003> field` of the FIX protocol. * * The unique ID assigned to the trade entity once it is received or matched by * the exchange or central counterparty. + * + * Can correspond to the `TradeID <1003> field` of the FIX protocol. */ typedef struct TradeId_t { /** - * The trade match ID C string value as a fixed-length byte array. + * The trade match ID value as a fixed-length C string byte array (includes null terminator). */ uint8_t value[37]; } TradeId_t; diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index ee7a99997d1d..43417c96fa9a 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -197,6 +197,14 @@ def convert_to_snake_case(s: str) -> str: ### Logging +class LogGuard: + """ + Provides a `LogGuard` which serves as a token to signal the initialization + of the logging system. It also ensures that the global logger is flushed + of any buffered records when the instance is destroyed. + + """ + def init_tracing() -> None: ... @@ -212,7 +220,7 @@ def init_logging( is_colored: bool | None = None, is_bypassed: bool | None = None, print_config: bool | None = None, -) -> None: ... +) -> LogGuard: ... def log_header( trader_id: TraderId, @@ -866,7 +874,87 @@ class VenueOrderId: ### Orders -class LimitOrder: ... +class LimitOrder: + def __init__( + self, + trader_id: TraderId, + strategy_id: StrategyId, + instrument_id: InstrumentId, + client_order_id: ClientOrderId, + order_side: OrderSide, + quantity: Quantity, + price: Price, + time_in_force: TimeInForce, + post_only: bool, + reduce_only: bool, + quote_quantity: bool, + init_id: UUID4, + ts_init: int, + expire_time: int | None = None, + display_qty: Quantity | None = None, + emulation_trigger: TriggerType | None = None, + trigger_instrument_id: InstrumentId | None = None, + contingency_type: ContingencyType | None = None, + order_list_id: OrderListId | None = None, + linked_order_ids: list[ClientOrderId] | None = None, + parent_order_id: ClientOrderId | None = None, + exec_algorithm_id: ExecAlgorithmId | None = None, + exec_algorithm_params: dict[str, str] | None = None, + exec_spawn_id: ClientOrderId | None = None, + tags: str | None = None, + ): ... + def to_dict(self) -> dict[str, str]: ... + @property + def trader_id(self) -> TraderId: ... + @property + def strategy_id(self) -> StrategyId: ... + @property + def instrument_id(self) -> InstrumentId: ... + @property + def client_order_id(self) -> ClientOrderId: ... + @property + def order_type(self) -> OrderType: ... + @property + def side(self) -> OrderSide: ... + @property + def quantity(self) -> Quantity: ... + @property + def price(self) -> Price: ... + @property + def expire_time(self) -> int | None: ... + @property + def status(self) -> OrderStatus: ... + @property + def time_in_force(self) -> TimeInForce: ... + @property + def is_post_only(self) -> bool: ... + @property + def is_reduce_only(self) -> bool: ... + @property + def is_quote_quantity(self) -> bool: ... + @property + def has_price(self) -> bool: ... + @property + def has_trigger_price(self) -> bool: ... + @property + def is_passive(self) -> bool: ... + @property + def is_aggressive(self) -> bool: ... + @property + def is_open(self) -> bool: ... + @property + def is_closed(self) -> bool: ... + @property + def is_emulated(self) -> bool: ... + @property + def is_active_local(self) -> bool: ... + @property + def is_primary(self) -> bool: ... + @property + def is_spawned(self) -> bool: ... + def from_dict(cls, values: dict[str, str]) -> LimitOrder: ... + + class LimitIfTouchedOrder: ... class MarketOrder: @@ -892,6 +980,7 @@ class MarketOrder: exec_spawn_id: ClientOrderId | None = None, tags: str | None = None, ) -> None: ... + def to_dict(self) -> dict[str, str]: ... @staticmethod def opposite_side(side: OrderSide) -> OrderSide: ... @staticmethod @@ -1242,6 +1331,8 @@ class FuturesContract: lot_size: Quantity, ts_event: int, ts_init: int, + margin_init: Decimal | None = None, + margin_maint: Decimal | None = None, max_quantity: Quantity | None = None, min_quantity: Quantity | None = None, max_price: Price | None = None, @@ -1249,6 +1340,8 @@ class FuturesContract: exchange: str | None = None, info: dict[str, Any] | None = None, ) -> None: ... + @classmethod + def from_dict(cls, values: dict[str, str]) -> CryptoFuture: ... @property def id(self) -> InstrumentId: ... @property @@ -1288,9 +1381,13 @@ class FuturesSpread: min_quantity: Quantity | None = None, max_price: Price | None = None, min_price: Price | None = None, + margin_init: Decimal | None = None, + margin_maint: Decimal | None = None, exchange: str | None = None, info: dict[str, Any] | None = None, ) -> None: ... + @classmethod + def from_dict(cls, values: dict[str, str]) -> FuturesSpread: ... @property def id(self) -> InstrumentId: ... @property @@ -1331,9 +1428,13 @@ class OptionsContract: min_quantity: Quantity | None = None, max_price: Price | None = None, min_price: Price | None = None, + margin_init: Decimal | None = None, + margin_maint: Decimal | None = None, exchange: str | None = None, info: dict[str, Any] | None = None, ) -> None : ... + @classmethod + def from_dict(cls, values: dict[str, str]) -> OptionsContract: ... @property def id(self) -> InstrumentId: ... @property @@ -1373,9 +1474,13 @@ class OptionsSpread: min_quantity: Quantity | None = None, max_price: Price | None = None, min_price: Price | None = None, + margin_init: Decimal | None = None, + margin_maint: Decimal | None = None, exchange: str | None = None, info: dict[str, Any] | None = None, ) -> None : ... + @classmethod + def from_dict(cls, values: dict[str, str]) -> OptionsContract: ... @property def id(self) -> InstrumentId: ... @property @@ -2427,7 +2532,7 @@ class DatabentoHistoricalClient: async def get_range_instruments( self, dataset: str, - symbols: str, + symbols: list[str], start: int, end: int | None = None, limit: int | None = None, @@ -2435,7 +2540,7 @@ class DatabentoHistoricalClient: async def get_range_quotes( self, dataset: str, - symbols: str, + symbols: list[str], start: int, end: int | None = None, limit: int | None = None, @@ -2443,7 +2548,7 @@ class DatabentoHistoricalClient: async def get_range_trades( self, dataset: str, - symbols: str, + symbols: list[str], start: int, end: int | None = None, limit: int | None = None, @@ -2451,7 +2556,7 @@ class DatabentoHistoricalClient: async def get_range_bars( self, dataset: str, - symbols: str, + symbols: list[str], aggregation: BarAggregation, start: int, end: int | None = None, @@ -2460,7 +2565,7 @@ class DatabentoHistoricalClient: async def get_range_imbalance( self, dataset: str, - symbols: str, + symbols: list[str], start: int, end: int | None = None, limit: int | None = None, @@ -2468,7 +2573,7 @@ class DatabentoHistoricalClient: async def get_range_statistics( self, dataset: str, - symbols: str, + symbols: list[str], start: int, end: int | None = None, limit: int | None = None, @@ -2492,7 +2597,7 @@ class DatabentoLiveClient: def subscribe( self, schema: str, - symbols: str, + symbols: list[str], stype_in: str | None = None, start: int | None = None, ) -> dict[str, str]: ... diff --git a/nautilus_trader/core/rust/common.pxd b/nautilus_trader/core/rust/common.pxd index f207994af6c1..a49b134a9878 100644 --- a/nautilus_trader/core/rust/common.pxd +++ b/nautilus_trader/core/rust/common.pxd @@ -104,6 +104,9 @@ cdef extern from "../includes/common.h": cdef struct LiveClock: pass + cdef struct LogGuard: + pass + # Provides a generic message bus to facilitate various messaging patterns. # # The bus provides both a producer and consumer API for Pub/Sub, Req/Rep, as @@ -153,6 +156,17 @@ cdef extern from "../includes/common.h": cdef struct LiveClock_API: LiveClock *_0; + # Provides a C compatible Foreign Function Interface (FFI) for an underlying [`LogGuard`]. + # + # This struct wraps `LogGuard` in a way that makes it compatible with C function + # calls, enabling interaction with `LogGuard` in a C environment. + # + # It implements the `Deref` trait, allowing instances of `LogGuard_API` to be + # dereferenced to `LogGuard`, providing access to `LogGuard`'s methods without + # having to manually access the underlying `LogGuard` instance. + cdef struct LogGuard_API: + LogGuard *_0; + # Provides a C compatible Foreign Function Interface (FFI) for an underlying [`MessageBus`]. # # This struct wraps `MessageBus` in a way that makes it compatible with C function @@ -370,17 +384,17 @@ cdef extern from "../includes/common.h": # - Assume `file_name_ptr` is either NULL or a valid C string pointer. # - Assume `file_format_ptr` is either NULL or a valid C string pointer. # - Assume `component_level_ptr` is either NULL or a valid C string pointer. - void logging_init(TraderId_t trader_id, - UUID4_t instance_id, - LogLevel level_stdout, - LogLevel level_file, - const char *directory_ptr, - const char *file_name_ptr, - const char *file_format_ptr, - const char *component_levels_ptr, - uint8_t is_colored, - uint8_t is_bypassed, - uint8_t print_config); + LogGuard_API logging_init(TraderId_t trader_id, + UUID4_t instance_id, + LogLevel level_stdout, + LogLevel level_file, + const char *directory_ptr, + const char *file_name_ptr, + const char *file_format_ptr, + const char *component_levels_ptr, + uint8_t is_colored, + uint8_t is_bypassed, + uint8_t print_config); # Creates a new log event. # @@ -411,8 +425,8 @@ cdef extern from "../includes/common.h": # - Assumes `component_ptr` is a valid C string pointer. void logging_log_sysinfo(const char *component_ptr); - # Flushes global logger buffers. - void logger_flush(); + # Flushes global logger buffers of any records. + void logger_drop(LogGuard_API log_guard); # # Safety # diff --git a/nautilus_trader/core/rust/core.pxd b/nautilus_trader/core/rust/core.pxd index d76a944ffb53..58acfd03d7f8 100644 --- a/nautilus_trader/core/rust/core.pxd +++ b/nautilus_trader/core/rust/core.pxd @@ -30,7 +30,7 @@ cdef extern from "../includes/core.h": # Represents a pseudo-random UUID (universally unique identifier) # version 4 based on a 128-bit label as specified in RFC 4122. cdef struct UUID4_t: - # The UUID v4 C string value as a fixed-length byte array. + # The UUID v4 value as a fixed-length C string byte array (includes null terminator). uint8_t value[37]; # Converts seconds to nanoseconds (ns). diff --git a/nautilus_trader/core/rust/model.pxd b/nautilus_trader/core/rust/model.pxd index 0d2de60ffd04..a83b17530744 100644 --- a/nautilus_trader/core/rust/model.pxd +++ b/nautilus_trader/core/rust/model.pxd @@ -150,7 +150,7 @@ cdef extern from "../includes/model.h": # The liqudity side for a trade in a financial market. cpdef enum LiquiditySide: - # No specific liqudity side. + # No liquidity side specified. NO_LIQUIDITY_SIDE # = 0, # The order passively provided liqudity to the market to complete the trade (made a market). MAKER # = 1, @@ -492,12 +492,13 @@ cdef extern from "../includes/model.h": # Represents a valid trade match ID (assigned by a trading venue). # # Maximum length is 36 characters. - # Can correspond to the `TradeID <1003> field` of the FIX protocol. # # The unique ID assigned to the trade entity once it is received or matched by # the exchange or central counterparty. + # + # Can correspond to the `TradeID <1003> field` of the FIX protocol. cdef struct TradeId_t: - # The trade match ID C string value as a fixed-length byte array. + # The trade match ID value as a fixed-length C string byte array (includes null terminator). uint8_t value[37]; # Represents a single trade tick in a financial market. diff --git a/nautilus_trader/indicators/cmo.pyx b/nautilus_trader/indicators/cmo.pyx index 421f7d97331e..b60335050a2d 100644 --- a/nautilus_trader/indicators/cmo.pyx +++ b/nautilus_trader/indicators/cmo.pyx @@ -96,9 +96,13 @@ cdef class ChandeMomentumOscillator(Indicator): if self._average_gain.initialized and self._average_loss.initialized: self._set_initialized(True) + cdef double divisor if self.initialized: - self.value = 100.0 * (self._average_gain.value - self._average_loss.value) - self.value = self.value / (self._average_gain.value + self._average_loss.value) + divisor = self._average_gain.value + self._average_loss.value + if divisor == 0.0: + self.value = 0.0 + else: + self.value = 100.0 * (self._average_gain.value - self._average_loss.value) / divisor self._previous_close = close diff --git a/nautilus_trader/model/instruments/crypto_perpetual.pyx b/nautilus_trader/model/instruments/crypto_perpetual.pyx index 062e05c3e8c4..9a61964351cc 100644 --- a/nautilus_trader/model/instruments/crypto_perpetual.pyx +++ b/nautilus_trader/model/instruments/crypto_perpetual.pyx @@ -246,7 +246,6 @@ cdef class CryptoPerpetual(Instrument): "margin_maint": str(obj.margin_maint), "maker_fee": str(obj.maker_fee), "taker_fee": str(obj.taker_fee), - "ts_event": obj.ts_event, "ts_init": obj.ts_init, "info": obj.info, diff --git a/nautilus_trader/model/instruments/futures_contract.pyx b/nautilus_trader/model/instruments/futures_contract.pyx index e4a2cd063597..1ff36bc4d396 100644 --- a/nautilus_trader/model/instruments/futures_contract.pyx +++ b/nautilus_trader/model/instruments/futures_contract.pyx @@ -26,7 +26,6 @@ from nautilus_trader.core.rust.model cimport AssetClass from nautilus_trader.core.rust.model cimport InstrumentClass from nautilus_trader.model.functions cimport asset_class_from_str from nautilus_trader.model.functions cimport asset_class_to_str -from nautilus_trader.model.functions cimport instrument_class_from_str from nautilus_trader.model.functions cimport instrument_class_to_str from nautilus_trader.model.identifiers cimport InstrumentId from nautilus_trader.model.identifiers cimport Symbol @@ -223,6 +222,10 @@ cdef class FuturesContract(Instrument): "size_precision": obj.size_precision, "size_increment": str(obj.size_increment), "multiplier": str(obj.multiplier), + "max_quantity": str(obj.max_quantity) if obj.max_quantity is not None else None, + "min_quantity": str(obj.min_quantity) if obj.min_quantity is not None else None, + "max_price": str(obj.max_price) if obj.max_price is not None else None, + "min_price": str(obj.min_price) if obj.min_price is not None else None, "lot_size": str(obj.lot_size), "underlying": obj.underlying, "activation_ns": obj.activation_ns, @@ -249,6 +252,7 @@ cdef class FuturesContract(Instrument): underlying=pyo3_instrument.underlying, activation_ns=pyo3_instrument.activation_ns, expiration_ns=pyo3_instrument.expiration_ns, + info=pyo3_instrument.info, ts_event=pyo3_instrument.ts_event, ts_init=pyo3_instrument.ts_init, exchange=pyo3_instrument.exchange, diff --git a/nautilus_trader/model/instruments/futures_spread.pyx b/nautilus_trader/model/instruments/futures_spread.pyx index e609324781c0..02cc0888921a 100644 --- a/nautilus_trader/model/instruments/futures_spread.pyx +++ b/nautilus_trader/model/instruments/futures_spread.pyx @@ -234,6 +234,10 @@ cdef class FuturesSpread(Instrument): "size_precision": obj.size_precision, "size_increment": str(obj.size_increment), "multiplier": str(obj.multiplier), + "max_quantity": str(obj.max_quantity) if obj.max_quantity is not None else None, + "min_quantity": str(obj.min_quantity) if obj.min_quantity is not None else None, + "max_price": str(obj.max_price) if obj.max_price is not None else None, + "min_price": str(obj.min_price) if obj.min_price is not None else None, "lot_size": str(obj.lot_size), "underlying": obj.underlying, "strategy_type": obj.strategy_type, @@ -262,6 +266,7 @@ cdef class FuturesSpread(Instrument): strategy_type=pyo3_instrument.strategy_type, activation_ns=pyo3_instrument.activation_ns, expiration_ns=pyo3_instrument.expiration_ns, + info=pyo3_instrument.info, ts_event=pyo3_instrument.ts_event, ts_init=pyo3_instrument.ts_init, exchange=pyo3_instrument.exchange, diff --git a/nautilus_trader/model/instruments/options_contract.pyx b/nautilus_trader/model/instruments/options_contract.pyx index 8e3b255480c0..78de9096813a 100644 --- a/nautilus_trader/model/instruments/options_contract.pyx +++ b/nautilus_trader/model/instruments/options_contract.pyx @@ -237,6 +237,10 @@ cdef class OptionsContract(Instrument): "size_precision": obj.size_precision, "size_increment": str(obj.size_increment), "multiplier": str(obj.multiplier), + "max_quantity": str(obj.max_quantity) if obj.max_quantity is not None else None, + "min_quantity": str(obj.min_quantity) if obj.min_quantity is not None else None, + "max_price": str(obj.max_price) if obj.max_price is not None else None, + "min_price": str(obj.min_price) if obj.min_price is not None else None, "lot_size": str(obj.lot_size), "underlying": str(obj.underlying), "option_kind": option_kind_to_str(obj.option_kind), @@ -268,6 +272,7 @@ cdef class OptionsContract(Instrument): activation_ns=pyo3_instrument.activation_ns, expiration_ns=pyo3_instrument.expiration_ns, strike_price=Price.from_raw_c(pyo3_instrument.strike_price.raw, pyo3_instrument.strike_price.precision), + info=pyo3_instrument.info, ts_event=pyo3_instrument.ts_event, ts_init=pyo3_instrument.ts_init, exchange=pyo3_instrument.exchange, diff --git a/nautilus_trader/model/instruments/options_spread.pyx b/nautilus_trader/model/instruments/options_spread.pyx index 6b917992bb27..0f5514d3dca1 100644 --- a/nautilus_trader/model/instruments/options_spread.pyx +++ b/nautilus_trader/model/instruments/options_spread.pyx @@ -231,12 +231,17 @@ cdef class OptionsSpread(Instrument): "id": obj.id.to_str(), "raw_symbol": obj.raw_symbol.to_str(), "asset_class": asset_class_to_str(obj.asset_class), + "strategy_type": obj.strategy_type, "currency": obj.quote_currency.code, "price_precision": obj.price_precision, "price_increment": str(obj.price_increment), "size_precision": obj.size_precision, "size_increment": str(obj.size_increment), "multiplier": str(obj.multiplier), + "max_quantity": str(obj.max_quantity) if obj.max_quantity is not None else None, + "min_quantity": str(obj.min_quantity) if obj.min_quantity is not None else None, + "max_price": str(obj.max_price) if obj.max_price is not None else None, + "min_price": str(obj.min_price) if obj.min_price is not None else None, "lot_size": str(obj.lot_size), "underlying": str(obj.underlying), "activation_ns": obj.activation_ns, @@ -265,6 +270,7 @@ cdef class OptionsSpread(Instrument): strategy_type=pyo3_instrument.strategy_type, activation_ns=pyo3_instrument.activation_ns, expiration_ns=pyo3_instrument.expiration_ns, + info=pyo3_instrument.info, ts_event=pyo3_instrument.ts_event, ts_init=pyo3_instrument.ts_init, exchange=pyo3_instrument.exchange, diff --git a/nautilus_trader/model/orders/limit.pxd b/nautilus_trader/model/orders/limit.pxd index cde5409a1134..7b4c03c620da 100644 --- a/nautilus_trader/model/orders/limit.pxd +++ b/nautilus_trader/model/orders/limit.pxd @@ -34,3 +34,6 @@ cdef class LimitOrder(Order): @staticmethod cdef LimitOrder transform(Order order, uint64_t ts_init, Price price=*) + + @staticmethod + cdef LimitOrder from_pyo3_c(pyo3_order) diff --git a/nautilus_trader/model/orders/limit.pyx b/nautilus_trader/model/orders/limit.pyx index f9d3fb170433..86ba7aa4b189 100644 --- a/nautilus_trader/model/orders/limit.pyx +++ b/nautilus_trader/model/orders/limit.pyx @@ -26,11 +26,15 @@ from nautilus_trader.core.rust.model cimport TriggerType from nautilus_trader.core.uuid cimport UUID4 from nautilus_trader.model.events.order cimport OrderInitialized from nautilus_trader.model.events.order cimport OrderUpdated +from nautilus_trader.model.functions cimport contingency_type_from_str from nautilus_trader.model.functions cimport contingency_type_to_str from nautilus_trader.model.functions cimport liquidity_side_to_str +from nautilus_trader.model.functions cimport order_side_from_str from nautilus_trader.model.functions cimport order_side_to_str from nautilus_trader.model.functions cimport order_type_to_str +from nautilus_trader.model.functions cimport time_in_force_from_str from nautilus_trader.model.functions cimport time_in_force_to_str +from nautilus_trader.model.functions cimport trigger_type_from_str from nautilus_trader.model.functions cimport trigger_type_to_str from nautilus_trader.model.identifiers cimport ClientOrderId from nautilus_trader.model.identifiers cimport ExecAlgorithmId @@ -257,6 +261,40 @@ cdef class LimitOrder(Order): f"{emulation_str}" ) + @staticmethod + cdef LimitOrder from_pyo3_c(pyo3_order): + return LimitOrder( + trader_id=TraderId(str(pyo3_order.trader_id)), + strategy_id=StrategyId(str(pyo3_order.strategy_id)), + instrument_id=InstrumentId.from_str_c(str(pyo3_order.instrument_id)), + client_order_id=ClientOrderId(str(pyo3_order.client_order_id)), + order_side=order_side_from_str(str(pyo3_order.side)), + quantity=Quantity.from_raw_c(pyo3_order.quantity.raw, pyo3_order.quantity.precision), + price=Price.from_raw_c(pyo3_order.price.raw, pyo3_order.price.precision), + init_id=UUID4(str(pyo3_order.init_id)), + ts_init=pyo3_order.ts_init, + time_in_force=time_in_force_from_str(str(pyo3_order.time_in_force)), + expire_time_ns=int(pyo3_order.expire_time_ns) if pyo3_order.expire_time_ns is not None else 0, + post_only=pyo3_order.is_post_only, + reduce_only=pyo3_order.is_reduce_only, + quote_quantity=pyo3_order.is_quote_quantity, + display_qty=Quantity.from_str_c(pyo3_order.display_qty) if pyo3_order.display_qty is not None else None, + emulation_trigger=trigger_type_from_str(str(pyo3_order.emulation_trigger)) if pyo3_order.emulation_trigger is not None else TriggerType.NO_TRIGGER, + trigger_instrument_id=InstrumentId.from_str_c(str(pyo3_order.trigger_instrument_id)) if pyo3_order.trigger_instrument_id is not None else None, + contingency_type=contingency_type_from_str(str(pyo3_order.contingency_type)) if pyo3_order.contingency_type is not None else ContingencyType.NO_CONTINGENCY, + order_list_id=OrderListId(str(pyo3_order.order_list_id)) if pyo3_order.order_list_id is not None else None, + linked_order_ids=[ClientOrderId(str(o)) for o in pyo3_order.linked_order_ids] if pyo3_order.linked_order_ids is not None else None, + parent_order_id=ClientOrderId(str(pyo3_order.parent_order_id)) if pyo3_order.parent_order_id is not None else None, + exec_algorithm_id=ExecAlgorithmId(str(pyo3_order.exec_algorithm_id)) if pyo3_order.exec_algorithm_id is not None else None, + exec_algorithm_params=pyo3_order.exec_algorithm_params, + exec_spawn_id=ClientOrderId(str(pyo3_order.exec_spawn_id)) if pyo3_order.exec_spawn_id is not None else None, + tags=pyo3_order.tags if pyo3_order.tags is not None else None, + ) + + @staticmethod + def from_pyo3(pyo3_order): + return LimitOrder.from_pyo3_c(pyo3_order) + cpdef dict to_dict(self): """ Return a dictionary representation of this object. @@ -286,7 +324,7 @@ cdef class LimitOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([c.to_str() for c in self.commissions()]) if self._commissions else None, + "commissions": str([c.to_str() for c in self.commissions()]) if self._commissions else {}, "status": self._fsm.state_string_c(), "is_post_only": self.is_post_only, "is_reduce_only": self.is_reduce_only, @@ -302,6 +340,7 @@ cdef class LimitOrder(Order): "exec_algorithm_params": self.exec_algorithm_params, "exec_spawn_id": self.exec_spawn_id.to_str() if self.exec_spawn_id is not None else None, "tags": self.tags, + "init_id": str(self.init_id), "ts_init": self.ts_init, "ts_last": self.ts_last, } diff --git a/nautilus_trader/model/orders/market.pxd b/nautilus_trader/model/orders/market.pxd index 186e0ca38b9d..96e89d71d120 100644 --- a/nautilus_trader/model/orders/market.pxd +++ b/nautilus_trader/model/orders/market.pxd @@ -25,3 +25,6 @@ cdef class MarketOrder(Order): @staticmethod cdef MarketOrder transform(Order order, uint64_t ts_init) + + @staticmethod + cdef MarketOrder from_pyo3_c(pyo3_order) diff --git a/nautilus_trader/model/orders/market.pyx b/nautilus_trader/model/orders/market.pyx index 615d9d6b3d5c..fc5532193064 100644 --- a/nautilus_trader/model/orders/market.pyx +++ b/nautilus_trader/model/orders/market.pyx @@ -24,11 +24,15 @@ from nautilus_trader.core.rust.model cimport TriggerType from nautilus_trader.core.uuid cimport UUID4 from nautilus_trader.model.events.order cimport OrderInitialized from nautilus_trader.model.events.order cimport OrderUpdated +from nautilus_trader.model.functions cimport contingency_type_from_str from nautilus_trader.model.functions cimport contingency_type_to_str from nautilus_trader.model.functions cimport liquidity_side_to_str +from nautilus_trader.model.functions cimport order_side_from_str from nautilus_trader.model.functions cimport order_side_to_str from nautilus_trader.model.functions cimport order_type_to_str +from nautilus_trader.model.functions cimport time_in_force_from_str from nautilus_trader.model.functions cimport time_in_force_to_str +from nautilus_trader.model.functions cimport trigger_type_to_str from nautilus_trader.model.identifiers cimport ClientOrderId from nautilus_trader.model.identifiers cimport ExecAlgorithmId from nautilus_trader.model.identifiers cimport InstrumentId @@ -188,6 +192,34 @@ cdef class MarketOrder(Order): f"{time_in_force_to_str(self.time_in_force)}" ) + @staticmethod + cdef MarketOrder from_pyo3_c(pyo3_order): + return MarketOrder( + trader_id=TraderId(str(pyo3_order.trader_id)), + strategy_id=StrategyId(str(pyo3_order.strategy_id)), + instrument_id=InstrumentId.from_str_c(str(pyo3_order.instrument_id)), + client_order_id=ClientOrderId(str(pyo3_order.client_order_id)), + order_side=order_side_from_str(str(pyo3_order.side)), + quantity=Quantity.from_raw_c(pyo3_order.quantity.raw, pyo3_order.quantity.precision), + init_id=UUID4(str(pyo3_order.init_id)), + ts_init=pyo3_order.ts_init, + time_in_force=time_in_force_from_str(str(pyo3_order.time_in_force)), + reduce_only=pyo3_order.is_reduce_only, + quote_quantity=pyo3_order.is_quote_quantity, + contingency_type=contingency_type_from_str(str(pyo3_order.contingency_type)) if pyo3_order.contingency_type is not None else ContingencyType.NO_CONTINGENCY, + order_list_id=OrderListId(str(pyo3_order.order_list_id)) if pyo3_order.order_list_id is not None else None, + linked_order_ids=[ClientOrderId(str(o)) for o in pyo3_order.linked_order_ids] if pyo3_order.linked_order_ids is not None else None, + parent_order_id=ClientOrderId(str(pyo3_order.parent_order_id)) if pyo3_order.parent_order_id is not None else None, + exec_algorithm_id=ExecAlgorithmId(str(pyo3_order.exec_algorithm_id)) if pyo3_order.exec_algorithm_id is not None else None, + exec_algorithm_params=pyo3_order.exec_algorithm_params, + exec_spawn_id=ClientOrderId(str(pyo3_order.exec_spawn_id)) if pyo3_order.exec_spawn_id is not None else None, + tags=pyo3_order.tags if pyo3_order.tags is not None else None, + ) + + @staticmethod + def from_pyo3(pyo3_order): + return MarketOrder.from_pyo3_c(pyo3_order) + cpdef dict to_dict(self): """ Return a dictionary representation of this object. @@ -217,7 +249,8 @@ cdef class MarketOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([c.to_str() for c in self.commissions()]) if self._commissions else None, + "commissions": str([c.to_str() for c in self.commissions()]) if self._commissions else {}, + "emulation_trigger": trigger_type_to_str(self.emulation_trigger), "status": self._fsm.state_string_c(), "contingency_type": contingency_type_to_str(self.contingency_type), "order_list_id": self.order_list_id.to_str() if self.order_list_id is not None else None, @@ -227,6 +260,7 @@ cdef class MarketOrder(Order): "exec_algorithm_params": self.exec_algorithm_params, "exec_spawn_id": self.exec_spawn_id.to_str() if self.exec_spawn_id is not None else None, "tags": self.tags, + "init_id": str(self.init_id), "ts_init": self.ts_init, "ts_last": self.ts_last, } diff --git a/nautilus_trader/persistence/loaders.py b/nautilus_trader/persistence/loaders.py index f8cdb7c0a0a5..8c3c84d64c52 100644 --- a/nautilus_trader/persistence/loaders.py +++ b/nautilus_trader/persistence/loaders.py @@ -14,6 +14,7 @@ # ------------------------------------------------------------------------------------------------- from os import PathLike +from typing import Any import pandas as pd @@ -27,7 +28,9 @@ class CSVTickDataLoader: def load( file_path: PathLike[str] | str, index_col: str | int = "timestamp", - format: str = "mixed", + parse_dates: bool = True, + datetime_format: str = "mixed", + **kwargs: Any, ) -> pd.DataFrame: """ Return a tick `pandas.DataFrame` loaded from the given CSV `file_path`. @@ -36,10 +39,14 @@ def load( ---------- file_path : str, path object or file-like object The path to the CSV file. - index_col : str | int, default 'timestamp' - The index column. - format : str, default 'mixed' + index_col : str or int, default 'timestamp' + The column to use as the row labels of the DataFrame. + parse_dates : bool, default True + If True, attempt to parse the index. + datetime_format : str, default 'mixed' The timestamp column format. + **kwargs : Any + The additional parameters to be passed to pd.read_csv. Returns ------- @@ -49,9 +56,10 @@ def load( df = pd.read_csv( file_path, index_col=index_col, - parse_dates=True, + parse_dates=parse_dates, + **kwargs, ) - df.index = pd.to_datetime(df.index, format=format) + df.index = pd.to_datetime(df.index, format=datetime_format) return df @@ -61,7 +69,12 @@ class CSVBarDataLoader: """ @staticmethod - def load(file_path: PathLike[str] | str) -> pd.DataFrame: + def load( + file_path: PathLike[str] | str, + index_col: str | int = "timestamp", + parse_dates: bool = True, + **kwargs: Any, + ) -> pd.DataFrame: """ Return the bar `pandas.DataFrame` loaded from the given CSV `file_path`. @@ -69,6 +82,12 @@ def load(file_path: PathLike[str] | str) -> pd.DataFrame: ---------- file_path : str, path object or file-like object The path to the CSV file. + index_col : str | int, default 'timestamp' + The column to use as the row labels of the DataFrame. + parse_dates : bool, default True + If True, attempt to parse the index. + **kwargs : Any + The additional parameters to be passed to pd.read_csv. Returns ------- @@ -77,8 +96,9 @@ def load(file_path: PathLike[str] | str) -> pd.DataFrame: """ df = pd.read_csv( file_path, - index_col="timestamp", - parse_dates=True, + index_col=index_col, + parse_dates=parse_dates, + **kwargs, ) df.index = pd.to_datetime(df.index, format="mixed") return df diff --git a/nautilus_trader/system/kernel.py b/nautilus_trader/system/kernel.py index 56bac70586e7..7413cac41e4d 100644 --- a/nautilus_trader/system/kernel.py +++ b/nautilus_trader/system/kernel.py @@ -157,7 +157,7 @@ def __init__( # noqa (too complex) register_component_clock(self._instance_id, self._clock) - # Setup logging + # Initialize logging system logging: LoggingConfig = config.logging or LoggingConfig() if not is_logging_initialized(): @@ -168,7 +168,7 @@ def __init__( # noqa (too complex) nautilus_pyo3.init_tracing() # Initialize logging for sync Rust and Python - nautilus_pyo3.init_logging( + self._log_guard = nautilus_pyo3.init_logging( trader_id=nautilus_pyo3.TraderId(self._trader_id.value), instance_id=nautilus_pyo3.UUID4(self._instance_id.value), level_stdout=nautilus_pyo3.LogLevel(logging.log_level), @@ -187,7 +187,7 @@ def __init__( # noqa (too complex) ) else: # Initialize logging for sync Rust and Python - init_logging( + self._log_guard = init_logging( trader_id=self._trader_id, machine_id=self._machine_id, instance_id=self._instance_id, @@ -218,7 +218,6 @@ def __init__( # noqa (too complex) ) self._log: Logger = Logger(name=name) - self._log.info("Building system kernel...") # Setup loop (if sandbox live) diff --git a/nautilus_trader/test_kit/rust/instruments_pyo3.py b/nautilus_trader/test_kit/rust/instruments_pyo3.py index 30156cdc4f15..ab637cad8519 100644 --- a/nautilus_trader/test_kit/rust/instruments_pyo3.py +++ b/nautilus_trader/test_kit/rust/instruments_pyo3.py @@ -264,6 +264,24 @@ def btcusdt_binance() -> CurrencyPair: ts_init=0, ) + @staticmethod + def aapl_equity() -> Equity: + return Equity( + id=InstrumentId.from_str("AAPL.XNAS"), + raw_symbol=Symbol("AAPL"), + isin="US0378331005", + currency=_USD, + price_precision=2, + price_increment=Price.from_str("0.01"), + lot_size=Quantity.from_int(100), + max_quantity=None, + min_quantity=None, + max_price=None, + min_price=None, + ts_event=0, + ts_init=0, + ) + @staticmethod def aapl_option( activation: pd.Timestamp | None = None, @@ -296,24 +314,6 @@ def aapl_option( ts_init=0, ) - @staticmethod - def aapl_equity() -> Equity: - return Equity( - id=InstrumentId.from_str("AAPL.XNAS"), - raw_symbol=Symbol("AAPL"), - isin="US0378331005", - currency=_USD, - price_precision=2, - price_increment=Price.from_str("0.01"), - lot_size=Quantity.from_int(100), - max_quantity=None, - min_quantity=None, - max_price=None, - min_price=None, - ts_event=0, - ts_init=0, - ) - @staticmethod def futures_contract_es( activation: pd.Timestamp | None = None, diff --git a/nautilus_trader/test_kit/rust/orders_pyo3.py b/nautilus_trader/test_kit/rust/orders_pyo3.py index 67021cb99edf..86a68693795e 100644 --- a/nautilus_trader/test_kit/rust/orders_pyo3.py +++ b/nautilus_trader/test_kit/rust/orders_pyo3.py @@ -14,24 +14,29 @@ # ------------------------------------------------------------------------------------------------- from nautilus_trader.core.nautilus_pyo3 import ClientOrderId +from nautilus_trader.core.nautilus_pyo3 import ExecAlgorithmId +from nautilus_trader.core.nautilus_pyo3 import InstrumentId +from nautilus_trader.core.nautilus_pyo3 import LimitOrder from nautilus_trader.core.nautilus_pyo3 import MarketOrder from nautilus_trader.core.nautilus_pyo3 import OrderSide +from nautilus_trader.core.nautilus_pyo3 import Price from nautilus_trader.core.nautilus_pyo3 import Quantity from nautilus_trader.core.nautilus_pyo3 import StrategyId from nautilus_trader.core.nautilus_pyo3 import TimeInForce +from nautilus_trader.core.nautilus_pyo3 import TraderId from nautilus_trader.test_kit.rust.identifiers_pyo3 import TestIdProviderPyo3 class TestOrderProviderPyo3: @staticmethod def market_order( - instrument_id=None, - order_side=None, - quantity=None, - trader_id=None, + instrument_id: InstrumentId | None = None, + order_side: OrderSide | None = None, + quantity: Quantity | None = None, + trader_id: TraderId | None = None, strategy_id: StrategyId | None = None, client_order_id: ClientOrderId | None = None, - time_in_force=None, + time_in_force: TimeInForce | None = None, ) -> MarketOrder: return MarketOrder( trader_id=trader_id or TestIdProviderPyo3.trader_id(), @@ -41,12 +46,38 @@ def market_order( order_side=order_side or OrderSide.BUY, quantity=quantity or Quantity.from_str("100"), time_in_force=time_in_force or TimeInForce.GTC, + reduce_only=False, + quote_quantity=False, init_id=TestIdProviderPyo3.uuid(), ts_init=0, + ) + + @staticmethod + def limit_order( + instrument_id: InstrumentId, + order_side: OrderSide, + quantity: Quantity, + price: Price, + trader_id: TraderId | None = None, + strategy_id: StrategyId | None = None, + client_order_id: ClientOrderId | None = None, + time_in_force: TimeInForce | None = None, + exec_algorithm_id: ExecAlgorithmId | None = None, + ) -> LimitOrder: + return LimitOrder( + trader_id=trader_id or TestIdProviderPyo3.trader_id(), + strategy_id=strategy_id or TestIdProviderPyo3.strategy_id(), + instrument_id=instrument_id or TestIdProviderPyo3.audusd_id(), + client_order_id=client_order_id or TestIdProviderPyo3.client_order_id(1), + order_side=order_side or OrderSide.BUY, + quantity=quantity or Quantity.from_str("100"), + time_in_force=time_in_force or TimeInForce.GTC, + price=price, + post_only=False, reduce_only=False, - contingency_type=None, - order_list_id=None, - linked_order_ids=None, - parent_order_id=None, - tags=None, + quote_quantity=False, + init_id=TestIdProviderPyo3.uuid(), + ts_init=0, + exec_algorithm_id=exec_algorithm_id, + exec_spawn_id=TestIdProviderPyo3.client_order_id(1), ) diff --git a/poetry.lock b/poetry.lock index 41a79dedf09d..bf985ce073f4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -202,33 +202,33 @@ msgspec = ">=0.18.5" [[package]] name = "black" -version = "24.2.0" +version = "24.3.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"}, - {file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"}, - {file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"}, - {file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"}, - {file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"}, - {file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"}, - {file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"}, - {file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"}, - {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, - {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, - {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, - {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, - {file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"}, - {file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"}, - {file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"}, - {file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"}, - {file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"}, - {file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"}, - {file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"}, - {file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"}, - {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, - {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, + {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, + {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, + {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, + {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, + {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, + {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, + {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, + {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, + {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, + {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, + {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, + {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, + {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, + {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, + {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, + {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, + {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, + {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, + {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, + {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, + {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, + {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, ] [package.dependencies] @@ -1487,13 +1487,13 @@ xml = ["lxml (>=4.9.2)"] [[package]] name = "pandas-stubs" -version = "2.2.0.240218" +version = "2.2.1.240316" description = "Type annotations for pandas" optional = false python-versions = ">=3.9" files = [ - {file = "pandas_stubs-2.2.0.240218-py3-none-any.whl", hash = "sha256:e97478320add9b958391b15a56c5f1bf29da656d5b747d28bbe708454b3a1fe6"}, - {file = "pandas_stubs-2.2.0.240218.tar.gz", hash = "sha256:63138c12eec715d66d48611bdd922f31cd7c78bcadd19384c3bd61fd3720a11a"}, + {file = "pandas_stubs-2.2.1.240316-py3-none-any.whl", hash = "sha256:0126a26451a37cb893ea62357ca87ba3d181bd999ec8ba2ca5602e20207d6682"}, + {file = "pandas_stubs-2.2.1.240316.tar.gz", hash = "sha256:236a4f812fb6b1922e9607ff09e427f6d8540c421c9e5a40e3e4ddf7adac7f05"}, ] [package.dependencies] @@ -1600,47 +1600,47 @@ files = [ [[package]] name = "pyarrow" -version = "15.0.0" +version = "15.0.2" description = "Python library for Apache Arrow" optional = false python-versions = ">=3.8" files = [ - {file = "pyarrow-15.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:0a524532fd6dd482edaa563b686d754c70417c2f72742a8c990b322d4c03a15d"}, - {file = "pyarrow-15.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:60a6bdb314affa9c2e0d5dddf3d9cbb9ef4a8dddaa68669975287d47ece67642"}, - {file = "pyarrow-15.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66958fd1771a4d4b754cd385835e66a3ef6b12611e001d4e5edfcef5f30391e2"}, - {file = "pyarrow-15.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f500956a49aadd907eaa21d4fff75f73954605eaa41f61cb94fb008cf2e00c6"}, - {file = "pyarrow-15.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6f87d9c4f09e049c2cade559643424da84c43a35068f2a1c4653dc5b1408a929"}, - {file = "pyarrow-15.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85239b9f93278e130d86c0e6bb455dcb66fc3fd891398b9d45ace8799a871a1e"}, - {file = "pyarrow-15.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b8d43e31ca16aa6e12402fcb1e14352d0d809de70edd185c7650fe80e0769e3"}, - {file = "pyarrow-15.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:fa7cd198280dbd0c988df525e50e35b5d16873e2cdae2aaaa6363cdb64e3eec5"}, - {file = "pyarrow-15.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8780b1a29d3c8b21ba6b191305a2a607de2e30dab399776ff0aa09131e266340"}, - {file = "pyarrow-15.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0ec198ccc680f6c92723fadcb97b74f07c45ff3fdec9dd765deb04955ccf19"}, - {file = "pyarrow-15.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036a7209c235588c2f07477fe75c07e6caced9b7b61bb897c8d4e52c4b5f9555"}, - {file = "pyarrow-15.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2bd8a0e5296797faf9a3294e9fa2dc67aa7f10ae2207920dbebb785c77e9dbe5"}, - {file = "pyarrow-15.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e8ebed6053dbe76883a822d4e8da36860f479d55a762bd9e70d8494aed87113e"}, - {file = "pyarrow-15.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:17d53a9d1b2b5bd7d5e4cd84d018e2a45bc9baaa68f7e6e3ebed45649900ba99"}, - {file = "pyarrow-15.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9950a9c9df24090d3d558b43b97753b8f5867fb8e521f29876aa021c52fda351"}, - {file = "pyarrow-15.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:003d680b5e422d0204e7287bb3fa775b332b3fce2996aa69e9adea23f5c8f970"}, - {file = "pyarrow-15.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f75fce89dad10c95f4bf590b765e3ae98bcc5ba9f6ce75adb828a334e26a3d40"}, - {file = "pyarrow-15.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca9cb0039923bec49b4fe23803807e4ef39576a2bec59c32b11296464623dc2"}, - {file = "pyarrow-15.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9ed5a78ed29d171d0acc26a305a4b7f83c122d54ff5270810ac23c75813585e4"}, - {file = "pyarrow-15.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6eda9e117f0402dfcd3cd6ec9bfee89ac5071c48fc83a84f3075b60efa96747f"}, - {file = "pyarrow-15.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a3a6180c0e8f2727e6f1b1c87c72d3254cac909e609f35f22532e4115461177"}, - {file = "pyarrow-15.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:19a8918045993349b207de72d4576af0191beef03ea655d8bdb13762f0cd6eac"}, - {file = "pyarrow-15.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0ec076b32bacb6666e8813a22e6e5a7ef1314c8069d4ff345efa6246bc38593"}, - {file = "pyarrow-15.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5db1769e5d0a77eb92344c7382d6543bea1164cca3704f84aa44e26c67e320fb"}, - {file = "pyarrow-15.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2617e3bf9df2a00020dd1c1c6dce5cc343d979efe10bc401c0632b0eef6ef5b"}, - {file = "pyarrow-15.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:d31c1d45060180131caf10f0f698e3a782db333a422038bf7fe01dace18b3a31"}, - {file = "pyarrow-15.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:c8c287d1d479de8269398b34282e206844abb3208224dbdd7166d580804674b7"}, - {file = "pyarrow-15.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:07eb7f07dc9ecbb8dace0f58f009d3a29ee58682fcdc91337dfeb51ea618a75b"}, - {file = "pyarrow-15.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:47af7036f64fce990bb8a5948c04722e4e3ea3e13b1007ef52dfe0aa8f23cf7f"}, - {file = "pyarrow-15.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93768ccfff85cf044c418bfeeafce9a8bb0cee091bd8fd19011aff91e58de540"}, - {file = "pyarrow-15.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6ee87fd6892700960d90abb7b17a72a5abb3b64ee0fe8db6c782bcc2d0dc0b4"}, - {file = "pyarrow-15.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:001fca027738c5f6be0b7a3159cc7ba16a5c52486db18160909a0831b063c4e4"}, - {file = "pyarrow-15.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:d1c48648f64aec09accf44140dccb92f4f94394b8d79976c426a5b79b11d4fa7"}, - {file = "pyarrow-15.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:972a0141be402bb18e3201448c8ae62958c9c7923dfaa3b3d4530c835ac81aed"}, - {file = "pyarrow-15.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:f01fc5cf49081426429127aa2d427d9d98e1cb94a32cb961d583a70b7c4504e6"}, - {file = "pyarrow-15.0.0.tar.gz", hash = "sha256:876858f549d540898f927eba4ef77cd549ad8d24baa3207cf1b72e5788b50e83"}, + {file = "pyarrow-15.0.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:88b340f0a1d05b5ccc3d2d986279045655b1fe8e41aba6ca44ea28da0d1455d8"}, + {file = "pyarrow-15.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eaa8f96cecf32da508e6c7f69bb8401f03745c050c1dd42ec2596f2e98deecac"}, + {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23c6753ed4f6adb8461e7c383e418391b8d8453c5d67e17f416c3a5d5709afbd"}, + {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f639c059035011db8c0497e541a8a45d98a58dbe34dc8fadd0ef128f2cee46e5"}, + {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:290e36a59a0993e9a5224ed2fb3e53375770f07379a0ea03ee2fce2e6d30b423"}, + {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:06c2bb2a98bc792f040bef31ad3e9be6a63d0cb39189227c08a7d955db96816e"}, + {file = "pyarrow-15.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:f7a197f3670606a960ddc12adbe8075cea5f707ad7bf0dffa09637fdbb89f76c"}, + {file = "pyarrow-15.0.2-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:5f8bc839ea36b1f99984c78e06e7a06054693dc2af8920f6fb416b5bca9944e4"}, + {file = "pyarrow-15.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5e81dfb4e519baa6b4c80410421528c214427e77ca0ea9461eb4097c328fa33"}, + {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a4f240852b302a7af4646c8bfe9950c4691a419847001178662a98915fd7ee7"}, + {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e7d9cfb5a1e648e172428c7a42b744610956f3b70f524aa3a6c02a448ba853e"}, + {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2d4f905209de70c0eb5b2de6763104d5a9a37430f137678edfb9a675bac9cd98"}, + {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:90adb99e8ce5f36fbecbbc422e7dcbcbed07d985eed6062e459e23f9e71fd197"}, + {file = "pyarrow-15.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:b116e7fd7889294cbd24eb90cd9bdd3850be3738d61297855a71ac3b8124ee38"}, + {file = "pyarrow-15.0.2-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:25335e6f1f07fdaa026a61c758ee7d19ce824a866b27bba744348fa73bb5a440"}, + {file = "pyarrow-15.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:90f19e976d9c3d8e73c80be84ddbe2f830b6304e4c576349d9360e335cd627fc"}, + {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a22366249bf5fd40ddacc4f03cd3160f2d7c247692945afb1899bab8a140ddfb"}, + {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2a335198f886b07e4b5ea16d08ee06557e07db54a8400cc0d03c7f6a22f785f"}, + {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e6d459c0c22f0b9c810a3917a1de3ee704b021a5fb8b3bacf968eece6df098f"}, + {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:033b7cad32198754d93465dcfb71d0ba7cb7cd5c9afd7052cab7214676eec38b"}, + {file = "pyarrow-15.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:29850d050379d6e8b5a693098f4de7fd6a2bea4365bfd073d7c57c57b95041ee"}, + {file = "pyarrow-15.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:7167107d7fb6dcadb375b4b691b7e316f4368f39f6f45405a05535d7ad5e5058"}, + {file = "pyarrow-15.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e85241b44cc3d365ef950432a1b3bd44ac54626f37b2e3a0cc89c20e45dfd8bf"}, + {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:248723e4ed3255fcd73edcecc209744d58a9ca852e4cf3d2577811b6d4b59818"}, + {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ff3bdfe6f1b81ca5b73b70a8d482d37a766433823e0c21e22d1d7dde76ca33f"}, + {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:f3d77463dee7e9f284ef42d341689b459a63ff2e75cee2b9302058d0d98fe142"}, + {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:8c1faf2482fb89766e79745670cbca04e7018497d85be9242d5350cba21357e1"}, + {file = "pyarrow-15.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:28f3016958a8e45a1069303a4a4f6a7d4910643fc08adb1e2e4a7ff056272ad3"}, + {file = "pyarrow-15.0.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:89722cb64286ab3d4daf168386f6968c126057b8c7ec3ef96302e81d8cdb8ae4"}, + {file = "pyarrow-15.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd0ba387705044b3ac77b1b317165c0498299b08261d8122c96051024f953cd5"}, + {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad2459bf1f22b6a5cdcc27ebfd99307d5526b62d217b984b9f5c974651398832"}, + {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58922e4bfece8b02abf7159f1f53a8f4d9f8e08f2d988109126c17c3bb261f22"}, + {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:adccc81d3dc0478ea0b498807b39a8d41628fa9210729b2f718b78cb997c7c91"}, + {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:8bd2baa5fe531571847983f36a30ddbf65261ef23e496862ece83bdceb70420d"}, + {file = "pyarrow-15.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6669799a1d4ca9da9c7e06ef48368320f5856f36f9a4dd31a11839dda3f6cc8c"}, + {file = "pyarrow-15.0.2.tar.gz", hash = "sha256:9c9bc803cb3b7bfacc1e96ffbfd923601065d9d3f911179d81e72d99fd74a3d9"}, ] [package.dependencies] @@ -1760,17 +1760,17 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-mock" -version = "3.12.0" +version = "3.14.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, - {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, ] [package.dependencies] -pytest = ">=5.0" +pytest = ">=6.2.5" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] @@ -1945,28 +1945,28 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.3.2" +version = "0.3.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77f2612752e25f730da7421ca5e3147b213dca4f9a0f7e0b534e9562c5441f01"}, - {file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9966b964b2dd1107797be9ca7195002b874424d1d5472097701ae8f43eadef5d"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b83d17ff166aa0659d1e1deaf9f2f14cbe387293a906de09bc4860717eb2e2da"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb875c6cc87b3703aeda85f01c9aebdce3d217aeaca3c2e52e38077383f7268a"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be75e468a6a86426430373d81c041b7605137a28f7014a72d2fc749e47f572aa"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:967978ac2d4506255e2f52afe70dda023fc602b283e97685c8447d036863a302"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1231eacd4510f73222940727ac927bc5d07667a86b0cbe822024dd00343e77e9"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6d613b19e9a8021be2ee1d0e27710208d1603b56f47203d0abbde906929a9b"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8439338a6303585d27b66b4626cbde89bb3e50fa3cae86ce52c1db7449330a7"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:de8b480d8379620cbb5ea466a9e53bb467d2fb07c7eca54a4aa8576483c35d36"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b74c3de9103bd35df2bb05d8b2899bf2dbe4efda6474ea9681280648ec4d237d"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f380be9fc15a99765c9cf316b40b9da1f6ad2ab9639e551703e581a5e6da6745"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0ac06a3759c3ab9ef86bbeca665d31ad3aa9a4b1c17684aadb7e61c10baa0df4"}, - {file = "ruff-0.3.2-py3-none-win32.whl", hash = "sha256:9bd640a8f7dd07a0b6901fcebccedadeb1a705a50350fb86b4003b805c81385a"}, - {file = "ruff-0.3.2-py3-none-win_amd64.whl", hash = "sha256:0c1bdd9920cab5707c26c8b3bf33a064a4ca7842d91a99ec0634fec68f9f4037"}, - {file = "ruff-0.3.2-py3-none-win_arm64.whl", hash = "sha256:5f65103b1d76e0d600cabd577b04179ff592064eaa451a70a81085930e907d0b"}, - {file = "ruff-0.3.2.tar.gz", hash = "sha256:fa78ec9418eb1ca3db392811df3376b46471ae93792a81af2d1cbb0e5dcb5142"}, + {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4"}, + {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"}, + {file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"}, + {file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"}, + {file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"}, + {file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"}, ] [[package]] @@ -2611,4 +2611,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "006f49cde91f1b880860b76d79ec687315f7c8fbf3e7d49624c8546a10dd1b19" +content-hash = "61899ddfdeb6e2422bd7a06565219da0a6805023c1a6fb3767a022a8eed95b01" diff --git a/pyproject.toml b/pyproject.toml index fb6a56ae2082..6b656d246b93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nautilus_trader" -version = "1.189.0" +version = "1.190.0" description = "A high-performance algorithmic trading platform and event-driven backtester" authors = ["Nautech Systems "] license = "LGPL-3.0-or-later" @@ -58,7 +58,7 @@ click = "^8.1.7" fsspec = "==2023.6.0" # Pinned due breaking changes msgspec = "^0.18.6" pandas = "^2.2.1" -pyarrow = "==15.0.0" # 15.0.1 wheels not available for glibc 2.25 yet +pyarrow = ">=15.0.2" pytz = ">=2023.4.0" tqdm = "^4.66.2" uvloop = {version = "^0.19.0", markers = "sys_platform != 'win32'"} @@ -78,12 +78,12 @@ ib = ["nautilus_ibapi", "async-timeout", "defusedxml"] optional = true [tool.poetry.group.dev.dependencies] -black = "^24.2.0" +black = "^24.3.0" docformatter = "^1.7.5" mypy = "^1.9.0" -pandas-stubs = "^2.1.4" +pandas-stubs = "^2.2.1" pre-commit = "^3.6.2" -ruff = "^0.3.2" +ruff = "^0.3.4" types-pytz = "^2023.3" types-requests = "^2.31" types-toml = "^0.10.2" @@ -98,7 +98,7 @@ pytest-aiohttp = "^1.0.5" pytest-asyncio = "==0.21.1" # Pinned due Cython: cannot set '__pytest_asyncio_scoped_event_loop' attribute of immutable type pytest-benchmark = "^4.0.0" pytest-cov = "^4.1.0" -pytest-mock = "^3.12.0" +pytest-mock = "^3.14.0" pytest-xdist = { version = "^3.5.0", extras = ["psutil"] } [tool.poetry.group.docs] diff --git a/tests/integration_tests/adapters/databento/test_loaders.py b/tests/integration_tests/adapters/databento/test_loaders.py index d7326fd12c92..f1918a7b764d 100644 --- a/tests/integration_tests/adapters/databento/test_loaders.py +++ b/tests/integration_tests/adapters/databento/test_loaders.py @@ -52,7 +52,7 @@ def test_get_publishers() -> None: result = loader.get_publishers() # Assert - assert len(result) == 61 # From built-in map + assert len(result) == 80 # From built-in map def test_loader_definition_glbx_futures() -> None: @@ -85,37 +85,6 @@ def test_loader_definition_glbx_futures() -> None: assert instrument.ts_init == 1680451436501583647 -@pytest.mark.skip(reason="WIP: Future spreads not currently supported") -def test_loader_definition_glbx_futures_spread() -> None: - # Arrange - loader = DatabentoDataLoader() - path = DATABENTO_TEST_DATA_DIR / "definition-glbx-es-futspread.dbn.zst" - - # Act - data = loader.from_dbn_file(path) - - # Assert - assert len(data) == 2 - assert isinstance(data[0], FuturesContract) - assert isinstance(data[1], FuturesContract) - instrument = data[0] - assert instrument.id == InstrumentId.from_str("ESH5-ESM5.GLBX") - assert instrument.raw_symbol == Symbol("ESH5-ESM5") - assert instrument.asset_class == AssetClass.INDEX - assert instrument.instrument_class == InstrumentClass.FUTURE - assert instrument.quote_currency == USD - assert not instrument.is_inverse - assert instrument.underlying == "ES" - assert instrument.price_precision == 2 - assert instrument.price_increment == Price.from_str("0.05") - assert instrument.size_precision == 0 - assert instrument.size_increment == 1 - assert instrument.multiplier == 1 - assert instrument.lot_size == 1 - assert instrument.ts_event == 1690848000000000000 - assert instrument.ts_init == 1690848000000000000 - - def test_loader_definition_glbx_options() -> None: # Arrange loader = DatabentoDataLoader() diff --git a/tests/integration_tests/adapters/interactive_brokers/client/test_client.py b/tests/integration_tests/adapters/interactive_brokers/client/test_client.py index ca55104fa357..7b3eaea4ec10 100644 --- a/tests/integration_tests/adapters/interactive_brokers/client/test_client.py +++ b/tests/integration_tests/adapters/interactive_brokers/client/test_client.py @@ -21,73 +21,95 @@ import pytest +from nautilus_trader.test_kit.functions import ensure_all_tasks_completed from nautilus_trader.test_kit.functions import eventually def test_start(ib_client): - # Arrange, Act - ib_client._start() + # Arrange + ib_client._is_ib_connected.set() + ib_client._connect = AsyncMock() + ib_client._eclient = MagicMock() + + # Act + ib_client.start() # Assert assert ib_client._is_client_ready.is_set() -def test_start_client_tasks_and_tws_api(ib_client): +def test_start_tasks(ib_client): # Arrange + ib_client._eclient = MagicMock() ib_client._tws_incoming_msg_reader_task = None ib_client._internal_msg_queue_task = None - ib_client._eclient.startApi = Mock() + ib_client._connection_watchdog_task = None # Act - ib_client._start_client_tasks_and_tws_api() + ib_client._start_tws_incoming_msg_reader() + ib_client._start_internal_msg_queue_processor() + ib_client._start_connection_watchdog() # Assert - assert ib_client._tws_incoming_msg_reader_task - assert ib_client._internal_msg_queue_task - assert ib_client._eclient.startApi.called + # Tasks should be running if there's a (simulated) connection + assert not ib_client._tws_incoming_msg_reader_task.done() + assert not ib_client._internal_msg_queue_processor_task.done() + assert not ib_client._connection_watchdog_task.done() def test_stop(ib_client): # Arrange - ib_client._start_client_tasks_and_tws_api() - ib_client._eclient.disconnect = Mock() + ib_client._is_ib_connected.set() + ib_client._connect = AsyncMock() + ib_client._eclient = MagicMock() + ib_client.start() # Act - ib_client._stop() + ib_client.stop() + ensure_all_tasks_completed() # Assert - assert ib_client._watch_dog_task.cancel() - assert ib_client._tws_incoming_msg_reader_task.cancel() - assert ib_client._internal_msg_queue_task.cancel() - assert ib_client._eclient.disconnect.called + assert ib_client.is_stopped + assert ib_client._connection_watchdog_task.done() + assert ib_client._tws_incoming_msg_reader_task.done() + assert ib_client._internal_msg_queue_processor_task.done() assert not ib_client._is_client_ready.is_set() + assert len(ib_client.registered_nautilus_clients) == 0 def test_reset(ib_client): # Arrange ib_client._stop = Mock() - ib_client._eclient.reset = Mock() + ib_client._start = Mock() # Act - ib_client._reset() + ib_client.reset() # Assert assert ib_client._stop.called - assert ib_client._eclient.reset.called - assert ib_client._watch_dog_task + assert ib_client._start.called + + +def test_resume(ib_client_running): + # Arrange, Act, Assert + ib_client_running._degrade() + + # Act + ib_client_running._resume() + + # Assert + assert ib_client_running._is_client_ready.is_set() -def test_resume(ib_client): +def test_degrade(ib_client_running): # Arrange - ib_client._is_client_ready.clear() - ib_client._connection_attempt_counter = 1 # Act - ib_client._resume() + ib_client_running._degrade() # Assert - assert ib_client._is_client_ready.is_set() - assert ib_client._connection_attempt_counter == 0 + assert not ib_client_running._is_client_ready.is_set() + assert len(ib_client_running._account_ids) == 0 @pytest.mark.asyncio @@ -142,46 +164,29 @@ def test_next_req_id(ib_client): @pytest.mark.asyncio -async def test_wait_until_ready(ib_client): +async def test_wait_until_ready(ib_client_running): # Arrange - ib_client._is_client_ready = Mock() - ib_client._is_client_ready.is_set.return_value = True # Act - await ib_client.wait_until_ready() + await ib_client_running.wait_until_ready() # Assert - # Assert wait was not called since is_client_ready is already set - ib_client._is_client_ready.wait.assert_not_called() + assert True @pytest.mark.asyncio -async def test_run_watch_dog_reconnect(ib_client): +async def test_run_connection_watchdog_reconnect(ib_client): # Arrange + ib_client._is_ib_connected.clear() ib_client._eclient = MagicMock() ib_client._eclient.isConnected.return_value = False - ib_client._reconnect = AsyncMock(side_effect=asyncio.CancelledError) - - # Act - await ib_client._run_watch_dog() - - # Assert - ib_client._reconnect.assert_called() - - -@pytest.mark.asyncio -async def test_run_watch_dog_probe(ib_client): - # Arrange - ib_client._eclient = MagicMock() - ib_client._eclient.isConnected.return_value = True - ib_client._is_ib_ready.clear() - ib_client._probe_for_connectivity = AsyncMock(side_effect=asyncio.CancelledError) + ib_client._handle_disconnection = AsyncMock(side_effect=asyncio.CancelledError) # Act - await ib_client._run_watch_dog() + await ib_client._run_connection_watchdog() # Assert - ib_client._probe_for_connectivity.assert_called() + ib_client._handle_disconnection.assert_called() @pytest.mark.asyncio @@ -194,9 +199,7 @@ async def test_run_tws_incoming_msg_reader(ib_client): with patch("ibapi.comm.read_msg", side_effect=[(None, msg, b"") for msg in test_messages]): # Act - ib_client._tws_incoming_msg_reader_task = ib_client._create_task( - ib_client._run_tws_incoming_msg_reader(), - ) + ib_client._start_tws_incoming_msg_reader() await eventually(lambda: ib_client._internal_msg_queue.qsize() == len(test_messages)) # Assert @@ -205,18 +208,15 @@ async def test_run_tws_incoming_msg_reader(ib_client): @pytest.mark.asyncio -async def test_run_internal_msg_queue(ib_client): +async def test_run_internal_msg_queue(ib_client_running): # Arrange test_messages = [b"test message 1", b"test message 2"] for msg in test_messages: - ib_client._internal_msg_queue.put_nowait(msg) - ib_client._process_message = Mock() + ib_client_running._internal_msg_queue.put_nowait(msg) + ib_client_running._process_message = Mock() # Act - ib_client._internal_msg_queue_task = ib_client._create_task( - ib_client._run_internal_msg_queue(), - ) # Assert - await eventually(lambda: ib_client._process_message.call_count == len(test_messages)) - assert ib_client._internal_msg_queue.qsize() == 0 + await eventually(lambda: ib_client_running._process_message.call_count == len(test_messages)) + assert ib_client_running._internal_msg_queue.qsize() == 0 diff --git a/tests/integration_tests/adapters/interactive_brokers/client/test_client_account.py b/tests/integration_tests/adapters/interactive_brokers/client/test_client_account.py index 10c500a3f239..74ef547d1a38 100644 --- a/tests/integration_tests/adapters/interactive_brokers/client/test_client_account.py +++ b/tests/integration_tests/adapters/interactive_brokers/client/test_client_account.py @@ -64,7 +64,8 @@ async def test_process_account_id(ib_client): ] with patch("ibapi.comm.read_msg", side_effect=[(None, msg, b"") for msg in test_messages]): # Act - ib_client._start_client_tasks_and_tws_api() + ib_client._start_tws_incoming_msg_reader() + ib_client._start_internal_msg_queue_processor() # Assert await eventually(lambda: "DU1234567" in ib_client.accounts()) diff --git a/tests/integration_tests/adapters/interactive_brokers/client/test_client_connection.py b/tests/integration_tests/adapters/interactive_brokers/client/test_client_connection.py index 20fde18b9fbf..12b7e3d904c1 100644 --- a/tests/integration_tests/adapters/interactive_brokers/client/test_client_connection.py +++ b/tests/integration_tests/adapters/interactive_brokers/client/test_client_connection.py @@ -1,63 +1,54 @@ -# ------------------------------------------------------------------------------------------------- -# Copyright (C) 2015-2024 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 unittest.mock import AsyncMock -from unittest.mock import Mock -from unittest.mock import patch +from unittest.mock import MagicMock import pytest -from ibapi.client import EClient +from ibapi.common import NO_VALID_ID +from ibapi.errors import CONNECT_FAIL @pytest.mark.asyncio -async def test_establish_socket_connection(ib_client): - # Arrange - ib_client._eclient.connState = EClient.DISCONNECTED - ib_client._tws_incoming_msg_reader_task = None - ib_client._internal_msg_queue_task = None - ib_client._initialize_connection_params = Mock() +async def test_connect_success(ib_client): + ib_client._initialize_connection_params = MagicMock() ib_client._connect_socket = AsyncMock() ib_client._send_version_info = AsyncMock() ib_client._receive_server_info = AsyncMock() - ib_client._eclient.serverVersion = Mock() - ib_client._eclient.wrapper = Mock() - ib_client._eclient.startApi = Mock() - ib_client._eclient.conn = Mock() - ib_client._eclient.conn.isConnected = Mock(return_value=True) + ib_client._eclient.connTime = MagicMock() + ib_client._eclient.setConnState = MagicMock() + + await ib_client._connect() + + ib_client._initialize_connection_params.assert_called_once() + ib_client._connect_socket.assert_awaited_once() + ib_client._send_version_info.assert_awaited_once() + ib_client._receive_server_info.assert_awaited_once() + ib_client._eclient.setConnState.assert_called_with(ib_client._eclient.CONNECTED) + + +@pytest.mark.asyncio +async def test_connect_cancelled(ib_client): + ib_client._initialize_connection_params = MagicMock() + ib_client._connect_socket = AsyncMock(side_effect=asyncio.CancelledError()) + ib_client._disconnect = AsyncMock() - # Act - await ib_client._establish_socket_connection() + await ib_client._connect() - # Assert - assert ib_client._eclient.isConnected() - assert ib_client._tws_incoming_msg_reader_task - assert ib_client._internal_msg_queue_task - ib_client._eclient.startApi.assert_called_once() + ib_client._disconnect.assert_awaited_once() @pytest.mark.asyncio -async def test_connect_socket(ib_client): - # Arrange - with patch( - "nautilus_trader.adapters.interactive_brokers.client.connection.Connection", - ) as MockConnection: - mock_connection_instance = MockConnection.return_value - mock_connection_instance.connect = Mock() - - # Act - await ib_client._connect_socket() - - # Assert - mock_connection_instance.connect.assert_called_once() +async def test_connect_fail(ib_client): + ib_client._initialize_connection_params = MagicMock() + ib_client._connect_socket = AsyncMock(side_effect=Exception("Connection failed")) + ib_client._disconnect = AsyncMock() + ib_client._handle_reconnect = AsyncMock() + ib_client._eclient.wrapper.error = MagicMock() + + await ib_client._connect() + + ib_client._eclient.wrapper.error.assert_called_with( + NO_VALID_ID, + CONNECT_FAIL.code(), + CONNECT_FAIL.msg(), + ) + ib_client._handle_reconnect.assert_awaited_once() diff --git a/tests/integration_tests/adapters/interactive_brokers/client/test_client_error.py b/tests/integration_tests/adapters/interactive_brokers/client/test_client_error.py index 7949cf2760f7..764531510cb9 100644 --- a/tests/integration_tests/adapters/interactive_brokers/client/test_client_error.py +++ b/tests/integration_tests/adapters/interactive_brokers/client/test_client_error.py @@ -16,54 +16,61 @@ import functools from unittest.mock import Mock +import pytest + +@pytest.mark.asyncio def test_ib_is_ready_by_notification_1101(ib_client): # Arrange - ib_client._is_ib_ready.clear() + ib_client._is_ib_connected.clear() # Act - ib_client.error( - -1, - 1101, - "Connectivity between IB and Trader Workstation has been restored", + ib_client.process_error( + req_id=-1, + error_code=1101, + error_string="Connectivity between IB and Trader Workstation has been restored", ) # Assert - assert ib_client._is_ib_ready.is_set() + assert ib_client._is_ib_connected.is_set() def test_ib_is_ready_by_notification_1102(ib_client): # Arrange - ib_client._is_ib_ready.clear() + ib_client._is_ib_connected.clear() # Act - ib_client.error( - -1, - 1102, - "Connectivity between IB and Trader Workstation has been restored", + ib_client.process_error( + req_id=-1, + error_code=1102, + error_string="Connectivity between IB and Trader Workstation has been restored", ) # Assert - assert ib_client._is_ib_ready.is_set() + assert ib_client._is_ib_connected.is_set() def test_ib_is_not_ready_by_error_10182(ib_client): # Arrange req_id = 6 - ib_client._is_ib_ready.set() + ib_client._is_ib_connected.set() ib_client._subscriptions.add(req_id, "EUR.USD", ib_client._eclient.reqHistoricalData, {}) # Act - ib_client.error(req_id, 10182, "Failed to request live updates (disconnected).") + ib_client.process_error( + req_id=req_id, + error_code=10182, + error_string="Failed to request live updates (disconnected).", + ) # Assert - assert not ib_client._is_ib_ready.is_set() + assert not ib_client._is_ib_connected.is_set() def test_ib_is_not_ready_by_error_10189(ib_client): # Arrange req_id = 6 - ib_client._is_ib_ready.set() + ib_client._is_ib_connected.set() ib_client._subscriptions.add( req_id=req_id, name="EUR.USD", @@ -80,11 +87,11 @@ def test_ib_is_not_ready_by_error_10189(ib_client): ) # Act - ib_client.error( - req_id, - 10189, - "Failed to request tick-by-tick data.BidAsk tick-by-tick requests are not supported for EUR.USD.", + ib_client.process_error( + req_id=req_id, + error_code=10189, + error_string="Failed to request tick-by-tick data.BidAsk tick-by-tick requests are not supported for EUR.USD.", ) # Assert - assert not ib_client._is_ib_ready.is_set() + assert not ib_client._is_ib_connected.is_set() diff --git a/tests/integration_tests/adapters/interactive_brokers/client/test_client_market_data.py b/tests/integration_tests/adapters/interactive_brokers/client/test_client_market_data.py index f757b2b9bb80..1d30943fe701 100644 --- a/tests/integration_tests/adapters/interactive_brokers/client/test_client_market_data.py +++ b/tests/integration_tests/adapters/interactive_brokers/client/test_client_market_data.py @@ -420,14 +420,14 @@ def test_tickByTickBidAsk(ib_client): ib_client._handle_data = Mock() # Act - ib_client.tickByTickBidAsk( - 1, - 1704067200, - 100.01, - 100.02, - Decimal(100), - Decimal(200), - TickAttribBidAsk(), + ib_client.process_tick_by_tick_bid_ask( + req_id=1, + time=1704067200, + bid_price=100.01, + ask_price=100.02, + bid_size=Decimal(100), + ask_size=Decimal(200), + tick_attrib_bid_ask=TickAttribBidAsk(), ) # Assert @@ -453,15 +453,15 @@ def test_tickByTickAllLast(ib_client): ib_client._handle_data = Mock() # Act - ib_client.tickByTickAllLast( - 1, - "Last", - 1704067200, - 100.01, - Decimal(100), - TickAttribLast(), - "", - "", + ib_client.process_tick_by_tick_all_last( + req_id=1, + tick_type="Last", + time=1704067200, + price=100.01, + size=Decimal(100), + tick_attrib_last=TickAttribLast(), + exchange="", + special_conditions="", ) # Assert @@ -488,16 +488,16 @@ def test_realtimeBar(ib_client): ib_client._handle_data = Mock() # Act - ib_client.realtimeBar( - 1, - 1704067200, - 100.01, - 101.00, - 99.01, - 100.50, - Decimal(100), - Decimal(-1), - Decimal(-1), + ib_client.process_realtime_bar( + req_id=1, + time=1704067200, + open_=100.01, + high=101.00, + low=99.01, + close=100.50, + volume=Decimal(100), + wap=Decimal(-1), + count=Decimal(-1), ) # Assert diff --git a/tests/integration_tests/adapters/interactive_brokers/client/test_client_order.py b/tests/integration_tests/adapters/interactive_brokers/client/test_client_order.py index 32aa56071cb3..f496e7eb2150 100644 --- a/tests/integration_tests/adapters/interactive_brokers/client/test_client_order.py +++ b/tests/integration_tests/adapters/interactive_brokers/client/test_client_order.py @@ -119,11 +119,11 @@ def test_openOrder(ib_client): order_state = IBTestExecStubs.ib_order_state(state="PreSubmitted") # Act - ib_client.openOrder( - order_id, - contract, - order, - order_state, + ib_client.process_open_order( + order_id=order_id, + contract=contract, + order=order, + order_state=order_state, ) # Assert @@ -142,18 +142,18 @@ def test_orderStatus(ib_client): ib_client._event_subscriptions.get = MagicMock(return_value=handler_func) # Act - ib_client.orderStatus( - 1, - "Filled", - Decimal("100"), - Decimal("0"), - 100.0, - 1916994655, - 0, - 100.0, - 1, - "", - 0.0, + ib_client.process_order_status( + order_id=1, + status="Filled", + filled=Decimal("100"), + remaining=Decimal("0"), + avg_fill_price=100.0, + perm_id=1916994655, + parent_id=0, + last_fill_price=100.0, + client_id=1, + why_held="", + mkt_cap_price=0.0, ) # Assert @@ -188,10 +188,10 @@ def test_execDetails(ib_client): ib_client._event_subscriptions.get = MagicMock(return_value=handler_func) # Act - ib_client.execDetails( - req_id, - contract, - execution, + ib_client.process_exec_details( + req_id=req_id, + contract=contract, + execution=execution, ) # Assert @@ -223,7 +223,7 @@ def test_commissionReport(ib_client): ib_client._event_subscriptions.get = MagicMock(return_value=handler_func) # Act - ib_client.commissionReport(commission_report) + ib_client.process_commission_report(commission_report=commission_report) # Assert handler_func.assert_called_with( diff --git a/tests/integration_tests/adapters/interactive_brokers/conftest.py b/tests/integration_tests/adapters/interactive_brokers/conftest.py index 7df3e0187bcf..e182892f4e52 100644 --- a/tests/integration_tests/adapters/interactive_brokers/conftest.py +++ b/tests/integration_tests/adapters/interactive_brokers/conftest.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------------------------- +import asyncio +from unittest.mock import AsyncMock +from unittest.mock import MagicMock import pytest @@ -28,6 +31,7 @@ from nautilus_trader.model.events import AccountState from nautilus_trader.model.identifiers import AccountId from nautilus_trader.model.identifiers import Venue +from nautilus_trader.test_kit.functions import ensure_all_tasks_completed from nautilus_trader.test_kit.stubs.events import TestEventStubs from tests.integration_tests.adapters.interactive_brokers.test_kit import IBTestContractStubs @@ -35,6 +39,15 @@ # fmt: on +@pytest.fixture() +def event_loop(): + loop = asyncio.get_event_loop() + asyncio.set_event_loop(loop) + loop.set_debug(True) + yield loop + ensure_all_tasks_completed() + + @pytest.fixture() def venue(): return IB_VENUE @@ -73,9 +86,9 @@ def exec_client_config(): @pytest.fixture() -def ib_client(data_client_config, loop, msgbus, cache, clock): +def ib_client(data_client_config, event_loop, msgbus, cache, clock): client = InteractiveBrokersClient( - loop=loop, + loop=event_loop, msgbus=msgbus, cache=cache, clock=clock, @@ -84,7 +97,18 @@ def ib_client(data_client_config, loop, msgbus, cache, clock): client_id=data_client_config.ibg_client_id, ) yield client - client._stop() + if client.is_running: + client._stop() + + +@pytest.fixture() +def ib_client_running(ib_client): + ib_client._is_ib_connected.set() + ib_client._connect = AsyncMock() + ib_client._eclient = MagicMock() + ib_client._account_ids = {"DU123456,"} + ib_client.start() + yield ib_client @pytest.fixture() @@ -96,11 +120,11 @@ def instrument_provider(ib_client): @pytest.fixture() -def data_client(mocker, data_client_config, venue, loop, msgbus, cache, clock): +def data_client(mocker, data_client_config, venue, event_loop, msgbus, cache, clock): mocker.patch( "nautilus_trader.adapters.interactive_brokers.factories.get_cached_ib_client", return_value=InteractiveBrokersClient( - loop=loop, + loop=event_loop, msgbus=msgbus, cache=cache, clock=clock, @@ -110,23 +134,27 @@ def data_client(mocker, data_client_config, venue, loop, msgbus, cache, clock): ), ) client = InteractiveBrokersLiveDataClientFactory.create( - loop=loop, + loop=event_loop, name=venue.value, config=data_client_config, msgbus=msgbus, cache=cache, clock=clock, ) - client._client.start() + client._client._is_ib_connected.set() + client._client._connect = AsyncMock() + client._client._eclient = MagicMock() + client._client._account_ids = {"DU123456,"} + # client._client.start() return client @pytest.fixture() -def exec_client(mocker, exec_client_config, venue, loop, msgbus, cache, clock): +def exec_client(mocker, exec_client_config, venue, event_loop, msgbus, cache, clock): mocker.patch( "nautilus_trader.adapters.interactive_brokers.factories.get_cached_ib_client", return_value=InteractiveBrokersClient( - loop=loop, + loop=event_loop, msgbus=msgbus, cache=cache, clock=clock, @@ -136,16 +164,18 @@ def exec_client(mocker, exec_client_config, venue, loop, msgbus, cache, clock): ), ) client = InteractiveBrokersLiveExecClientFactory.create( - loop=loop, + loop=event_loop, name=venue.value, config=exec_client_config, msgbus=msgbus, cache=cache, clock=clock, ) - client._client.start() - client._client.managedAccounts("DU123456,") - client._client.nextValidId(1) + client._client._is_ib_connected.set() + client._client._connect = AsyncMock() + client._client._eclient = MagicMock() + client._client._account_ids = {"DU123456,"} + # client._client.start() return client diff --git a/tests/integration_tests/live/test_live_node.py b/tests/integration_tests/live/test_live_node.py index 4aba914e7c3e..745166b907d5 100644 --- a/tests/integration_tests/live/test_live_node.py +++ b/tests/integration_tests/live/test_live_node.py @@ -23,7 +23,6 @@ from nautilus_trader.adapters.binance.config import BinanceExecClientConfig from nautilus_trader.adapters.binance.factories import BinanceLiveDataClientFactory from nautilus_trader.adapters.binance.factories import BinanceLiveExecClientFactory -from nautilus_trader.common.component import init_logging from nautilus_trader.config import InstrumentProviderConfig from nautilus_trader.config import LoggingConfig from nautilus_trader.config import TradingNodeConfig @@ -89,10 +88,6 @@ class TestTradingNodeConfiguration: - def setup(self): - # Pre-initialize logging and bypass to avoid the `InvalidConfiguration` exception - init_logging(bypass=True) - def teardown(self): ensure_all_tasks_completed() @@ -211,10 +206,6 @@ def test_setting_instance_id(self, monkeypatch): class TestTradingNodeOperation: - def setup(self): - # Pre-initialize logging and bypass to avoid the `InvalidConfiguration` exception - init_logging(bypass=True) - def teardown(self): ensure_all_tasks_completed() diff --git a/tests/performance_tests/test_perf_logger.py b/tests/performance_tests/test_perf_logger.py index 472cd7890ad5..6712147054e6 100644 --- a/tests/performance_tests/test_perf_logger.py +++ b/tests/performance_tests/test_perf_logger.py @@ -18,12 +18,14 @@ from nautilus_trader.common.component import Logger from nautilus_trader.common.component import init_logging +from nautilus_trader.common.component import is_logging_initialized from nautilus_trader.common.enums import LogLevel def test_logging(benchmark: Any) -> None: random.seed(45362718) - init_logging(level_stdout=LogLevel.ERROR, bypass=True) + if not is_logging_initialized: + init_logging(level_stdout=LogLevel.ERROR, bypass=True) logger = Logger(name="TEST_LOGGER") diff --git a/tests/unit_tests/config/test_common.py b/tests/unit_tests/config/test_common.py index 8522c3fb6e27..0afb6582022c 100644 --- a/tests/unit_tests/config/test_common.py +++ b/tests/unit_tests/config/test_common.py @@ -40,6 +40,17 @@ from nautilus_trader.test_kit.providers import TestInstrumentProvider +def test_repr_with_redacted_password() -> None: + # Arrange + config = DatabaseConfig(username="username", password="password") + + # Act, Assert + assert ( + repr(config) + == "DatabaseConfig(type=redis, host=None, port=None, username=username, password=pa...rd, ssl=False, timeout=20)" + ) + + def test_equality_hash_repr() -> None: # Arrange config1 = DatabaseConfig() @@ -51,7 +62,7 @@ def test_equality_hash_repr() -> None: assert isinstance(hash(config1), int) assert ( repr(config1) - == "DatabaseConfig(type='redis', host=None, port=None, username=None, password=None, ssl=False)" + == "DatabaseConfig(type=redis, host=None, port=None, username=None, password=None, ssl=False, timeout=20)" ) @@ -60,7 +71,7 @@ def test_config_id() -> None: config = DatabaseConfig() # Act, Assert - assert config.id == "18a63bfe7acf0b0126940542dc4e261c58e326db70194e5c65949e26a2f5bf1b" + assert config.id == "c3fad60cbcd4eb9d9f19081f6f342f04a77f1328e9487f11696f9abc119ff0e1" def test_fully_qualified_name() -> None: @@ -83,6 +94,7 @@ def test_dict() -> None: "username": None, "password": None, "ssl": False, + "timeout": 20, } @@ -93,7 +105,7 @@ def test_json() -> None: # Act, Assert assert ( config.json() - == b'{"type":"redis","host":null,"port":null,"username":null,"password":null,"ssl":false}' + == b'{"type":"redis","host":null,"port":null,"username":null,"password":null,"ssl":false,"timeout":20}' ) diff --git a/tests/unit_tests/model/instruments/__init__.py b/tests/unit_tests/model/instruments/__init__.py index e69de29bb2d1..3d34cab4588e 100644 --- a/tests/unit_tests/model/instruments/__init__.py +++ b/tests/unit_tests/model/instruments/__init__.py @@ -0,0 +1,14 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2024 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. +# ------------------------------------------------------------------------------------------------- diff --git a/tests/unit_tests/model/instruments/test_futures_contract_pyo3.py b/tests/unit_tests/model/instruments/test_futures_contract_pyo3.py index 1dcebd0f9687..1184a5a6b397 100644 --- a/tests/unit_tests/model/instruments/test_futures_contract_pyo3.py +++ b/tests/unit_tests/model/instruments/test_futures_contract_pyo3.py @@ -13,8 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from nautilus_trader.core.nautilus_pyo3 import FuturesContract -from nautilus_trader.model.instruments import FuturesContract as LegacyFuturesContract +from nautilus_trader.core import nautilus_pyo3 +from nautilus_trader.model.instruments import FuturesContract from nautilus_trader.test_kit.rust.instruments_pyo3 import TestInstrumentProviderPyo3 @@ -33,7 +33,7 @@ def test_hash(): def test_to_dict(): result = _ES_FUTURE.to_dict() - assert FuturesContract.from_dict(result) == _ES_FUTURE + assert nautilus_pyo3.FuturesContract.from_dict(result) == _ES_FUTURE assert result == { "type": "FuturesContract", "id": "ESZ1.GLBX", @@ -45,12 +45,17 @@ def test_to_dict(): "currency": "USD", "price_precision": 2, "price_increment": "0.01", + "size_precision": 0, + "size_increment": "1", "multiplier": "1", "lot_size": "1", "max_price": None, "max_quantity": None, "min_price": None, - "min_quantity": None, + "min_quantity": "1", + "margin_init": "0", + "margin_maint": "0", + "info": {}, "ts_event": 0, "ts_init": 0, "exchange": "XCME", @@ -58,6 +63,18 @@ def test_to_dict(): def test_legacy_futures_contract_from_pyo3(): - future = LegacyFuturesContract.from_pyo3(_ES_FUTURE) + future = FuturesContract.from_pyo3(_ES_FUTURE) assert future.id.value == "ESZ1.GLBX" + + +def test_pyo3_cython_conversion(): + futures_contract_pyo3 = TestInstrumentProviderPyo3.futures_contract_es() + futures_contract_pyo3_dict = futures_contract_pyo3.to_dict() + futures_contract_cython = FuturesContract.from_pyo3(futures_contract_pyo3) + futures_contract_cython_dict = FuturesContract.to_dict(futures_contract_cython) + futures_contract_pyo3_back = nautilus_pyo3.FuturesContract.from_dict( + futures_contract_cython_dict, + ) + assert futures_contract_pyo3 == futures_contract_pyo3_back + assert futures_contract_pyo3_dict == futures_contract_cython_dict diff --git a/tests/unit_tests/model/instruments/test_futures_spread_pyo3.py b/tests/unit_tests/model/instruments/test_futures_spread_pyo3.py index 203537876dbf..3af59ecb5cce 100644 --- a/tests/unit_tests/model/instruments/test_futures_spread_pyo3.py +++ b/tests/unit_tests/model/instruments/test_futures_spread_pyo3.py @@ -13,8 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from nautilus_trader.core.nautilus_pyo3 import FuturesSpread -from nautilus_trader.model.instruments import FuturesSpread as LegacyFuturesSpread +from nautilus_trader.core import nautilus_pyo3 +from nautilus_trader.model.instruments import FuturesSpread from nautilus_trader.test_kit.rust.instruments_pyo3 import TestInstrumentProviderPyo3 @@ -33,7 +33,7 @@ def test_hash(): def test_to_dict(): result = _ES_FUTURES_SPREAD.to_dict() - assert FuturesSpread.from_dict(result) == _ES_FUTURES_SPREAD + assert nautilus_pyo3.FuturesSpread.from_dict(result) == _ES_FUTURES_SPREAD assert result == { "type": "FuturesSpread", "id": "ESM4-ESU4.GLBX", @@ -46,12 +46,17 @@ def test_to_dict(): "currency": "USD", "price_precision": 2, "price_increment": "0.01", + "size_increment": "1", + "size_precision": 0, "multiplier": "1", "lot_size": "1", "max_price": None, "max_quantity": None, "min_price": None, - "min_quantity": None, + "min_quantity": "1", + "margin_init": "0", + "margin_maint": "0", + "info": {}, "ts_event": 0, "ts_init": 0, "exchange": "XCME", @@ -59,6 +64,16 @@ def test_to_dict(): def test_legacy_futures_contract_from_pyo3(): - future = LegacyFuturesSpread.from_pyo3(_ES_FUTURES_SPREAD) + future = FuturesSpread.from_pyo3(_ES_FUTURES_SPREAD) assert future.id.value == "ESM4-ESU4.GLBX" + + +def test_pyo3_cython_conversion(): + futures_spread_pyo3 = TestInstrumentProviderPyo3.futures_spread_es() + futures_spread_pyo3_dict = futures_spread_pyo3.to_dict() + futures_spread_cython = FuturesSpread.from_pyo3(futures_spread_pyo3) + futures_spread_cython_dict = FuturesSpread.to_dict(futures_spread_cython) + futures_spread_pyo3_back = nautilus_pyo3.FuturesSpread.from_dict(futures_spread_cython_dict) + assert futures_spread_pyo3_dict == futures_spread_cython_dict + assert futures_spread_pyo3 == futures_spread_pyo3_back diff --git a/tests/unit_tests/model/instruments/test_options_contract_pyo3.py b/tests/unit_tests/model/instruments/test_options_contract_pyo3.py index c42b665e2f26..9aa2b4c9e775 100644 --- a/tests/unit_tests/model/instruments/test_options_contract_pyo3.py +++ b/tests/unit_tests/model/instruments/test_options_contract_pyo3.py @@ -13,8 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from nautilus_trader.core.nautilus_pyo3 import OptionsContract -from nautilus_trader.model.instruments import OptionsContract as LegacyOptionsContract +from nautilus_trader.core import nautilus_pyo3 +from nautilus_trader.model.instruments import OptionsContract from nautilus_trader.test_kit.rust.instruments_pyo3 import TestInstrumentProviderPyo3 @@ -33,7 +33,7 @@ def test_hash(): def test_to_dict(): result = _AAPL_OPTION.to_dict() - assert OptionsContract.from_dict(result) == _AAPL_OPTION + assert nautilus_pyo3.OptionsContract.from_dict(result) == _AAPL_OPTION assert result == { "type": "OptionsContract", "id": "AAPL211217C00150000.OPRA", @@ -48,18 +48,35 @@ def test_to_dict(): "currency": "USDT", "price_precision": 2, "price_increment": "0.01", + "size_increment": "1", + "size_precision": 0, "multiplier": "1", "lot_size": "1", "max_quantity": None, - "min_quantity": None, + "min_quantity": "1", "max_price": None, "min_price": None, + "margin_init": "0", + "margin_maint": "0", + "info": {}, "ts_event": 0, "ts_init": 0, } def test_legacy_options_contract_from_pyo3(): - option = LegacyOptionsContract.from_pyo3(_AAPL_OPTION) + option = OptionsContract.from_pyo3(_AAPL_OPTION) assert option.id.value == "AAPL211217C00150000.OPRA" + + +def test_pyo3_cython_conversion(): + options_contract_pyo3 = TestInstrumentProviderPyo3.aapl_option() + options_contract_pyo3_dict = options_contract_pyo3.to_dict() + options_contract_cython = OptionsContract.from_pyo3(options_contract_pyo3) + options_contract_cython_dict = OptionsContract.to_dict(options_contract_cython) + options_contract_pyo3_back = nautilus_pyo3.OptionsContract.from_dict( + options_contract_cython_dict, + ) + assert options_contract_cython_dict == options_contract_pyo3_dict + assert options_contract_pyo3 == options_contract_pyo3_back diff --git a/tests/unit_tests/model/instruments/test_options_spread_pyo3.py b/tests/unit_tests/model/instruments/test_options_spread_pyo3.py index fa4742a768ba..8cb2ceb5afab 100644 --- a/tests/unit_tests/model/instruments/test_options_spread_pyo3.py +++ b/tests/unit_tests/model/instruments/test_options_spread_pyo3.py @@ -13,8 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from nautilus_trader.core.nautilus_pyo3 import OptionsSpread -from nautilus_trader.model.instruments import OptionsSpread as LegacyOptionsSpread +from nautilus_trader.core import nautilus_pyo3 +from nautilus_trader.model.instruments import OptionsSpread from nautilus_trader.test_kit.rust.instruments_pyo3 import TestInstrumentProviderPyo3 @@ -33,7 +33,7 @@ def test_hash(): def test_to_dict(): result = _OPTIONS_SPREAD.to_dict() - assert OptionsSpread.from_dict(result) == _OPTIONS_SPREAD + assert nautilus_pyo3.OptionsSpread.from_dict(result) == _OPTIONS_SPREAD assert result == { "type": "OptionsSpread", "id": "UD:U$: GN 2534559.GLBX", @@ -47,18 +47,33 @@ def test_to_dict(): "currency": "USDT", "price_precision": 2, "price_increment": "0.01", + "size_increment": "1", + "size_precision": 0, "multiplier": "1", "lot_size": "1", "max_quantity": None, - "min_quantity": None, + "min_quantity": "1", "max_price": None, "min_price": None, + "margin_init": "0", + "margin_maint": "0", + "info": {}, "ts_event": 0, "ts_init": 0, } def test_legacy_options_contract_from_pyo3(): - option = LegacyOptionsSpread.from_pyo3(_OPTIONS_SPREAD) + option = OptionsSpread.from_pyo3(_OPTIONS_SPREAD) assert option.id.value == "UD:U$: GN 2534559.GLBX" + + +def test_pyo3_cython_conversion(): + options_spread_pyo3 = TestInstrumentProviderPyo3.options_spread() + options_spread_pyo3_dict = options_spread_pyo3.to_dict() + options_spread_cython = OptionsSpread.from_pyo3(options_spread_pyo3) + options_spread_cython_dict = OptionsSpread.to_dict(options_spread_cython) + options_spread_pyo3_back = nautilus_pyo3.OptionsSpread.from_dict(options_spread_cython_dict) + assert options_spread_cython_dict == options_spread_pyo3_dict + assert options_spread_pyo3 == options_spread_pyo3_back diff --git a/tests/unit_tests/model/objects/__init__.py b/tests/unit_tests/model/objects/__init__.py index e69de29bb2d1..3d34cab4588e 100644 --- a/tests/unit_tests/model/objects/__init__.py +++ b/tests/unit_tests/model/objects/__init__.py @@ -0,0 +1,14 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2024 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. +# ------------------------------------------------------------------------------------------------- diff --git a/tests/unit_tests/model/orders/__init__.py b/tests/unit_tests/model/orders/__init__.py new file mode 100644 index 000000000000..3d34cab4588e --- /dev/null +++ b/tests/unit_tests/model/orders/__init__.py @@ -0,0 +1,14 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2024 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. +# ------------------------------------------------------------------------------------------------- diff --git a/tests/unit_tests/model/orders/test_limit_order_pyo3.py b/tests/unit_tests/model/orders/test_limit_order_pyo3.py new file mode 100644 index 000000000000..d1d79bb19d0a --- /dev/null +++ b/tests/unit_tests/model/orders/test_limit_order_pyo3.py @@ -0,0 +1,82 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2024 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.core import nautilus_pyo3 +from nautilus_trader.core.nautilus_pyo3 import ExecAlgorithmId +from nautilus_trader.core.nautilus_pyo3 import InstrumentId +from nautilus_trader.core.nautilus_pyo3 import OrderSide +from nautilus_trader.core.nautilus_pyo3 import OrderStatus +from nautilus_trader.core.nautilus_pyo3 import OrderType +from nautilus_trader.core.nautilus_pyo3 import Price +from nautilus_trader.core.nautilus_pyo3 import Quantity +from nautilus_trader.core.nautilus_pyo3 import TimeInForce +from nautilus_trader.model.orders import LimitOrder +from nautilus_trader.test_kit.rust.orders_pyo3 import TestOrderProviderPyo3 + + +AUDUSD_SIM = InstrumentId.from_str("AUD/USD.SIM") + + +def test_initialize_limit_order(): + order = TestOrderProviderPyo3.limit_order( + instrument_id=AUDUSD_SIM, + order_side=OrderSide.BUY, + quantity=Quantity.from_int(100_000), + price=Price.from_str("1.00000"), + exec_algorithm_id=ExecAlgorithmId("TWAP"), + ) + + # Assert + assert order.order_type == OrderType.LIMIT + assert order.expire_time == 0 + assert order.status == OrderStatus.INITIALIZED + assert order.time_in_force == TimeInForce.GTC + assert order.has_price + assert not order.has_trigger_price + assert order.is_passive + assert not order.is_open + assert not order.is_aggressive + assert not order.is_closed + assert not order.is_emulated + assert order.is_active_local + assert order.is_primary + assert not order.is_spawned + assert ( + str(order) + == "LimitOrder(BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, status=INITIALIZED, " + + "client_order_id=O-20210410-022422-001-001-1, venue_order_id=None, position_id=None, " + + "exec_algorithm_id=TWAP, exec_spawn_id=O-20210410-022422-001-001-1, tags=None)" + ) + assert ( + repr(order) + == "LimitOrder(BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, status=INITIALIZED, " + + "client_order_id=O-20210410-022422-001-001-1, venue_order_id=None, position_id=None, " + + "exec_algorithm_id=TWAP, exec_spawn_id=O-20210410-022422-001-001-1, tags=None)" + ) + + +def test_pyo3_cython_conversion(): + limit_order_pyo3 = TestOrderProviderPyo3.limit_order( + instrument_id=AUDUSD_SIM, + order_side=OrderSide.BUY, + quantity=Quantity.from_int(100_000), + price=Price.from_str("1.00000"), + ) + limit_order_pyo3_dict = limit_order_pyo3.to_dict() + limit_order_cython = LimitOrder.from_pyo3(limit_order_pyo3) + limit_order_cython_dict = LimitOrder.to_dict(limit_order_cython) + limit_order_pyo3_back = nautilus_pyo3.LimitOrder.from_dict(limit_order_cython_dict) + assert limit_order_pyo3_dict == limit_order_cython_dict + assert limit_order_pyo3 == limit_order_pyo3_back diff --git a/tests/unit_tests/model/test_orders_pyo3.py b/tests/unit_tests/model/orders/test_market_order_pyo3.py similarity index 73% rename from tests/unit_tests/model/test_orders_pyo3.py rename to tests/unit_tests/model/orders/test_market_order_pyo3.py index cd80a9b0e338..a76f88beac29 100644 --- a/tests/unit_tests/model/test_orders_pyo3.py +++ b/tests/unit_tests/model/orders/test_market_order_pyo3.py @@ -15,17 +15,16 @@ import pytest -from nautilus_trader.core.nautilus_pyo3 import UUID4 +from nautilus_trader.core import nautilus_pyo3 from nautilus_trader.core.nautilus_pyo3 import AccountId -from nautilus_trader.core.nautilus_pyo3 import ClientOrderId from nautilus_trader.core.nautilus_pyo3 import InstrumentId -from nautilus_trader.core.nautilus_pyo3 import MarketOrder from nautilus_trader.core.nautilus_pyo3 import OrderSide from nautilus_trader.core.nautilus_pyo3 import PositionSide from nautilus_trader.core.nautilus_pyo3 import Quantity from nautilus_trader.core.nautilus_pyo3 import StrategyId from nautilus_trader.core.nautilus_pyo3 import TimeInForce from nautilus_trader.core.nautilus_pyo3 import TraderId +from nautilus_trader.model.orders import MarketOrder from nautilus_trader.test_kit.rust.orders_pyo3 import TestOrderProviderPyo3 @@ -49,7 +48,7 @@ ) def test_opposite_side_returns_expected_sides(side, expected): # Arrange, Act - result = MarketOrder.opposite_side(side) + result = nautilus_pyo3.MarketOrder.opposite_side(side) # Assert assert result == expected @@ -67,7 +66,7 @@ def test_closing_side_returns_expected_sides( expected: OrderSide, ) -> None: # Arrange, Act - result = MarketOrder.closing_side(side) + result = nautilus_pyo3.MarketOrder.closing_side(side) # Assert assert result == expected @@ -110,29 +109,39 @@ def test_would_reduce_only_with_various_values_returns_expected( def test_market_order_with_quantity_zero_raises_value_error(): # Arrange, Act, Assert with pytest.raises(ValueError): - MarketOrder( - trader_id, - strategy_id, - AUDUSD_SIM, - ClientOrderId("O-123456"), - OrderSide.BUY, - Quantity.zero(), # <- invalid - UUID4(), - 0, + TestOrderProviderPyo3.market_order( + trader_id=trader_id, + strategy_id=strategy_id, + instrument_id=AUDUSD_SIM, + order_side=OrderSide.BUY, + quantity=Quantity.from_int(0), ) def test_market_order_with_invalid_tif_raises_value_error(): # Arrange, Act, Assert with pytest.raises(ValueError): - MarketOrder( - trader_id, - strategy_id, - AUDUSD_SIM, - ClientOrderId("O-123456"), - OrderSide.BUY, - Quantity.from_int(100_000), - UUID4(), - 0, - TimeInForce.GTD, # <-- invalid + TestOrderProviderPyo3.market_order( + trader_id=trader_id, + strategy_id=strategy_id, + instrument_id=AUDUSD_SIM, + order_side=OrderSide.BUY, + quantity=Quantity.from_int(0), + time_in_force=TimeInForce.GTD, ) + + +def test_pyo3_cython_conversion(): + market_order_pyo3 = TestOrderProviderPyo3.market_order( + trader_id=trader_id, + strategy_id=strategy_id, + instrument_id=AUDUSD_SIM, + order_side=OrderSide.BUY, + quantity=Quantity.from_int(1), + ) + market_order_pyo3_dict = market_order_pyo3.to_dict() + market_order_cython = MarketOrder.from_pyo3(market_order_pyo3) + market_order_cython_dict = MarketOrder.to_dict(market_order_cython) + market_order_pyo3_back = nautilus_pyo3.MarketOrder.from_dict(market_order_cython_dict) + assert market_order_pyo3_dict == market_order_cython_dict + assert market_order_pyo3 == market_order_pyo3_back diff --git a/tests/unit_tests/model/test_identifiers.py b/tests/unit_tests/model/test_identifiers.py index 43bc9d37541f..e32e02323cdb 100644 --- a/tests/unit_tests/model/test_identifiers.py +++ b/tests/unit_tests/model/test_identifiers.py @@ -208,11 +208,11 @@ def test_instrument_id_from_str() -> None: ], [ ".USDT", - "Error parsing `InstrumentId` from '.USDT': Condition failed: invalid string for `Symbol` value, was empty", + "Error parsing `InstrumentId` from '.USDT': Condition failed: invalid string for 'value', was empty", ], [ "BTC.", - "Error parsing `InstrumentId` from 'BTC.': Condition failed: invalid string for `Venue` value, was empty", + "Error parsing `InstrumentId` from 'BTC.': Condition failed: invalid string for 'value', was empty", ], ], ) diff --git a/tests/unit_tests/model/test_identifiers_pyo3.py b/tests/unit_tests/model/test_identifiers_pyo3.py index f6b42f7936d1..dfdf020c92e7 100644 --- a/tests/unit_tests/model/test_identifiers_pyo3.py +++ b/tests/unit_tests/model/test_identifiers_pyo3.py @@ -190,11 +190,11 @@ def test_instrument_id_from_str() -> None: ], [ ".USDT", - "Error parsing `InstrumentId` from '.USDT': Condition failed: invalid string for `Symbol` value, was empty", + "Error parsing `InstrumentId` from '.USDT': Condition failed: invalid string for 'value', was empty", ], [ "BTC.", - "Error parsing `InstrumentId` from 'BTC.': Condition failed: invalid string for `Venue` value, was empty", + "Error parsing `InstrumentId` from 'BTC.': Condition failed: invalid string for 'value', was empty", ], ], ) diff --git a/tests/unit_tests/model/test_instrument.py b/tests/unit_tests/model/test_instrument.py index d53d804328aa..cf053181387e 100644 --- a/tests/unit_tests/model/test_instrument.py +++ b/tests/unit_tests/model/test_instrument.py @@ -266,6 +266,10 @@ def test_future_instrument_to_dict(self): "currency": "USD", "activation_ns": 1622842200000000000, "expiration_ns": 1702650600000000000, + "max_price": None, + "max_quantity": None, + "min_price": None, + "min_quantity": "1", "lot_size": "1", "margin_init": "0", "margin_maint": "0", @@ -297,6 +301,10 @@ def test_option_instrument_to_dict(self): "expiration_ns": 1639699200000000000, "option_kind": "CALL", "lot_size": "1", + "max_price": None, + "max_quantity": None, + "min_price": None, + "min_quantity": "1", "margin_init": "0", "margin_maint": "0", "multiplier": "100", diff --git a/tests/unit_tests/model/test_orders.py b/tests/unit_tests/model/test_orders.py index 0a5b8fcf141a..eb47fb268745 100644 --- a/tests/unit_tests/model/test_orders.py +++ b/tests/unit_tests/model/test_orders.py @@ -384,6 +384,8 @@ def test_market_order_to_dict(self): # Act result = order.to_dict() + # remove init_id as it non-deterministic with order-factory + del result["init_id"] # Assert assert result == { @@ -405,7 +407,8 @@ def test_market_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": None, + "commissions": {}, + "emulation_trigger": "NO_TRIGGER", "status": "INITIALIZED", "contingency_type": "NO_CONTINGENCY", "order_list_id": None, @@ -468,6 +471,8 @@ def test_limit_order_to_dict(self): # Act result = order.to_dict() + # remove init_id as it non-deterministic with order-factory + del result["init_id"] # Assert assert result == { @@ -489,7 +494,7 @@ def test_limit_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": None, + "commissions": {}, "status": "INITIALIZED", "is_post_only": False, "is_reduce_only": False, diff --git a/version.json b/version.json index 9ffa1cf78a3e..619e83a7a44c 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, "label": "", - "message": "v1.189.0", + "message": "v1.190.0", "color": "orange" }