diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ede213706e0..1092b9be56b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -238,7 +238,7 @@ jobs: strategy: fail-fast: false matrix: - arch: [x64] + arch: [arm64] os: [macos-latest] python-version: ["3.10", "3.11", "3.12"] defaults: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 40eafdd92e3..40585970422 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,12 +31,12 @@ repos: # - id: gitlint-ci - repo: https://github.com/codespell-project/codespell - rev: v2.2.6 + rev: v2.3.0 hooks: - id: codespell description: Checks for common misspellings. types_or: [python, cython, rst, markdown] - args: ["-L", "crate,ot,zar"] + args: ["-L", "crate,ot,socio-economic,zar"] ############################################################################## # Rust formatting and linting @@ -82,7 +82,7 @@ repos: exclude: "docs/_pygments/monokai.py" - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.5 + rev: v0.4.6 hooks: - id: ruff args: ["--fix"] diff --git a/README.md b/README.md index 7b4a7a2c1ce..a3b2e979ed2 100644 --- a/README.md +++ b/README.md @@ -43,16 +43,16 @@ including FX, Equities, Futures, Options, CFDs, Crypto and Betting - across mult ## Features -- **Fast** - Core written in Rust with asynchronous networking using [tokio](https://crates.io/crates/tokio) -- **Reliable** - Type safety and thread safety through Rust. Redis backed performant state persistence -- **Portable** - OS independent, runs on Linux, macOS, Windows. Deploy using Docker -- **Flexible** - Modular adapters mean any REST, WebSocket, or FIX API can be integrated -- **Advanced** - Time in force `IOC`, `FOK`, `GTD`, `AT_THE_OPEN`, `AT_THE_CLOSE`, advanced order types and conditional triggers. Execution instructions `post-only`, `reduce-only`, and icebergs. Contingency order lists including `OCO`, `OTO` -- **Customizable** - Add user defined custom components, or assemble entire systems from scratch leveraging the cache and message bus -- **Backtesting** - Run with multiple venues, instruments and strategies simultaneously using historical quote tick, trade tick, bar, order book and custom data with nanosecond resolution -- **Live** - Use identical strategy implementations between backtesting and live deployments -- **Multi-venue** - Multiple venue capabilities facilitate market making and statistical arbitrage strategies -- **AI Agent Training** - Backtest engine fast enough to be used to train AI trading agents (RL/ES) +- **Fast:** Core written in Rust with asynchronous networking using [tokio](https://crates.io/crates/tokio) +- **Reliable:** Type safety and thread safety through Rust. Redis backed performant state persistence +- **Portable:** OS independent, runs on Linux, macOS, Windows. Deploy using Docker +- **Flexible:** Modular adapters mean any REST, WebSocket, or FIX API can be integrated +- **Advanced:** Time in force `IOC`, `FOK`, `GTD`, `AT_THE_OPEN`, `AT_THE_CLOSE`, advanced order types and conditional triggers. Execution instructions `post-only`, `reduce-only`, and icebergs. Contingency order lists including `OCO`, `OTO` +- **Customizable:** Add user defined custom components, or assemble entire systems from scratch leveraging the cache and message bus +- **Backtesting:** Run with multiple venues, instruments and strategies simultaneously using historical quote tick, trade tick, bar, order book and custom data with nanosecond resolution +- **Live:** Use identical strategy implementations between backtesting and live deployments +- **Multi-venue:** Multiple venue capabilities facilitate market making and statistical arbitrage strategies +- **AI Training:** Backtest engine fast enough to be used to train AI trading agents (RL/ES) ![Alt text](https://github.com/nautechsystems/nautilus_trader/blob/develop/docs/_images/nautilus-art.png?raw=true "nautilus") @@ -63,10 +63,10 @@ including FX, Equities, Futures, Options, CFDs, Crypto and Betting - across mult ## Why NautilusTrader? -- **Highly performant event-driven Python** - native binary core components -- **Parity between backtesting and live trading** - identical strategy code -- **Reduced operational risk** - risk management functionality, logical correctness and type safety -- **Highly extendable** - message bus, custom components and actors, custom data, custom adapters +- **Highly performant event-driven Python:** Native binary core components +- **Parity between backtesting and live trading:** Identical strategy code +- **Reduced operational risk:** Risk management functionality, logical correctness and type safety +- **Highly extendable:** Message bus, custom components and actors, custom data, custom adapters Traditionally, trading strategy research and backtesting might be conducted in Python (or other suitable language) using vectorized methods, with the strategy then needing to be reimplemented in a more event-drive way @@ -129,8 +129,8 @@ into a unified interface. The following integrations are currently supported: | [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) +- **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 @@ -196,19 +196,23 @@ Refer to the [Installation Guide](https://docs.nautilustrader.io/getting_started ## Versioning and releases -NautilusTrader is currently following a bi-weekly beta release schedule. +NautilusTrader is currently targeting a weekly release schedule, occasionally there may be experimental +or larger features which will delay a release by several weeks. + The API is becoming more stable, however breaking changes are still possible between releases. Documentation of these changes in the release notes are made on a best-effort basis. ### Branches +We aim to maintain a stable passing build on all branches. + - `master` branch will always reflect the source code for the latest released version - `nightly` branch may contain experimental features and is generally merged from `develop` branch daily, and also when required -- `develop` branch is normally very active with frequent commits and may contain experimental features. We aim to maintain a stable - passing build on this branch +- `develop` branch is normally very active with frequent commits and may contain experimental features -The current roadmap has a goal of achieving a stable API for a `2.x` version. From this -point we will follow a formal process for releases, with deprecation periods for any API changes. +The current roadmap has a goal of achieving a stable API for a `2.x` version (likely post Rust port). +From this point we will follow a formal process for releases, with deprecation periods for any API changes. +This allows us to maintain a maximum pace of development for now. ## Makefile diff --git a/RELEASES.md b/RELEASES.md index 98f0e7a6932..c298ed67395 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,31 @@ +# NautilusTrader 1.194.0 Beta + +Released on 31st May 2024 (UTC). + +### Enhancements +- Added `DataEngine` order book deltas buffering to `F_LAST` flag (#1673), thanks @davidsblom +- Added `DataEngineConfig.buffer_deltas` config option for the above (#1670), thanks @davidsblom +- Improved Bybit order book deltas parsing to set `F_LAST` flag (#1670), thanks @davidsblom +- Improved Bybit handling for top-of-book quotes and order book deltas (#1672), thanks @davidsblom +- Improved Interactive Brokers integration test mocks (#1669), thanks @rsmb7z +- Improved error message when no tick scheme initialized for an instrument, thanks for reporting @VeraLyu +- Improved `SandboxExecutionClient` instrument handling (instruments just need to be added to cache) +- Ported `VolumeWeightedAveragePrice` indicator to Rust (#1665), thanks @Pushkarm029 +- Ported `VerticalHorizontalFilter` indicator to Rust (#1666), thanks @Pushkarm029 + +### Breaking Changes +None + +### Fixes +- Fixed `SimulatedExchange` processing of commands in real-time for sandbox mode +- Fixed `DataEngine` unsubscribe handling (edge case would attempt to unsubscribe from the client multiple times) +- Fixed Bybit order book deltas parsing (was appending bid side twice) (#1668), thanks @davidsblom +- Fixed Binance instruments price and size precision parsing (was incorrectly stripping trailing zeros) +- Fixed `BinanceBar` streaming feather writing (was not setting up writer) +- Fixed backtest high-level tutorial documentation errors, thanks for reporting @Leonz5288 + +--- + # NautilusTrader 1.193.0 Beta Released on 24th May 2024 (UTC). @@ -72,7 +100,7 @@ Released on 20th April 2024 (UTC). - Improved `modify_order` error logging when order values remain unchanged - Added `RecordFlag` enum for Rust and Python - Interactive Brokers further improvements and fixes, thanks @rsmb7z -- Ported Bias indicator to Rust, thanks @Pushkarm029 +- Ported `Bias` indicator to Rust, thanks @Pushkarm029 ### Breaking Changes - Reordered `OrderBookDelta` params `flags` and `sequence` and removed default 0 values (more explicit and less chance of mismatches) @@ -111,8 +139,8 @@ Released on 22nd March 2024 (UTC). - 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) -- Ported ChandeMomentumOscillator indicator to Rust, thanks @Pushkarm029 -- Ported VIDYA indicator to Rust, thanks @Pushkarm029 +- Ported `ChandeMomentumOscillator` indicator to Rust, thanks @Pushkarm029 +- Ported `VIDYA` indicator to Rust, thanks @Pushkarm029 - 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 @@ -355,7 +383,7 @@ Released on 23rd December 2023 (UTC). - Changed `StrategyConfig.strategy_id` to type `StrategyId | None` - Changed `Instrument`, `OrderFilled` and `AccountState` `info` field serialization due below fix (you'll need to flush your cache) - Changed `CacheConfig` to take a `DatabaseConfig` (better symmetry with `MessageBusConfig`) -- Changed `RedisCacheDatabase` data structure for currencies from hashset to simpler key-value (you'll need to clear cache or delete all curreny keys) +- Changed `RedisCacheDatabase` data structure for currencies from hashset to simpler key-value (you'll need to clear cache or delete all currency keys) - Changed `Actor` state loading to now use the standard `Serializer` - Renamed `register_json_encoding` to `register_config_encoding` - Renamed `register_json_decoding` to `register_config_decoding` diff --git a/docs/api_reference/index.md b/docs/api_reference/index.md index 86e7a057ab9..4aafd5a097b 100644 --- a/docs/api_reference/index.md +++ b/docs/api_reference/index.md @@ -34,8 +34,8 @@ from the latest NautilusTrader source code using [Sphinx](https://www.sphinx-doc Please note that there are separate references for different versions of NautilusTrader: -- **Latest**: This API reference is built from the head of the `master` branch and represents the latest stable release. -- **Nightly**: This API reference is built from the head of the `nightly` branch and represents bleeding edge and experimental changes/features currently in development. +- **Latest:** This API reference is built from the head of the `master` branch and represents the latest stable release. +- **Nightly:** This API reference is built from the head of the `nightly` branch and represents bleeding edge and experimental changes/features currently in development. You can select the desired API reference from the **Versions** top right drop down menu. diff --git a/docs/concepts/data.md b/docs/concepts/data.md index 9964a606c86..c53d751cde3 100644 --- a/docs/concepts/data.md +++ b/docs/concepts/data.md @@ -199,7 +199,7 @@ from nautilus_trader.model.data import OrderBookDelta data_config = BacktestDataConfig( catalog_path=str(catalog.path), data_cls=OrderBookDelta, - instrument_id=instrument.id.value, + instrument_id=instrument.id, start_time=start, end_time=end, ) diff --git a/docs/concepts/overview.md b/docs/concepts/overview.md index 080cd14b66c..5d163d66397 100644 --- a/docs/concepts/overview.md +++ b/docs/concepts/overview.md @@ -26,7 +26,7 @@ including FX, Equities, Futures, Options, CFDs, Crypto and Betting - across mult - **Backtesting:** Run with multiple venues, instruments and strategies simultaneously using historical quote tick, trade tick, bar, order book and custom data with nanosecond resolution - **Live:** Use identical strategy implementations between backtesting and live deployments - **Multi-venue:** Multiple venue capabilities facilitate market making and statistical arbitrage strategies -- **AI Agent Training:** Backtest engine fast enough to be used to train AI trading agents (RL/ES) +- **AI Training:** Backtest engine fast enough to be used to train AI trading agents (RL/ES) ![Nautilus](https://github.com/nautechsystems/nautilus_trader/blob/develop/docs/_images/nautilus-art.png?raw=true "nautilus") > *nautilus - from ancient Greek 'sailor' and naus 'ship'.* @@ -36,10 +36,10 @@ including FX, Equities, Futures, Options, CFDs, Crypto and Betting - across mult ## Why NautilusTrader? -- **Highly performant event-driven Python** - native binary core components -- **Parity between backtesting and live trading** - identical strategy code -- **Reduced operational risk** - risk management functionality, logical correctness and type safety -- **Highly extendable** - message bus, custom components and actors, custom data, custom adapters +- **Highly performant event-driven Python:** Native binary core components +- **Parity between backtesting and live trading:** Identical strategy code +- **Reduced operational risk:** Risk management functionality, logical correctness and type safety +- **Highly extendable:** Message bus, custom components and actors, custom data, custom adapters Traditionally, trading strategy research and backtesting might be conducted in Python (or other suitable language) using vectorized methods, with the strategy then needing to be reimplemented in a more event-drive way @@ -95,11 +95,13 @@ Python 3.11 offers improved run-time performance, while Python 3.12 additionally ``` ## Domain model + The platform features a comprehensive trading domain model that includes various value types such as `Price` and `Quantity`, as well as more complex entities such as `Order` and `Position` objects, which are used to aggregate multiple events to determine state. ### Data Types + The following market data types can be requested historically, and also subscribed to as live streams when available from a venue / data provider, and implemented in an integrations adapter. - `OrderBookDelta` (L1/L2/L3) - `OrderBookDeltas` (container type) @@ -140,6 +142,7 @@ The price types and bar aggregations can be combined with step sizes >= 1 in any This enables maximum flexibility and now allows alternative bars to be aggregated for live trading. ### Account Types + The following account types are available for both live and backtest environments; - `Cash` single-currency (base currency) @@ -149,6 +152,7 @@ The following account types are available for both live and backtest environment - `Betting` single-currency ### Order Types + The following order types are available (when possible on a venue); - `MARKET` @@ -160,4 +164,3 @@ The following order types are available (when possible on a venue); - `LIMIT_IF_TOUCHED` - `TRAILING_STOP_MARKET` - `TRAILING_STOP_LIMIT` - diff --git a/docs/developer_guide/adapters.md b/docs/developer_guide/adapters.md index 335ff2c9595..9b0f42cdb40 100644 --- a/docs/developer_guide/adapters.md +++ b/docs/developer_guide/adapters.md @@ -9,10 +9,10 @@ into a unified interface. ## Structure of an Adapter An adapter typically consists of several components: -1. **Instrument Provider**: Supplies instrument definitions -2. **Data Client**: Handles live market data feeds and historical data requests -3. **Execution Client**: Handles order execution and management -5. **Configuration**: Configures the client settings +1. **Instrument Provider:** Supplies instrument definitions +2. **Data Client:** Handles live market data feeds and historical data requests +3. **Execution Client:** Handles order execution and management +5. **Configuration:** Configures the client settings ## Steps to Implement a New Adapter diff --git a/docs/integrations/databento.md b/docs/integrations/databento.md index 103fb175f3d..49d00a787ff 100644 --- a/docs/integrations/databento.md +++ b/docs/integrations/databento.md @@ -161,7 +161,7 @@ The following Databento instrument classes are supported by NautilusTrader: | FX spot | `X` | `CurrencyPair` | | Bond | `B` | Not yet available | -### MBO (market by order) +### MBO (market-by-order) This schema is the highest granularity data offered by Databento, and represents full order book depth. Some messages also provide trade information, and so when @@ -175,7 +175,7 @@ registered handler. Order book snapshots are also buffered into a discrete `OrderBookDeltas` container object, which occurs during the replay startup sequence. -### MBP-1 (market by price, top-of-book) +### MBP-1 (market-by-price, top-of-book) 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 diff --git a/docs/integrations/ib.md b/docs/integrations/ib.md index 3a669a17fd5..aa34049b093 100644 --- a/docs/integrations/ib.md +++ b/docs/integrations/ib.md @@ -56,7 +56,7 @@ print(gateway.is_logged_in(gateway.container)) print(gateway.container.logs()) ``` -**Note**: To supply credentials to the Interactive Brokers Gateway, either pass the `username` and `password` to the config dictionaries, or set the following environment variables: +**Note:** To supply credentials to the Interactive Brokers Gateway, either pass the `username` and `password` to the config dictionaries, or set the following environment variables: - `TWS_USERNAME` - `TWS_PASSWORD` diff --git a/docs/tutorials/backtest_high_level.md b/docs/tutorials/backtest_high_level.md index 092cc2fa4c2..bd3db45b402 100644 --- a/docs/tutorials/backtest_high_level.md +++ b/docs/tutorials/backtest_high_level.md @@ -63,7 +63,7 @@ Then we can create Nautilus `QuoteTick` objects by processing the DataFrame with ```python # Here we just take the first data file found and load into a pandas DataFrame -df = CSVTickDataLoader.load(raw_files[0], index_col=0, format="%Y%m%d %H%M%S%f") +df = CSVTickDataLoader.load(raw_files[0], index_col=0, datetime_format="%Y%m%d %H%M%S%f") df.columns = ["bid_price", "ask_price"] # Process quote ticks using a wrangler @@ -141,7 +141,7 @@ data_configs = [ BacktestDataConfig( catalog_path=str(ParquetDataCatalog.from_env().path), data_cls=QuoteTick, - instrument_id=instrument.id.value, + instrument_id=instrument.id, start_time=start, end_time=end, ), @@ -152,7 +152,7 @@ strategies = [ strategy_path="nautilus_trader.examples.strategies.ema_cross:EMACross", config_path="nautilus_trader.examples.strategies.ema_cross:EMACrossConfig", config=dict( - instrument_id=instrument.id.value, + instrument_id=instrument.id, bar_type="EUR/USD.SIM-15-MINUTE-BID-INTERNAL", fast_ema_period=10, slow_ema_period=20, diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index c34eb49efeb..046c9fa9b4e 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -19,8 +19,8 @@ From basic tasks to more advanced operations, these tutorials cater to a wide ra ```{tip} Make sure you are following the tutorial docs which match the version of NautilusTrader you are running: -- **Latest** - These docs are built from the HEAD of the `master` branch and work with the latest stable release. -- **Develop** - These docs are built from the HEAD of the `develop` branch and work with bleeding edge and experimental changes/features currently in development. +- **Latest:** These docs are built from the HEAD of the `master` branch and work with the latest stable release. +- **Develop:** These docs are built from the HEAD of the `develop` branch and work with bleeding edge and experimental changes/features currently in development. ``` ## Backtesting diff --git a/examples/live/binance/binance_futures_testnet_market_maker.py b/examples/live/binance/binance_futures_testnet_market_maker.py index 0b422259cba..0621afedbd3 100644 --- a/examples/live/binance/binance_futures_testnet_market_maker.py +++ b/examples/live/binance/binance_futures_testnet_market_maker.py @@ -71,6 +71,7 @@ # snapshot_orders=True, # snapshot_positions=True, # snapshot_positions_interval=5.0, + # streaming=StreamingConfig(catalog_path="catalog"), data_clients={ "BINANCE": BinanceDataClientConfig( api_key=None, # 'BINANCE_API_KEY' env var diff --git a/examples/notebooks/backtest_fx_usdjpy.ipynb b/examples/notebooks/backtest_fx_usdjpy.ipynb index d2c3242d840..5b3d02704cc 100644 --- a/examples/notebooks/backtest_fx_usdjpy.ipynb +++ b/examples/notebooks/backtest_fx_usdjpy.ipynb @@ -96,8 +96,8 @@ " venue=SIM,\n", " oms_type=OmsType.HEDGING, # Venue will generate position IDs\n", " account_type=AccountType.MARGIN,\n", - " base_currency=None, # Standard single-currency account\n", - " starting_balances=[Money(1_000_000, USD), Money(10_000_000, JPY)], # Single-currency or multi-currency accounts\n", + " base_currency=None, # Multi-currency account\n", + " starting_balances=[Money(1_000_000, USD), Money(10_000_000, JPY)],\n", " fill_model=fill_model,\n", " modules=[fx_rollover_interest],\n", ")" diff --git a/examples/sandbox/betfair_sandbox.py b/examples/sandbox/betfair_sandbox.py index 3ac80c81169..99db32f10ab 100644 --- a/examples/sandbox/betfair_sandbox.py +++ b/examples/sandbox/betfair_sandbox.py @@ -24,7 +24,6 @@ from nautilus_trader.adapters.betfair.factories import get_cached_betfair_instrument_provider from nautilus_trader.adapters.betfair.providers import BetfairInstrumentProviderConfig from nautilus_trader.adapters.sandbox.config import SandboxExecutionClientConfig -from nautilus_trader.adapters.sandbox.execution import SandboxExecutionClient from nautilus_trader.adapters.sandbox.factory import SandboxLiveExecClientFactory from nautilus_trader.config import LoggingConfig from nautilus_trader.config import TradingNodeConfig @@ -55,9 +54,6 @@ async def main(instrument_config: BetfairInstrumentProviderConfig) -> TradingNod instruments = provider.list_all() print(f"Found instruments:\n{[ins.id for ins in instruments]}") - # Need to manually set instruments for sandbox exec client - SandboxExecutionClient.INSTRUMENTS = instruments - # Load account currency account_currency = await provider.get_account_currency() @@ -72,7 +68,7 @@ async def main(instrument_config: BetfairInstrumentProviderConfig) -> TradingNod ), }, exec_clients={ - "SANDBOX": SandboxExecutionClientConfig( + "BETFAIR": SandboxExecutionClientConfig( venue="BETFAIR", base_currency="AUD", starting_balances=["10_000 AUD"], @@ -92,11 +88,16 @@ async def main(instrument_config: BetfairInstrumentProviderConfig) -> TradingNod # Setup TradingNode node = TradingNode(config=config) + + # Can manually set instruments for sandbox exec client + for instrument in instruments: + node.cache.add_instrument(instrument) + node.trader.add_strategies(strategies) # Register your client factories with the node (can take user defined factories) node.add_data_client_factory("BETFAIR", BetfairLiveDataClientFactory) - node.add_exec_client_factory("SANDBOX", SandboxLiveExecClientFactory) + node.add_exec_client_factory("BETFAIR", SandboxLiveExecClientFactory) node.build() try: diff --git a/examples/sandbox/binance_futures_testnet_sandbox.py b/examples/sandbox/binance_futures_testnet_sandbox.py index 6e00bf3f896..13e3bfdd1af 100644 --- a/examples/sandbox/binance_futures_testnet_sandbox.py +++ b/examples/sandbox/binance_futures_testnet_sandbox.py @@ -17,16 +17,11 @@ import asyncio from decimal import Decimal -# fmt: off from nautilus_trader.adapters.binance.common.enums import BinanceAccountType from nautilus_trader.adapters.binance.config import BinanceDataClientConfig from nautilus_trader.adapters.binance.factories import BinanceLiveDataClientFactory -from nautilus_trader.adapters.binance.factories import get_cached_binance_futures_instrument_provider -from nautilus_trader.adapters.binance.factories import get_cached_binance_http_client from nautilus_trader.adapters.sandbox.config import SandboxExecutionClientConfig -from nautilus_trader.adapters.sandbox.execution import SandboxExecutionClient from nautilus_trader.adapters.sandbox.factory import SandboxLiveExecClientFactory -from nautilus_trader.common.component import LiveClock from nautilus_trader.config import CacheConfig from nautilus_trader.config import InstrumentProviderConfig from nautilus_trader.config import LiveExecEngineConfig @@ -40,9 +35,6 @@ from nautilus_trader.model.identifiers import TraderId -# fmt: on - - # *** THIS IS A TEST STRATEGY WITH NO ALPHA ADVANTAGE WHATSOEVER. *** # *** IT IS NOT INTENDED TO BE USED TO TRADE LIVE WITH REAL MONEY. *** @@ -51,27 +43,6 @@ async def main(): """ Show how to run a strategy in a sandbox for the Binance venue. """ - # Connect to Binance client early to load all instruments - clock = LiveClock() - account_type = BinanceAccountType.USDT_FUTURE - client = get_cached_binance_http_client( - clock=clock, - account_type=account_type, - is_testnet=True, - ) - - provider = get_cached_binance_futures_instrument_provider( - client=client, - clock=clock, - account_type=account_type, - config=InstrumentProviderConfig(), - ) - await provider.load_all_async() - instruments = provider.list_all() - - # Need to manually set instruments for sandbox exec client - SandboxExecutionClient.INSTRUMENTS = instruments - # Configure the trading node config_node = TradingNodeConfig( trader_id=TraderId("TESTER-001"), @@ -118,7 +89,7 @@ async def main(): ), }, exec_clients={ - "SANDBOX": SandboxExecutionClientConfig( + "BINANCE": SandboxExecutionClientConfig( venue="BINANCE", starting_balances=["10_000 USDT", "10 ETH"], ), @@ -151,7 +122,7 @@ async def main(): # Register your client factories with the node (can take user defined factories) node.add_data_client_factory("BINANCE", BinanceLiveDataClientFactory) - node.add_exec_client_factory("SANDBOX", SandboxLiveExecClientFactory) + node.add_exec_client_factory("BINANCE", SandboxLiveExecClientFactory) node.build() try: diff --git a/examples/sandbox/bybit_sandbox.py b/examples/sandbox/bybit_sandbox.py index aa3a40f9ebc..e3f89338df0 100644 --- a/examples/sandbox/bybit_sandbox.py +++ b/examples/sandbox/bybit_sandbox.py @@ -17,16 +17,11 @@ import asyncio from decimal import Decimal -from nautilus_trader.adapters.bybit.common.constants import BYBIT_ALL_PRODUCTS from nautilus_trader.adapters.bybit.common.enums import BybitProductType from nautilus_trader.adapters.bybit.config import BybitDataClientConfig from nautilus_trader.adapters.bybit.factories import BybitLiveDataClientFactory -from nautilus_trader.adapters.bybit.factories import get_bybit_http_client -from nautilus_trader.adapters.bybit.factories import get_bybit_instrument_provider from nautilus_trader.adapters.sandbox.config import SandboxExecutionClientConfig -from nautilus_trader.adapters.sandbox.execution import SandboxExecutionClient from nautilus_trader.adapters.sandbox.factory import SandboxLiveExecClientFactory -from nautilus_trader.common.component import LiveClock from nautilus_trader.config import InstrumentProviderConfig from nautilus_trader.config import LoggingConfig from nautilus_trader.config import TradingNodeConfig @@ -46,27 +41,10 @@ async def main(): """ Show how to run a strategy in a sandbox for the Bybit venue. """ - # Connect to Bybit client early to load all instruments - clock = LiveClock() - client = get_bybit_http_client(clock=clock) - - product_types = BYBIT_ALL_PRODUCTS instrument_provider_config = InstrumentProviderConfig(load_all=True) - provider = get_bybit_instrument_provider( - client=client, - clock=clock, - product_types=product_types, - config=instrument_provider_config, - ) - await provider.load_all_async() - - instruments = provider.list_all() - - # Need to manually set instruments for sandbox exec client - SandboxExecutionClient.INSTRUMENTS = instruments # Set up the execution clients (required per venue) - venues = {str(instrument.venue) for instrument in instruments} + venues = ["BYBIT"] exec_clients = {} for venue in venues: diff --git a/examples/sandbox/interactive_brokers_sandbox.py b/examples/sandbox/interactive_brokers_sandbox.py index 11d3591087c..2895783c1e0 100644 --- a/examples/sandbox/interactive_brokers_sandbox.py +++ b/examples/sandbox/interactive_brokers_sandbox.py @@ -22,7 +22,6 @@ from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersInstrumentProviderConfig from nautilus_trader.adapters.interactive_brokers.factories import InteractiveBrokersLiveDataClientFactory from nautilus_trader.adapters.sandbox.config import SandboxExecutionClientConfig -from nautilus_trader.adapters.sandbox.execution import SandboxExecutionClient from nautilus_trader.adapters.sandbox.factory import SandboxLiveExecClientFactory from nautilus_trader.config import LiveDataEngineConfig from nautilus_trader.config import LoggingConfig @@ -41,11 +40,6 @@ catalog = ParquetDataCatalog(CATALOG_PATH) SANDBOX_INSTRUMENTS = catalog.instruments(instrument_ids=["EUR/USD.IDEALPRO"]) -# Need to manually set instruments for sandbox exec client -SandboxExecutionClient.INSTRUMENTS = ( - SANDBOX_INSTRUMENTS # <- ALL INSTRUMENTS MUST HAVE THE SAME VENUE -) - # Set up the Interactive Brokers gateway configuration, this is applicable only when using Docker. gateway = InteractiveBrokersGatewayConfig( start=False, @@ -104,6 +98,10 @@ # Instantiate the node with a configuration node = TradingNode(config=config_node) +# Can manually set instruments for sandbox exec client +for instrument in SANDBOX_INSTRUMENTS: + node.cache.add_instrument(instrument) + # Instantiate strategies strategies = {} for instrument in SANDBOX_INSTRUMENTS: diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 79c5f5270da..61ef3ca14af 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -384,9 +384,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c90a406b4495d129f00461241616194cb8a032c8d1c53c657f0961d5f8e0498" +checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" dependencies = [ "bzip2", "flate2", @@ -500,9 +500,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" dependencies = [ "addr2line", "cc", @@ -1637,12 +1637,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" -[[package]] -name = "finl_unicode" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" - [[package]] name = "fixedbitset" version = "0.4.2" @@ -1854,9 +1848,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "glob" @@ -2076,9 +2070,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" dependencies = [ "bytes", "futures-channel", @@ -2475,11 +2469,10 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -2493,7 +2486,7 @@ dependencies = [ [[package]] name = "nautilus-accounting" -version = "0.23.0" +version = "0.24.0" dependencies = [ "anyhow", "cbindgen", @@ -2509,7 +2502,7 @@ dependencies = [ [[package]] name = "nautilus-adapters" -version = "0.23.0" +version = "0.24.0" dependencies = [ "anyhow", "chrono", @@ -2540,7 +2533,7 @@ dependencies = [ [[package]] name = "nautilus-backtest" -version = "0.23.0" +version = "0.24.0" dependencies = [ "anyhow", "cbindgen", @@ -2557,7 +2550,7 @@ dependencies = [ [[package]] name = "nautilus-cli" -version = "0.23.0" +version = "0.24.0" dependencies = [ "anyhow", "clap 4.5.4", @@ -2574,7 +2567,7 @@ dependencies = [ [[package]] name = "nautilus-common" -version = "0.23.0" +version = "0.24.0" dependencies = [ "anyhow", "cbindgen", @@ -2602,7 +2595,7 @@ dependencies = [ [[package]] name = "nautilus-core" -version = "0.23.0" +version = "0.24.0" dependencies = [ "anyhow", "cbindgen", @@ -2622,7 +2615,7 @@ dependencies = [ [[package]] name = "nautilus-execution" -version = "0.23.0" +version = "0.24.0" dependencies = [ "anyhow", "criterion", @@ -2647,7 +2640,7 @@ dependencies = [ [[package]] name = "nautilus-indicators" -version = "0.23.0" +version = "0.24.0" dependencies = [ "anyhow", "nautilus-core", @@ -2659,7 +2652,7 @@ dependencies = [ [[package]] name = "nautilus-infrastructure" -version = "0.23.0" +version = "0.24.0" dependencies = [ "anyhow", "log", @@ -2682,7 +2675,7 @@ dependencies = [ [[package]] name = "nautilus-model" -version = "0.23.0" +version = "0.24.0" dependencies = [ "anyhow", "cbindgen", @@ -2710,7 +2703,7 @@ dependencies = [ [[package]] name = "nautilus-network" -version = "0.23.0" +version = "0.24.0" dependencies = [ "anyhow", "axum", @@ -2735,7 +2728,7 @@ dependencies = [ [[package]] name = "nautilus-persistence" -version = "0.23.0" +version = "0.24.0" dependencies = [ "anyhow", "binary-heap-plus", @@ -2758,7 +2751,7 @@ dependencies = [ [[package]] name = "nautilus-pyo3" -version = "0.23.0" +version = "0.24.0" dependencies = [ "nautilus-accounting", "nautilus-adapters", @@ -2946,9 +2939,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" dependencies = [ "memchr", ] @@ -3074,9 +3067,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -3358,9 +3351,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.83" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" +checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" dependencies = [ "unicode-ident", ] @@ -3589,9 +3582,9 @@ dependencies = [ [[package]] name = "redis" -version = "0.25.3" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6472825949c09872e8f2c50bde59fcefc17748b6be5c90fd67cd8b4daca73bfd" +checksum = "e0d7a6955c7511f60f3ba9e86c6d02b3c3f144f8c24b288d1f4e18074ab8bbec" dependencies = [ "arc-swap", "async-trait", @@ -4055,18 +4048,18 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -4523,13 +4516,13 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "stringprep" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "finl_unicode", "unicode-bidi", "unicode-normalization", + "unicode-properties", ] [[package]] @@ -4833,9 +4826,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", @@ -4852,9 +4845,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", @@ -5158,6 +5151,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" + [[package]] name = "unicode-segmentation" version = "1.11.0" @@ -5656,9 +5655,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zstd" diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 234c015038e..a43a67496d7 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -19,7 +19,7 @@ members = [ [workspace.package] rust-version = "1.78.0" -version = "0.23.0" +version = "0.24.0" edition = "2021" authors = ["Nautech Systems "] description = "A high-performance algorithmic trading platform and event-driven backtester" @@ -42,13 +42,13 @@ rmp-serde = "1.3.0" rust_decimal = "1.35.0" rust_decimal_macros = "1.34.2" semver = "1.0.23" -serde = { version = "1.0.202", features = ["derive"] } +serde = { version = "1.0.203", features = ["derive"] } serde_json = "1.0.117" strum = { version = "0.26.2", features = ["derive"] } thiserror = "1.0.61" thousands = "0.2.0" tracing = "0.1.40" -tokio = { version = "1.37.0", features = ["full"] } +tokio = { version = "1.38.0", features = ["full"] } ustr = { version = "1.0.0", features = ["serde"] } uuid = { version = "1.8.0", features = ["v4"] } diff --git a/nautilus_core/accounting/src/account/base.rs b/nautilus_core/accounting/src/account/base.rs index bbb74bf3996..b7a52d2e4e2 100644 --- a/nautilus_core/accounting/src/account/base.rs +++ b/nautilus_core/accounting/src/account/base.rs @@ -44,6 +44,7 @@ pub struct BaseAccount { } impl BaseAccount { + /// Creates a new [`BaseAccount`] instance. pub fn new(event: AccountState, calculate_account_state: bool) -> anyhow::Result { let mut balances_starting: HashMap = HashMap::new(); let mut balances: HashMap = HashMap::new(); diff --git a/nautilus_core/accounting/src/account/cash.rs b/nautilus_core/accounting/src/account/cash.rs index 7a7acb51f14..0fc53797b12 100644 --- a/nautilus_core/accounting/src/account/cash.rs +++ b/nautilus_core/accounting/src/account/cash.rs @@ -43,6 +43,7 @@ pub struct CashAccount { } impl CashAccount { + /// Creates a new [`CashAccount`] instance. pub fn new(event: AccountState, calculate_account_state: bool) -> anyhow::Result { Ok(Self { base: BaseAccount::new(event, calculate_account_state)?, diff --git a/nautilus_core/accounting/src/account/margin.rs b/nautilus_core/accounting/src/account/margin.rs index 4c016b43dc9..cbad02117d0 100644 --- a/nautilus_core/accounting/src/account/margin.rs +++ b/nautilus_core/accounting/src/account/margin.rs @@ -54,6 +54,7 @@ pub struct MarginAccount { } impl MarginAccount { + /// Creates a new [`MarginAccount`] instance. pub fn new(event: AccountState, calculate_account_state: bool) -> anyhow::Result { Ok(Self { base: BaseAccount::new(event, calculate_account_state)?, diff --git a/nautilus_core/accounting/src/account/mod.rs b/nautilus_core/accounting/src/account/mod.rs index 3aee414fe5f..62b28ed7a53 100644 --- a/nautilus_core/accounting/src/account/mod.rs +++ b/nautilus_core/accounting/src/account/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides account types and accounting functionality. +//! Account implementations and accounting functionality. pub mod base; pub mod cash; diff --git a/nautilus_core/adapters/src/databento/enums.rs b/nautilus_core/adapters/src/databento/enums.rs index 0e560533f2c..5cc16dc275e 100644 --- a/nautilus_core/adapters/src/databento/enums.rs +++ b/nautilus_core/adapters/src/databento/enums.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines enumerations for the Databento integration. +//! Enumerations for the Databento integration. use std::str::FromStr; diff --git a/nautilus_core/adapters/src/databento/live.rs b/nautilus_core/adapters/src/databento/live.rs index b2d6945e4b4..da2700b4e09 100644 --- a/nautilus_core/adapters/src/databento/live.rs +++ b/nautilus_core/adapters/src/databento/live.rs @@ -83,7 +83,7 @@ pub struct DatabentoFeedHandler { } impl DatabentoFeedHandler { - /// Initialize a new instance of the [`DatabentoFeedHandler`]. + /// Creates a new [`DatabentoFeedHandler`] instance. #[must_use] pub fn new( key: String, @@ -265,7 +265,7 @@ impl DatabentoFeedHandler { msg.flags.raw(), ); - // Check if last message in the packet + // Check if last message in the book event if !RecordFlag::F_LAST.matches(msg.flags.raw()) { continue; // NOT last message } diff --git a/nautilus_core/adapters/src/databento/loader.rs b/nautilus_core/adapters/src/databento/loader.rs index 9cc95fde38b..d4a95ea0dab 100644 --- a/nautilus_core/adapters/src/databento/loader.rs +++ b/nautilus_core/adapters/src/databento/loader.rs @@ -39,7 +39,7 @@ use super::{ types::{DatabentoImbalance, DatabentoPublisher, DatabentoStatistics, Dataset, PublisherId}, }; -/// Provides a Nautilus data loader for Databento Binary Encoding (DBN) format data. +/// A Nautilus data loader for Databento Binary Encoding (DBN) format data. /// /// # Supported schemas: /// - MBO -> `OrderBookDelta` @@ -68,6 +68,7 @@ pub struct DatabentoDataLoader { } impl DatabentoDataLoader { + /// Creates a new [`DatabentoDataLoader`] instance. pub fn new(path: Option) -> anyhow::Result { let mut loader = Self { publishers_map: IndexMap::new(), diff --git a/nautilus_core/adapters/src/databento/mod.rs b/nautilus_core/adapters/src/databento/mod.rs index 70880f3c558..bf8a79cb140 100644 --- a/nautilus_core/adapters/src/databento/mod.rs +++ b/nautilus_core/adapters/src/databento/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides the [Databento](https://databento.com) integration adapter. +//! The [Databento](https://databento.com) integration adapter. pub mod common; pub mod decode; diff --git a/nautilus_core/adapters/src/databento/types.rs b/nautilus_core/adapters/src/databento/types.rs index f7832a0e377..da3ed8aca2b 100644 --- a/nautilus_core/adapters/src/databento/types.rs +++ b/nautilus_core/adapters/src/databento/types.rs @@ -85,6 +85,7 @@ pub struct DatabentoImbalance { } impl DatabentoImbalance { + /// Creates a new [`DatabentoImbalance`] instance. #[allow(clippy::too_many_arguments)] pub fn new( instrument_id: InstrumentId, @@ -154,6 +155,7 @@ pub struct DatabentoStatistics { } impl DatabentoStatistics { + /// Creates a new [`DatabentoStatistics`] instance. #[allow(clippy::too_many_arguments)] pub fn new( instrument_id: InstrumentId, diff --git a/nautilus_core/backtest/src/engine.rs b/nautilus_core/backtest/src/engine.rs index 924de33c7b8..669259b12b3 100644 --- a/nautilus_core/backtest/src/engine.rs +++ b/nautilus_core/backtest/src/engine.rs @@ -29,7 +29,7 @@ pub struct TimeEventAccumulator { } impl TimeEventAccumulator { - /// Creates a new `TimeEventAccumulator` instance. + /// Creates a new [`TimeEventAccumulator`] instance. #[must_use] pub fn new() -> Self { Self { @@ -55,6 +55,7 @@ impl TimeEventAccumulator { } impl Default for TimeEventAccumulator { + /// Creates a new default [`TimeEventAccumulator`] instance. fn default() -> Self { Self::new() } diff --git a/nautilus_core/backtest/src/matching_engine.rs b/nautilus_core/backtest/src/matching_engine.rs index 744a7ab800f..10da49cc3bb 100644 --- a/nautilus_core/backtest/src/matching_engine.rs +++ b/nautilus_core/backtest/src/matching_engine.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides an `OrderMatchingEngine` for use in research, backtesting and sandbox environments. +//! An `OrderMatchingEngine` for use in research, backtesting and sandbox environments. // Under development #![allow(dead_code)] @@ -55,7 +55,7 @@ pub struct OrderMatchingEngineConfig { pub use_reduce_only: bool, } -/// Provides an order matching engine for a single market. +/// An order matching engine for a single market. pub struct OrderMatchingEngine { /// The venue for the matching engine. pub venue: Venue, @@ -91,8 +91,9 @@ pub struct OrderMatchingEngine { execution_count: usize, } -// Note: we'll probably be changing the `FillModel` (don't add for now) +// TODO: we'll probably be changing the `FillModel` (don't add for now) impl OrderMatchingEngine { + /// Creates a new [`OrderMatchingEngine`] instance. #[allow(clippy::too_many_arguments)] pub fn new( instrument: Box, diff --git a/nautilus_core/common/src/cache/core.rs b/nautilus_core/common/src/cache/core.rs index 143995e2570..015962e38d8 100644 --- a/nautilus_core/common/src/cache/core.rs +++ b/nautilus_core/common/src/cache/core.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! The core cache in-memory structure. + use std::{ collections::{HashMap, HashSet, VecDeque}, time::{SystemTime, UNIX_EPOCH}, @@ -49,7 +51,7 @@ use ustr::Ustr; use super::database::CacheDatabaseAdapter; use crate::{enums::SerializationEncoding, interface::account::Account}; -/// The configuration for `Cache` instances. +/// Configuration for `Cache` instances. pub struct CacheConfig { pub encoding: SerializationEncoding, pub timestamps_as_iso8601: bool, @@ -62,6 +64,7 @@ pub struct CacheConfig { } impl CacheConfig { + /// Creates a new [`CacheConfig`] instance. #[allow(clippy::too_many_arguments)] #[must_use] pub fn new( @@ -88,6 +91,7 @@ impl CacheConfig { } impl Default for CacheConfig { + /// Creates a new default [`CacheConfig`] instance. fn default() -> Self { Self::new( SerializationEncoding::MsgPack, @@ -135,7 +139,7 @@ pub struct CacheIndex { } impl CacheIndex { - /// Clear the index which will clear/reset all internal state. + /// Clears the index which will clear/reset all internal state. pub fn clear(&mut self) { self.venue_account.clear(); self.venue_orders.clear(); @@ -172,7 +176,7 @@ impl CacheIndex { pub struct Cache { config: CacheConfig, index: CacheIndex, - database: Option, + database: Option>, general: HashMap>, quotes: HashMap>, trades: HashMap>, @@ -189,14 +193,16 @@ pub struct Cache { } impl Default for Cache { + /// Creates a new default [`Cache`] instance. fn default() -> Self { Self::new(CacheConfig::default(), None) } } impl Cache { + /// Creates a new [`Cache`] instance. #[must_use] - pub fn new(config: CacheConfig, database: Option) -> Self { + pub fn new(config: CacheConfig, database: Option>) -> Self { let index = CacheIndex { venue_account: HashMap::new(), venue_orders: HashMap::new(), @@ -250,9 +256,9 @@ impl Cache { // -- COMMANDS -------------------------------------------------------------------------------- - /// Clear the current general cache and load the general objects from the cache database. + /// Clears the current general cache and loads the general objects from the cache database. pub fn cache_general(&mut self) -> anyhow::Result<()> { - self.general = match &self.database { + self.general = match &mut self.database { Some(db) => db.load()?, None => HashMap::new(), }; @@ -264,9 +270,9 @@ impl Cache { Ok(()) } - /// Clear the current currencies cache and load currencies from the cache database. + /// Clears the current currencies cache and loads currencies from the cache database. pub fn cache_currencies(&mut self) -> anyhow::Result<()> { - self.currencies = match &self.database { + self.currencies = match &mut self.database { Some(db) => db.load_currencies()?, None => HashMap::new(), }; @@ -275,9 +281,9 @@ impl Cache { Ok(()) } - /// Clear the current instruments cache and load instruments from the cache database. + /// Clears the current instruments cache and loads instruments from the cache database. pub fn cache_instruments(&mut self) -> anyhow::Result<()> { - self.instruments = match &self.database { + self.instruments = match &mut self.database { Some(db) => db.load_instruments()?, None => HashMap::new(), }; @@ -286,10 +292,10 @@ impl Cache { Ok(()) } - /// Clear the current synthetic instruments cache and load synthetic instruments from the cache + /// Clears the current synthetic instruments cache and loads synthetic instruments from the cache /// database. pub fn cache_synthetics(&mut self) -> anyhow::Result<()> { - self.synthetics = match &self.database { + self.synthetics = match &mut self.database { Some(db) => db.load_synthetics()?, None => HashMap::new(), }; @@ -301,9 +307,9 @@ impl Cache { Ok(()) } - /// Clear the current accounts cache and load accounts from the cache database. + /// Clears the current accounts cache and loads accounts from the cache database. pub fn cache_accounts(&mut self) -> anyhow::Result<()> { - self.accounts = match &self.database { + self.accounts = match &mut self.database { Some(db) => db.load_accounts()?, None => HashMap::new(), }; @@ -315,9 +321,9 @@ impl Cache { Ok(()) } - /// Clear the current orders cache and load orders from the cache database. + /// Clears the current orders cache and loads orders from the cache database. pub fn cache_orders(&mut self) -> anyhow::Result<()> { - self.orders = match &self.database { + self.orders = match &mut self.database { Some(db) => db.load_orders()?, None => HashMap::new(), }; @@ -326,9 +332,9 @@ impl Cache { Ok(()) } - /// Clear the current positions cache and load positions from the cache database. + /// Clears the current positions cache and loads positions from the cache database. pub fn cache_positions(&mut self) -> anyhow::Result<()> { - self.positions = match &self.database { + self.positions = match &mut self.database { Some(db) => db.load_positions()?, None => HashMap::new(), }; @@ -337,7 +343,7 @@ impl Cache { Ok(()) } - /// Clear the current cache index and re-build. + /// Clears the current cache index and re-build. pub fn build_index(&mut self) { self.index.clear(); debug!("Building index"); @@ -504,7 +510,7 @@ impl Cache { } } - /// Check integrity of data within the cache. + /// Checks integrity of data within the cache. /// /// All data should be loaded from the database prior to this call. /// If an error is found then a log error message will also be produced. @@ -877,7 +883,7 @@ impl Cache { } } - /// Check for any residual open state and log warnings if any are found. + /// Checks for any residual open state and log warnings if any are found. /// ///'Open state' is considered to be open orders and open positions. #[must_use] @@ -901,13 +907,13 @@ impl Cache { residuals } - /// Clear the caches index. + /// Clears the caches index. pub fn clear_index(&mut self) { self.index.clear(); debug!("Cleared index"); } - /// Reset the cache. + /// Resets the cache. /// /// All stateful fields are reset to their initial value. pub fn reset(&mut self) { @@ -932,27 +938,27 @@ impl Cache { } /// Dispose of the cache which will close any underlying database adapter. - pub fn dispose(&self) -> anyhow::Result<()> { - if let Some(database) = &self.database { + pub fn dispose(&mut self) -> anyhow::Result<()> { + if let Some(database) = &mut self.database { // TODO: Log operations in database adapter database.close()?; } Ok(()) } - /// Flush the caches database which permanently removes all persisted data. - pub fn flush_db(&self) -> anyhow::Result<()> { - if let Some(database) = &self.database { + /// Flushes the caches database which permanently removes all persisted data. + pub fn flush_db(&mut self) -> anyhow::Result<()> { + if let Some(database) = &mut self.database { // TODO: Log operations in database adapter database.flush()?; } Ok(()) } - /// Add the given general object to the cache. + /// Adds a general object `value` (as bytes) to the cache at the given `key`. /// - /// The cache is agnostic to what the object actually is (and how it may be serialized), - /// offering maximum flexibility. + /// The cache is agnostic to what the bytes actually represent (and how it may be serialized), + /// which provides maximum flexibility. pub fn add(&mut self, key: &str, value: Vec) -> anyhow::Result<()> { check_valid_string(key, stringify!(key))?; check_slice_not_empty(value.as_slice(), stringify!(value))?; @@ -960,20 +966,20 @@ impl Cache { debug!("Adding general {key}"); self.general.insert(key.to_string(), value.clone()); - if let Some(database) = &self.database { + if let Some(database) = &mut self.database { database.add(key.to_string(), value)?; } Ok(()) } - /// Add the given order `book` to the cache. + /// Adds the given order `book` to the cache. pub fn add_order_book(&mut self, book: OrderBook) -> anyhow::Result<()> { debug!("Adding `OrderBook` {}", book.instrument_id); self.books.insert(book.instrument_id, book); Ok(()) } - /// Add the given `quote` tick to the cache. + /// Adds the given `quote` tick to the cache. pub fn add_quote(&mut self, quote: QuoteTick) -> anyhow::Result<()> { debug!("Adding `QuoteTick` {}", quote.instrument_id); let quotes_deque = self @@ -984,7 +990,7 @@ impl Cache { Ok(()) } - /// Add the given `quotes` to the cache. + /// Adds the given `quotes` to the cache. pub fn add_quotes(&mut self, quotes: &[QuoteTick]) -> anyhow::Result<()> { check_slice_not_empty(quotes, stringify!(quotes))?; @@ -1001,7 +1007,7 @@ impl Cache { Ok(()) } - /// Add the given `trade` tick to the cache. + /// Adds the given `trade` tick to the cache. pub fn add_trade(&mut self, trade: TradeTick) -> anyhow::Result<()> { debug!("Adding `TradeTick` {}", trade.instrument_id); let trades_deque = self @@ -1012,7 +1018,7 @@ impl Cache { Ok(()) } - /// Add the give `trades` to the cache. + /// Adds the give `trades` to the cache. pub fn add_trades(&mut self, trades: &[TradeTick]) -> anyhow::Result<()> { check_slice_not_empty(trades, stringify!(trades))?; @@ -1029,7 +1035,7 @@ impl Cache { Ok(()) } - /// Add the given `bar` to the cache. + /// Adds the given `bar` to the cache. pub fn add_bar(&mut self, bar: Bar) -> anyhow::Result<()> { debug!("Adding `Bar` {}", bar.bar_type); let bars = self @@ -1040,7 +1046,7 @@ impl Cache { Ok(()) } - /// Add the given `bars` to the cache. + /// Adds the given `bars` to the cache. pub fn add_bars(&mut self, bars: &[Bar]) -> anyhow::Result<()> { check_slice_not_empty(bars, stringify!(bars))?; @@ -1057,11 +1063,11 @@ impl Cache { Ok(()) } - /// Add the given `currency` to the cache. + /// Adds the given `currency` to the cache. pub fn add_currency(&mut self, currency: Currency) -> anyhow::Result<()> { debug!("Adding `Currency` {}", currency.code); - if let Some(database) = &self.database { + if let Some(database) = &mut self.database { database.add_currency(¤cy)?; } @@ -1069,11 +1075,11 @@ impl Cache { Ok(()) } - /// Add the given `instrument` to the cache. + /// Adds the given `instrument` to the cache. pub fn add_instrument(&mut self, instrument: InstrumentAny) -> anyhow::Result<()> { debug!("Adding `Instrument` {}", instrument.id()); - if let Some(database) = &self.database { + if let Some(database) = &mut self.database { database.add_instrument(&instrument)?; } @@ -1081,11 +1087,11 @@ impl Cache { Ok(()) } - /// Add the given `synthetic` instrument to the cache. + /// Adds the given `synthetic` instrument to the cache. pub fn add_synthetic(&mut self, synthetic: SyntheticInstrument) -> anyhow::Result<()> { debug!("Adding `SyntheticInstrument` {}", synthetic.id); - if let Some(database) = &self.database { + if let Some(database) = &mut self.database { database.add_synthetic(&synthetic)?; } @@ -1093,11 +1099,11 @@ impl Cache { Ok(()) } - /// Add the given `account` to the cache. + /// Adds the given `account` to the cache. pub fn add_account(&mut self, account: Box) -> anyhow::Result<()> { debug!("Adding `Account` {}", account.id()); - if let Some(database) = &self.database { + if let Some(database) = &mut self.database { database.add_account(account.as_ref())?; } @@ -1105,7 +1111,9 @@ impl Cache { Ok(()) } - /// Index the given client order ID with the given venue order ID. + /// Indexes the given `client_order_id` with the given `venue_order_id`. + /// + /// The `overwrite` parameter determines whether to overwrite any existing cached identifier. pub fn add_venue_order_id( &mut self, client_order_id: &ClientOrderId, @@ -1133,7 +1141,7 @@ impl Cache { Ok(()) } - /// Add the order to the cache indexed with any given identifiers. + /// Adds the given `order` to the cache indexed with any given identifiers. /// /// # Parameters /// @@ -1271,7 +1279,7 @@ impl Cache { Ok(()) } - /// Index the given `position_id` with the other given IDs. + /// Indexes the given `position_id` with the other given IDs. pub fn add_position_id( &mut self, position_id: &PositionId, @@ -1310,6 +1318,7 @@ impl Cache { Ok(()) } + /// Adds the given `position` to the cache. pub fn add_position(&mut self, position: Position, oms_type: OmsType) -> anyhow::Result<()> { self.positions.insert(position.id, position.clone()); self.index.positions.insert(position.id); @@ -1352,7 +1361,7 @@ impl Cache { Ok(()) } - /// Update the given `account` in the cache. + /// Updates the given `account` in the cache. pub fn update_account(&mut self, account: &dyn Account) -> anyhow::Result<()> { if let Some(database) = &mut self.database { database.update_account(account)?; @@ -1360,7 +1369,7 @@ impl Cache { Ok(()) } - /// Update the given `order` in the cache. + /// Updates the given `order` in the cache. pub fn update_order(&mut self, order: &OrderAny) -> anyhow::Result<()> { let client_order_id = order.client_order_id(); @@ -1410,14 +1419,14 @@ impl Cache { Ok(()) } - /// Update the given `order` as pending cancel locally. + /// Updates the given `order` as pending cancel locally. pub fn update_order_pending_cancel_local(&mut self, order: &OrderAny) { self.index .orders_pending_cancel .insert(order.client_order_id()); } - /// Update the given `position` in the cache. + /// Updates the given `position` in the cache. pub fn update_position(&mut self, position: &Position) -> anyhow::Result<()> { // Update open/closed state if position.is_open() { @@ -1594,6 +1603,7 @@ impl Cache { positions } + /// Returns the `ClientOrderId`s of all orders. #[must_use] pub fn client_order_ids( &self, @@ -1608,6 +1618,7 @@ impl Cache { } } + /// Returns the `ClientOrderId`s of all open orders. #[must_use] pub fn client_order_ids_open( &self, @@ -1627,6 +1638,7 @@ impl Cache { } } + /// Returns the `ClientOrderId`s of all closed orders. #[must_use] pub fn client_order_ids_closed( &self, @@ -1646,6 +1658,7 @@ impl Cache { } } + /// Returns the `ClientOrderId`s of all emulated orders. #[must_use] pub fn client_order_ids_emulated( &self, @@ -1665,6 +1678,7 @@ impl Cache { } } + /// Returns the `ClientOrderId`s of all in-flight orders. #[must_use] pub fn client_order_ids_inflight( &self, @@ -1684,6 +1698,7 @@ impl Cache { } } + /// Returns `PositionId`s of all positions. #[must_use] pub fn position_ids( &self, @@ -1698,6 +1713,7 @@ impl Cache { } } + /// Returns the `PositionId`s of all open positions. #[must_use] pub fn position_open_ids( &self, @@ -1717,6 +1733,7 @@ impl Cache { } } + /// Returns the `PositionId`s of all closed positions. #[must_use] pub fn position_closed_ids( &self, @@ -1736,16 +1753,19 @@ impl Cache { } } + /// Returns the `ComponentId`s of all actors. #[must_use] pub fn actor_ids(&self) -> HashSet { self.index.actors.clone() } + /// Returns the `StrategyId`s of all strategies. #[must_use] pub fn strategy_ids(&self) -> HashSet { self.index.strategies.clone() } + /// Returns the `ExecAlgorithmId`s of all execution algorithms. #[must_use] pub fn exec_algorithm_ids(&self) -> HashSet { self.index.exec_algorithms.clone() @@ -1753,26 +1773,31 @@ impl Cache { // -- ORDER QUERIES --------------------------------------------------------------------------- + /// Gets a reference to the order with the given `client_order_id` (if found). #[must_use] pub fn order(&self, client_order_id: &ClientOrderId) -> Option<&OrderAny> { self.orders.get(client_order_id) } + /// Gets a reference to the client order ID for given `venue_order_id` (if found). #[must_use] pub fn client_order_id(&self, venue_order_id: &VenueOrderId) -> Option<&ClientOrderId> { self.index.venue_order_ids.get(venue_order_id) } + /// Gets a reference to the venue order ID for given `client_order_id` (if found). #[must_use] pub fn venue_order_id(&self, client_order_id: &ClientOrderId) -> Option<&VenueOrderId> { self.index.client_order_ids.get(client_order_id) } + /// Gets a reference to the client ID indexed for given `client_order_id` (if found). #[must_use] pub fn client_id(&self, client_order_id: &ClientOrderId) -> Option<&ClientId> { self.index.order_client.get(client_order_id) } + /// Returns references to all orders matching the given optional filter parameters. #[must_use] pub fn orders( &self, @@ -1785,6 +1810,7 @@ impl Cache { self.get_orders_for_ids(&client_order_ids, side) } + /// Returns references to all open orders matching the given optional filter parameters. #[must_use] pub fn orders_open( &self, @@ -1797,6 +1823,7 @@ impl Cache { self.get_orders_for_ids(&client_order_ids, side) } + /// Returns references to all closed orders matching the given optional filter parameters. #[must_use] pub fn orders_closed( &self, @@ -1809,6 +1836,7 @@ impl Cache { self.get_orders_for_ids(&client_order_ids, side) } + /// Returns references to all emulated orders matching the given optional filter parameters. #[must_use] pub fn orders_emulated( &self, @@ -1821,6 +1849,7 @@ impl Cache { self.get_orders_for_ids(&client_order_ids, side) } + /// Returns references to all in-flight orders matching the given optional filter parameters. #[must_use] pub fn orders_inflight( &self, @@ -1833,6 +1862,7 @@ impl Cache { self.get_orders_for_ids(&client_order_ids, side) } + /// Returns references to all orders for the given `position_id`. #[must_use] pub fn orders_for_position(&self, position_id: &PositionId) -> Vec<&OrderAny> { let client_order_ids = self.index.position_orders.get(position_id); @@ -1844,36 +1874,43 @@ impl Cache { } } + /// Returns whether an order with the given `client_order_id` exists. #[must_use] pub fn order_exists(&self, client_order_id: &ClientOrderId) -> bool { self.index.orders.contains(client_order_id) } + /// Returns whether an order with the given `client_order_id` is open. #[must_use] pub fn is_order_open(&self, client_order_id: &ClientOrderId) -> bool { self.index.orders_open.contains(client_order_id) } + /// Returns whether an order with the given `client_order_id` is closed. #[must_use] pub fn is_order_closed(&self, client_order_id: &ClientOrderId) -> bool { self.index.orders_closed.contains(client_order_id) } + /// Returns whether an order with the given `client_order_id` is emulated. #[must_use] pub fn is_order_emulated(&self, client_order_id: &ClientOrderId) -> bool { self.index.orders_emulated.contains(client_order_id) } + /// Returns whether an order with the given `client_order_id` is in-flight. #[must_use] pub fn is_order_inflight(&self, client_order_id: &ClientOrderId) -> bool { self.index.orders_inflight.contains(client_order_id) } + /// Returns whether an order with the given `client_order_id` is `PENDING_CANCEL` locally. #[must_use] pub fn is_order_pending_cancel_local(&self, client_order_id: &ClientOrderId) -> bool { self.index.orders_pending_cancel.contains(client_order_id) } + /// Returns the count of all open orders. #[must_use] pub fn orders_open_count( &self, @@ -1886,6 +1923,7 @@ impl Cache { .len() } + /// Returns the count of all closed orders. #[must_use] pub fn orders_closed_count( &self, @@ -1898,6 +1936,7 @@ impl Cache { .len() } + /// Returns the count of all emulated orders. #[must_use] pub fn orders_emulated_count( &self, @@ -1910,6 +1949,7 @@ impl Cache { .len() } + /// Returns the count of all in-flight orders. #[must_use] pub fn orders_inflight_count( &self, @@ -1922,6 +1962,7 @@ impl Cache { .len() } + /// Returns the count of all orders. #[must_use] pub fn orders_total_count( &self, @@ -1933,11 +1974,13 @@ impl Cache { self.orders(venue, instrument_id, strategy_id, side).len() } + /// Returns the order list for the given `order_list_id`. #[must_use] pub fn order_list(&self, order_list_id: &OrderListId) -> Option<&OrderList> { self.order_lists.get(order_list_id) } + /// Returns all order lists matching the given optional filter parameters. #[must_use] pub fn order_lists( &self, @@ -1962,6 +2005,7 @@ impl Cache { order_lists } + /// Returns whether an order list with the given `order_list_id` exists. #[must_use] pub fn order_list_exists(&self, order_list_id: &OrderListId) -> bool { self.order_lists.contains_key(order_list_id) @@ -1969,6 +2013,8 @@ impl Cache { // -- EXEC ALGORITHM QUERIES ------------------------------------------------------------------ + /// Returns references to all orders associated with the given `exec_algorithm_id` matching the given + /// optional filter parameters. #[must_use] pub fn orders_for_exec_algorithm( &self, @@ -1994,6 +2040,7 @@ impl Cache { } } + /// Returns references to all orders with the given `exec_spawn_id`. #[must_use] pub fn orders_for_exec_spawn(&self, exec_spawn_id: &ClientOrderId) -> Vec<&OrderAny> { self.get_orders_for_ids( @@ -2005,6 +2052,7 @@ impl Cache { ) } + /// Returns the total order quantity for the given `exec_spawn_id`. #[must_use] pub fn exec_spawn_total_quantity( &self, @@ -2028,6 +2076,7 @@ impl Cache { total_quantity } + /// Returns the total filled quantity for all orders with the given `exec_spawn_id`. #[must_use] pub fn exec_spawn_total_filled_qty( &self, @@ -2051,6 +2100,7 @@ impl Cache { total_quantity } + /// Returns the total leaves quantity for all orders with the given `exec_spawn_id`. #[must_use] pub fn exec_spawn_total_leaves_qty( &self, @@ -2076,11 +2126,13 @@ impl Cache { // -- POSITION QUERIES ------------------------------------------------------------------------ + /// Returns a reference to the position with the given `position_id` (if found). #[must_use] pub fn position(&self, position_id: &PositionId) -> Option<&Position> { self.positions.get(position_id) } + /// Returns a reference to the position for the given `client_order_id` (if found). #[must_use] pub fn position_for_order(&self, client_order_id: &ClientOrderId) -> Option<&Position> { self.index @@ -2089,11 +2141,13 @@ impl Cache { .and_then(|position_id| self.positions.get(position_id)) } + /// Returns a reference to the position ID for the given `client_order_id` (if found). #[must_use] pub fn position_id(&self, client_order_id: &ClientOrderId) -> Option<&PositionId> { self.index.order_position.get(client_order_id) } + /// Returns a reference to all positions matching the given optional filter parameters. #[must_use] pub fn positions( &self, @@ -2106,6 +2160,7 @@ impl Cache { self.get_positions_for_ids(&position_ids, side) } + /// Returns a reference to all open positions matching the given optional filter parameters. #[must_use] pub fn positions_open( &self, @@ -2118,6 +2173,7 @@ impl Cache { self.get_positions_for_ids(&position_ids, side) } + /// Returns a reference to all closed positions matching the given optional filter parameters. #[must_use] pub fn positions_closed( &self, @@ -2130,21 +2186,25 @@ impl Cache { self.get_positions_for_ids(&position_ids, side) } + /// Returns whether a position with the given `position_id` exists. #[must_use] pub fn position_exists(&self, position_id: &PositionId) -> bool { self.index.positions.contains(position_id) } + /// Returns whether a position with the given `position_id` is open. #[must_use] pub fn is_position_open(&self, position_id: &PositionId) -> bool { self.index.positions_open.contains(position_id) } + /// Returns whether a position with the given `position_id` is closed. #[must_use] pub fn is_position_closed(&self, position_id: &PositionId) -> bool { self.index.positions_closed.contains(position_id) } + /// Returns the count of all open positions. #[must_use] pub fn positions_open_count( &self, @@ -2152,11 +2212,12 @@ impl Cache { instrument_id: Option<&InstrumentId>, strategy_id: Option<&StrategyId>, side: Option, - ) -> u64 { + ) -> usize { self.positions_open(venue, instrument_id, strategy_id, side) - .len() as u64 + .len() } + /// Returns the count of all closed positions. #[must_use] pub fn positions_closed_count( &self, @@ -2164,11 +2225,12 @@ impl Cache { instrument_id: Option<&InstrumentId>, strategy_id: Option<&StrategyId>, side: Option, - ) -> u64 { + ) -> usize { self.positions_closed(venue, instrument_id, strategy_id, side) - .len() as u64 + .len() } + /// Returns the count of all positions. #[must_use] pub fn positions_total_count( &self, @@ -2176,18 +2238,20 @@ impl Cache { instrument_id: Option<&InstrumentId>, strategy_id: Option<&StrategyId>, side: Option, - ) -> u64 { + ) -> usize { self.positions(venue, instrument_id, strategy_id, side) - .len() as u64 + .len() } // -- STRATEGY QUERIES ------------------------------------------------------------------------ + /// Gets a reference to the strategy ID for the given `client_order_id` (if found). #[must_use] pub fn strategy_id_for_order(&self, client_order_id: &ClientOrderId) -> Option<&StrategyId> { self.index.order_strategy.get(client_order_id) } + /// Gets a reference to the strategy ID for the given `position_id` (if found). #[must_use] pub fn strategy_id_for_position(&self, position_id: &PositionId) -> Option<&StrategyId> { self.index.position_strategy.get(position_id) @@ -2195,6 +2259,7 @@ impl Cache { // -- GENERAL --------------------------------------------------------------------------------- + /// Gets a reference to the general object value for the given `key` (if found). pub fn get(&self, key: &str) -> anyhow::Result> { check_valid_string(key, stringify!(key))?; @@ -2203,6 +2268,7 @@ impl Cache { // -- DATA QUERIES ---------------------------------------------------------------------------- + /// Returns the price for the given `instrument_id` and `price_type` (if found). #[must_use] pub fn price(&self, instrument_id: &InstrumentId, price_type: PriceType) -> Option { match price_type { @@ -2230,6 +2296,7 @@ impl Cache { } } + /// Gets all quote ticks for the given `instrument_id`. #[must_use] pub fn quote_ticks(&self, instrument_id: &InstrumentId) -> Option> { self.quotes @@ -2237,6 +2304,7 @@ impl Cache { .map(|quotes| quotes.iter().copied().collect()) } + /// Gets all trade ticks for the given `instrument_id`. #[must_use] pub fn trade_ticks(&self, instrument_id: &InstrumentId) -> Option> { self.trades @@ -2244,6 +2312,7 @@ impl Cache { .map(|trades| trades.iter().copied().collect()) } + /// Gets all bars for the given `bar_type`. #[must_use] pub fn bars(&self, bar_type: &BarType) -> Option> { self.bars @@ -2251,11 +2320,13 @@ impl Cache { .map(|bars| bars.iter().copied().collect()) } + /// Gets a reference to the order book for the given `instrument_id`. #[must_use] pub fn order_book(&self, instrument_id: &InstrumentId) -> Option<&OrderBook> { self.books.get(instrument_id) } + /// Gets a reference to the latest quote tick for the given `instrument_id`. #[must_use] pub fn quote_tick(&self, instrument_id: &InstrumentId) -> Option<&QuoteTick> { self.quotes @@ -2263,6 +2334,7 @@ impl Cache { .and_then(|quotes| quotes.front()) } + /// Gets a refernece to the latest trade tick for the given `instrument_id`. #[must_use] pub fn trade_tick(&self, instrument_id: &InstrumentId) -> Option<&TradeTick> { self.trades @@ -2270,52 +2342,61 @@ impl Cache { .and_then(|trades| trades.front()) } + /// Gets a reference to the latest bar for the given `bar_type`. #[must_use] pub fn bar(&self, bar_type: &BarType) -> Option<&Bar> { self.bars.get(bar_type).and_then(|bars| bars.front()) } + /// Gets the order book update count for the given `instrument_id`. #[must_use] - pub fn book_update_count(&self, instrument_id: &InstrumentId) -> u64 { - self.books.get(instrument_id).map_or(0, |book| book.count) + pub fn book_update_count(&self, instrument_id: &InstrumentId) -> usize { + self.books.get(instrument_id).map_or(0, |book| book.count) as usize } + /// Gets the quote tick count for the given `instrument_id`. #[must_use] - pub fn quote_tick_count(&self, instrument_id: &InstrumentId) -> u64 { + pub fn quote_tick_count(&self, instrument_id: &InstrumentId) -> usize { self.quotes .get(instrument_id) - .map_or(0, std::collections::VecDeque::len) as u64 + .map_or(0, std::collections::VecDeque::len) } + /// Gets the trade tick count for the given `instrument_id`. #[must_use] - pub fn trade_tick_count(&self, instrument_id: &InstrumentId) -> u64 { + pub fn trade_tick_count(&self, instrument_id: &InstrumentId) -> usize { self.trades .get(instrument_id) - .map_or(0, std::collections::VecDeque::len) as u64 + .map_or(0, std::collections::VecDeque::len) } + /// Gets the bar count for the given `instrument_id`. #[must_use] - pub fn bar_count(&self, bar_type: &BarType) -> u64 { + pub fn bar_count(&self, bar_type: &BarType) -> usize { self.bars .get(bar_type) - .map_or(0, std::collections::VecDeque::len) as u64 + .map_or(0, std::collections::VecDeque::len) } + /// Returns whether the cache contains an order book for the given `instrument_id`. #[must_use] pub fn has_order_book(&self, instrument_id: &InstrumentId) -> bool { self.books.contains_key(instrument_id) } + /// Returns whether the cache contains quote ticks for the given `instrument_id`. #[must_use] pub fn has_quote_ticks(&self, instrument_id: &InstrumentId) -> bool { self.quote_tick_count(instrument_id) > 0 } + /// Returns whether the cache contains trade ticks for the given `instrument_id`. #[must_use] pub fn has_trade_ticks(&self, instrument_id: &InstrumentId) -> bool { self.trade_tick_count(instrument_id) > 0 } + /// Returns whether the cache contains bars for the given `bar_type`. #[must_use] pub fn has_bars(&self, bar_type: &BarType) -> bool { self.bar_count(bar_type) > 0 @@ -2323,11 +2404,13 @@ impl Cache { // -- INSTRUMENT QUERIES ---------------------------------------------------------------------- + /// Returns a reference to the instrument for the given `instrument_id` (if found). #[must_use] pub fn instrument(&self, instrument_id: &InstrumentId) -> Option<&InstrumentAny> { self.instruments.get(instrument_id) } + /// Returns references to all instrument IDs for the given `venue`. #[must_use] pub fn instrument_ids(&self, venue: &Venue) -> Vec<&InstrumentId> { self.instruments @@ -2336,6 +2419,7 @@ impl Cache { .collect() } + /// Returns references to all instruments for the given `venue`. #[must_use] pub fn instruments(&self, venue: &Venue) -> Vec<&InstrumentAny> { self.instruments @@ -2344,6 +2428,7 @@ impl Cache { .collect() } + /// Returns references to all bar types contained in the cache. #[must_use] pub fn bar_types( &self, @@ -2370,16 +2455,19 @@ impl Cache { // -- SYNTHETIC QUERIES ----------------------------------------------------------------------- + /// Returns a reference to the synthetic instrument for the given `instrument_id` (if found). #[must_use] pub fn synthetic(&self, instrument_id: &InstrumentId) -> Option<&SyntheticInstrument> { self.synthetics.get(instrument_id) } + /// Returns references to instrument IDs for all synthetic instruments contained in the cache. #[must_use] pub fn synthetic_ids(&self) -> Vec<&InstrumentId> { self.synthetics.keys().collect() } + /// Returns references to all synthetic instruments contained in the cache. #[must_use] pub fn synthetics(&self) -> Vec<&SyntheticInstrument> { self.synthetics.values().collect() @@ -2387,6 +2475,7 @@ impl Cache { // -- ACCOUNT QUERIES ----------------------------------------------------------------------- + /// Returns a reference to the account for the given `account_id` (if found). #[must_use] pub fn account(&self, account_id: &AccountId) -> Option<&dyn Account> { self.accounts @@ -2394,6 +2483,7 @@ impl Cache { .map(std::convert::AsRef::as_ref) } + /// Returns a reference to the account for the given `venue` (if found). #[must_use] pub fn account_for_venue(&self, venue: &Venue) -> Option<&dyn Account> { self.index @@ -2403,11 +2493,13 @@ impl Cache { .map(std::convert::AsRef::as_ref) } + /// Returns a reference to the account ID for the given `venue` (if found). #[must_use] pub fn account_id(&self, venue: &Venue) -> Option<&AccountId> { self.index.venue_account.get(venue) } + /// Returns references to all accounts for the given `account_id`. #[must_use] pub fn accounts(&self, account_id: &AccountId) -> Vec<&dyn Account> { self.accounts @@ -2425,7 +2517,7 @@ mod tests { use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, - enums::OrderSide, + enums::{OrderSide, OrderStatus}, events::order::{accepted::OrderAccepted, event::OrderEventAny, submitted::OrderSubmitted}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, position_id::PositionId, @@ -2437,8 +2529,8 @@ mod tests { }, orders::{any::OrderAny, stubs::TestOrderStubs}, polymorphism::{ - ApplyOrderEventAny, GetAccountId, GetClientOrderId, GetInstrumentId, GetStrategyId, - GetTraderId, GetVenueOrderId, IsOpen, + ApplyOrderEventAny, GetAccountId, GetClientOrderId, GetInstrumentId, GetOrderStatus, + GetStrategyId, GetTraderId, GetVenueOrderId, IsOpen, }, types::{price::Price, quantity::Quantity}, }; @@ -2467,21 +2559,21 @@ mod tests { } #[rstest] - fn test_dispose_when_empty(cache: Cache) { + fn test_dispose_when_empty(mut cache: Cache) { let result = cache.dispose(); assert!(result.is_ok()); } #[rstest] - fn test_flush_db_when_empty(cache: Cache) { + fn test_flush_db_when_empty(mut cache: Cache) { let result = cache.flush_db(); assert!(result.is_ok()); } #[rstest] fn test_check_residuals_when_empty(cache: Cache) { - let result = cache.flush_db(); - assert!(result.is_ok()); + let result = cache.check_residuals(); + assert!(!result); } #[rstest] @@ -2599,22 +2691,23 @@ mod tests { let result = cache.order(&order.client_order_id()).unwrap(); + assert_eq!(order.status(), OrderStatus::Submitted); assert_eq!(result, &order); assert_eq!(cache.orders(None, None, None, None), vec![&order]); assert!(cache.orders_open(None, None, None, None).is_empty()); assert!(cache.orders_closed(None, None, None, None).is_empty()); assert!(cache.orders_emulated(None, None, None, None).is_empty()); - assert!(cache.orders_inflight(None, None, None, None).is_empty()); + assert!(!cache.orders_inflight(None, None, None, None).is_empty()); assert!(cache.order_exists(&order.client_order_id())); assert!(!cache.is_order_open(&order.client_order_id())); assert!(!cache.is_order_closed(&order.client_order_id())); assert!(!cache.is_order_emulated(&order.client_order_id())); - assert!(!cache.is_order_inflight(&order.client_order_id())); + assert!(cache.is_order_inflight(&order.client_order_id())); assert!(!cache.is_order_pending_cancel_local(&order.client_order_id())); assert_eq!(cache.orders_open_count(None, None, None, None), 0); assert_eq!(cache.orders_closed_count(None, None, None, None), 0); assert_eq!(cache.orders_emulated_count(None, None, None, None), 0); - assert_eq!(cache.orders_inflight_count(None, None, None, None), 0); + assert_eq!(cache.orders_inflight_count(None, None, None, None), 1); assert_eq!(cache.orders_total_count(None, None, None, None), 1); assert_eq!(cache.venue_order_id(&order.client_order_id()), None); } diff --git a/nautilus_core/common/src/cache/database.rs b/nautilus_core/common/src/cache/database.rs index 8ee5905df02..62477d7ca19 100644 --- a/nautilus_core/common/src/cache/database.rs +++ b/nautilus_core/common/src/cache/database.rs @@ -19,269 +19,115 @@ #![allow(dead_code)] #![allow(unused_variables)] -use std::{collections::HashMap, sync::mpsc::Receiver}; +use std::collections::HashMap; -use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::nanos::UnixNanos; use nautilus_model::{ identifiers::{ account_id::AccountId, client_id::ClientId, client_order_id::ClientOrderId, component_id::ComponentId, instrument_id::InstrumentId, position_id::PositionId, - strategy_id::StrategyId, trader_id::TraderId, venue_order_id::VenueOrderId, + strategy_id::StrategyId, venue_order_id::VenueOrderId, }, instruments::{any::InstrumentAny, synthetic::SyntheticInstrument}, - orders::{any::OrderAny, base::Order}, + orders::any::OrderAny, position::Position, types::currency::Currency, }; use ustr::Ustr; -use crate::{enums::SerializationEncoding, interface::account::Account}; +use crate::interface::account::Account; -/// A type of database operation. -#[derive(Clone, Debug)] -pub enum DatabaseOperation { - Insert, - Update, - Delete, - Close, -} - -/// Represents a database command to be performed which may be executed in another thread. -#[derive(Clone, Debug)] -pub struct DatabaseCommand { - /// The database operation type. - pub op_type: DatabaseOperation, - /// The primary key for the operation. - pub key: Option, - /// The data payload for the operation. - pub payload: Option>>, -} - -impl DatabaseCommand { - #[must_use] - pub fn new(op_type: DatabaseOperation, key: String, payload: Option>>) -> Self { - Self { - op_type, - key: Some(key), - payload, - } - } - - /// Initialize a `Close` database command, this is meant to close the database cache channel. - #[must_use] - pub fn close() -> Self { - Self { - op_type: DatabaseOperation::Close, - key: None, - payload: None, - } - } -} - -/// 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; +pub trait CacheDatabaseAdapter { fn close(&mut self) -> 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 CacheDatabaseAdapter { - pub encoding: SerializationEncoding, - // database: Box, // TBD -} - -impl CacheDatabaseAdapter { - pub fn close(&self) -> anyhow::Result<()> { - Ok(()) // TODO - } - - pub fn flush(&self) -> anyhow::Result<()> { - Ok(()) // TODO - } - pub fn keys(&self) -> anyhow::Result> { - Ok(vec![]) - } + fn flush(&mut self) -> anyhow::Result<()>; - pub fn load(&self) -> anyhow::Result>> { - Ok(HashMap::new()) // TODO - } + fn load(&mut self) -> anyhow::Result>>; - pub fn load_currencies(&self) -> anyhow::Result> { - Ok(HashMap::new()) // TODO - } + fn load_currencies(&mut self) -> anyhow::Result>; - pub fn load_instruments(&self) -> anyhow::Result> { - Ok(HashMap::new()) // TODO - } + fn load_instruments(&mut self) -> anyhow::Result>; - pub fn load_synthetics(&self) -> anyhow::Result> { - Ok(HashMap::new()) // TODO - } + fn load_synthetics(&mut self) -> anyhow::Result>; - pub fn load_accounts(&self) -> anyhow::Result>> { - Ok(HashMap::new()) // TODO - } + fn load_accounts(&mut self) -> anyhow::Result>>; - pub fn load_orders(&self) -> anyhow::Result> { - Ok(HashMap::new()) // TODO - } + fn load_orders(&mut self) -> anyhow::Result>; - pub fn load_positions(&self) -> anyhow::Result> { - Ok(HashMap::new()) // TODO - } + fn load_positions(&mut self) -> anyhow::Result>; - pub fn load_index_order_position(&self) -> anyhow::Result> { - Ok(HashMap::new()) // TODO - } + fn load_index_order_position(&mut self) -> anyhow::Result>; - pub fn load_index_order_client(&self) -> anyhow::Result> { - Ok(HashMap::new()) // TODO - } + fn load_index_order_client(&mut self) -> anyhow::Result>; - pub fn load_currency(&self, code: &Ustr) -> anyhow::Result { - todo!() // TODO - } + fn load_currency(&mut self, code: &Ustr) -> anyhow::Result; - pub fn load_instrument(&self, instrument_id: &InstrumentId) -> anyhow::Result { - todo!() // TODO - } + fn load_instrument(&mut self, instrument_id: &InstrumentId) -> anyhow::Result; - pub fn load_synthetic( - &self, + fn load_synthetic( + &mut self, instrument_id: &InstrumentId, - ) -> anyhow::Result { - todo!() // TODO - } + ) -> anyhow::Result; - pub fn load_account(&self, account_id: &AccountId) -> anyhow::Result<()> { - todo!() // TODO - } + fn load_account(&mut self, account_id: &AccountId) -> anyhow::Result<()>; - pub fn load_order(&self, client_order_id: &ClientOrderId) -> anyhow::Result> { - todo!() // TODO - } + fn load_order(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result; - pub fn load_position(&self, position_id: &PositionId) -> anyhow::Result { - todo!() // TODO - } + fn load_position(&mut self, position_id: &PositionId) -> anyhow::Result; - pub fn load_actor( - &self, + fn load_actor( + &mut self, component_id: &ComponentId, - ) -> anyhow::Result>> { - todo!() // TODO - } + ) -> anyhow::Result>>; - pub fn delete_actor(&self, component_id: &ComponentId) -> anyhow::Result<()> { - todo!() // TODO - } + fn delete_actor(&mut self, component_id: &ComponentId) -> anyhow::Result<()>; - pub fn load_strategy( - &self, + fn load_strategy( + &mut self, strategy_id: &StrategyId, - ) -> anyhow::Result>> { - todo!() // TODO - } + ) -> anyhow::Result>>; - pub fn delete_strategy(&self, component_id: &StrategyId) -> anyhow::Result<()> { - todo!() // TODO - } + fn delete_strategy(&mut self, component_id: &StrategyId) -> anyhow::Result<()>; - pub fn add(&self, key: String, value: Vec) -> anyhow::Result<()> { - todo!() // TODO - } + fn add(&mut self, key: String, value: Vec) -> anyhow::Result<()>; - pub fn add_currency(&self, currency: &Currency) -> anyhow::Result<()> { - todo!() // TODO - } + fn add_currency(&mut self, currency: &Currency) -> anyhow::Result<()>; - pub fn add_instrument(&self, instrument: &InstrumentAny) -> anyhow::Result<()> { - todo!() // TODO - } + fn add_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()>; - pub fn add_synthetic(&self, synthetic: &SyntheticInstrument) -> anyhow::Result<()> { - todo!() // TODO - } + fn add_synthetic(&mut self, synthetic: &SyntheticInstrument) -> anyhow::Result<()>; - pub fn add_account(&self, account: &dyn Account) -> anyhow::Result> { - todo!() // TODO - } + fn add_account(&mut self, account: &dyn Account) -> anyhow::Result>; - pub fn add_order(&self, order: &OrderAny) -> anyhow::Result<()> { - todo!() // TODO - } + fn add_order(&mut self, order: &OrderAny) -> anyhow::Result<()>; - pub fn add_position(&self, position: &Position) -> anyhow::Result<()> { - todo!() // TODO - } + fn add_position(&mut self, position: &Position) -> anyhow::Result<()>; - pub fn index_venue_order_id( - &self, + fn index_venue_order_id( + &mut self, client_order_id: ClientOrderId, venue_order_id: VenueOrderId, - ) -> anyhow::Result<()> { - todo!() // TODO - } + ) -> anyhow::Result<()>; - pub fn index_order_position( - &self, + fn index_order_position( + &mut self, client_order_id: ClientOrderId, position_id: PositionId, - ) -> anyhow::Result<()> { - todo!() // TODO - } + ) -> anyhow::Result<()>; - pub fn update_actor(&self) -> anyhow::Result<()> { - todo!() // TODO - } + fn update_actor(&mut self) -> anyhow::Result<()>; - pub fn update_strategy(&self) -> anyhow::Result<()> { - todo!() // TODO - } + fn update_strategy(&mut self) -> anyhow::Result<()>; - pub fn update_account(&self, account: &dyn Account) -> anyhow::Result<()> { - todo!() // TODO - } + fn update_account(&mut self, account: &dyn Account) -> anyhow::Result<()>; - pub fn update_order(&self, order: &OrderAny) -> anyhow::Result<()> { - todo!() // TODO - } + fn update_order(&mut self, order: &OrderAny) -> anyhow::Result<()>; - pub fn update_position(&self, position: &Position) -> anyhow::Result<()> { - todo!() // TODO - } + fn update_position(&mut self, position: &Position) -> anyhow::Result<()>; - pub fn snapshot_order_state(&self, order: &OrderAny) -> anyhow::Result<()> { - todo!() // TODO - } + fn snapshot_order_state(&mut self, order: &OrderAny) -> anyhow::Result<()>; - pub fn snapshot_position_state(&self, position: &Position) -> anyhow::Result<()> { - todo!() // TODO - } + fn snapshot_position_state(&mut self, position: &Position) -> anyhow::Result<()>; - pub fn heartbeat(&self, timestamp: UnixNanos) -> anyhow::Result<()> { - todo!() // TODO - } + fn heartbeat(&mut self, timestamp: UnixNanos) -> anyhow::Result<()>; } diff --git a/nautilus_core/common/src/clock.rs b/nautilus_core/common/src/clock.rs index 3051268f961..4837d786bff 100644 --- a/nautilus_core/common/src/clock.rs +++ b/nautilus_core/common/src/clock.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides real-time and static test `Clock` implementations. +//! Real-time and static test `Clock` implementations. use std::{collections::HashMap, ops::Deref}; @@ -71,6 +71,9 @@ pub trait Clock { fn cancel_timers(&mut self); } +/// A static test clock. +/// +/// Stores the current timestamp internally which can be advanced. pub struct TestClock { time: AtomicTime, timers: HashMap, @@ -79,6 +82,7 @@ pub struct TestClock { } impl TestClock { + /// Creates a new [`TestClock`] instance. #[must_use] pub fn new() -> Self { Self { @@ -149,6 +153,7 @@ fn create_time_event_handler(event: TimeEvent, handler: &EventHandler) -> TimeEv } impl Default for TestClock { + /// Creates a new default [`TestClock`] instance. fn default() -> Self { Self::new() } @@ -261,6 +266,9 @@ impl Clock for TestClock { } } +/// A real-time clock which uses system time. +/// +/// Timestamps are guaranteed to be unique and monotonically increasing. pub struct LiveClock { time: &'static AtomicTime, timers: HashMap, @@ -268,6 +276,7 @@ pub struct LiveClock { } impl LiveClock { + /// Creates a new [`LiveClock`] instance. #[must_use] pub fn new() -> Self { Self { @@ -284,6 +293,7 @@ impl LiveClock { } impl Default for LiveClock { + /// Creates a new default [`LiveClock`] instance. fn default() -> Self { Self::new() } diff --git a/nautilus_core/common/src/enums.rs b/nautilus_core/common/src/enums.rs index 4a302e786bc..677502e3a88 100644 --- a/nautilus_core/common/src/enums.rs +++ b/nautilus_core/common/src/enums.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines common enumerations. +//! Enumerations for common components. use std::fmt::Debug; diff --git a/nautilus_core/common/src/factories.rs b/nautilus_core/common/src/factories.rs index 25f0d7a8662..d63fab6f6ae 100644 --- a/nautilus_core/common/src/factories.rs +++ b/nautilus_core/common/src/factories.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides factories for constructing domain objects such as orders. +//! Factories for constructing domain objects such as orders. use std::collections::HashMap; @@ -44,6 +44,7 @@ pub struct OrderFactory { } impl OrderFactory { + /// Creates a new [`OrderFactory`] instance. pub fn new( trader_id: TraderId, strategy_id: StrategyId, diff --git a/nautilus_core/common/src/ffi/clock.rs b/nautilus_core/common/src/ffi/clock.rs index adef67b72e1..2c7af904b57 100644 --- a/nautilus_core/common/src/ffi/clock.rs +++ b/nautilus_core/common/src/ffi/clock.rs @@ -34,7 +34,7 @@ use crate::{ timer::{TimeEvent, TimeEventHandler}, }; -/// Provides a C compatible Foreign Function Interface (FFI) for an underlying [`TestClock`]. +/// C compatible Foreign Function Interface (FFI) for an underlying [`TestClock`]. /// /// This struct wraps `TestClock` in a way that makes it compatible with C function /// calls, enabling interaction with `TestClock` in a C environment. @@ -243,7 +243,7 @@ pub extern "C" fn test_clock_cancel_timers(clock: &mut TestClock_API) { clock.cancel_timers(); } -/// Provides a C compatible Foreign Function Interface (FFI) for an underlying [`LiveClock`]. +/// C compatible Foreign Function Interface (FFI) for an underlying [`LiveClock`]. /// /// This struct wraps `LiveClock` in a way that makes it compatible with C function /// calls, enabling interaction with `LiveClock` in a C environment. diff --git a/nautilus_core/common/src/ffi/logging.rs b/nautilus_core/common/src/ffi/logging.rs index ea015315565..ec413a4dee1 100644 --- a/nautilus_core/common/src/ffi/logging.rs +++ b/nautilus_core/common/src/ffi/logging.rs @@ -37,7 +37,7 @@ use crate::{ }, }; -/// Provides a C compatible Foreign Function Interface (FFI) for an underlying [`LogGuard`]. +/// 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. diff --git a/nautilus_core/common/src/ffi/mod.rs b/nautilus_core/common/src/ffi/mod.rs index 6ef20a9758b..b8a88091d31 100644 --- a/nautilus_core/common/src/ffi/mod.rs +++ b/nautilus_core/common/src/ffi/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides a C foreign function interface (FFI) from `cbindgen`. +//! C foreign function interface (FFI) from `cbindgen`. pub mod clock; pub mod enums; diff --git a/nautilus_core/common/src/generators/client_order_id.rs b/nautilus_core/common/src/generators/client_order_id.rs index 1063484ac27..67154218c8c 100644 --- a/nautilus_core/common/src/generators/client_order_id.rs +++ b/nautilus_core/common/src/generators/client_order_id.rs @@ -29,6 +29,7 @@ pub struct ClientOrderIdGenerator { } impl ClientOrderIdGenerator { + /// Creates a new [`ClientOrderIdGenerator`] instance. #[must_use] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/common/src/generators/order_list_id.rs b/nautilus_core/common/src/generators/order_list_id.rs index a0a3ff8fa98..1c6efc6c8e2 100644 --- a/nautilus_core/common/src/generators/order_list_id.rs +++ b/nautilus_core/common/src/generators/order_list_id.rs @@ -29,6 +29,7 @@ pub struct OrderListIdGenerator { } impl OrderListIdGenerator { + /// Creates a new [`OrderListIdGenerator`] instance. #[must_use] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/common/src/generators/position_id.rs b/nautilus_core/common/src/generators/position_id.rs index 93f7cde6c89..24d183b8674 100644 --- a/nautilus_core/common/src/generators/position_id.rs +++ b/nautilus_core/common/src/generators/position_id.rs @@ -30,6 +30,7 @@ pub struct PositionIdGenerator { } impl PositionIdGenerator { + /// Creates a new [`PositionIdGenerator`] instance. #[must_use] pub fn new(trader_id: TraderId, clock: &'static AtomicTime) -> Self { Self { diff --git a/nautilus_core/common/src/handlers.rs b/nautilus_core/common/src/handlers.rs index 1ebbcd39f38..7ffb7b6e7a4 100644 --- a/nautilus_core/common/src/handlers.rs +++ b/nautilus_core/common/src/handlers.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides common message handlers. +//! Common message handlers. #[cfg(not(feature = "python"))] use std::ffi::c_char; diff --git a/nautilus_core/common/src/logging/logger.rs b/nautilus_core/common/src/logging/logger.rs index 5e1b84ae9c0..a1ff6364a04 100644 --- a/nautilus_core/common/src/logging/logger.rs +++ b/nautilus_core/common/src/logging/logger.rs @@ -64,6 +64,7 @@ pub struct LoggerConfig { } impl Default for LoggerConfig { + /// Creates a new default [`LoggerConfig`] instance. fn default() -> Self { Self { stdout_level: LevelFilter::Info, @@ -76,6 +77,7 @@ impl Default for LoggerConfig { } impl LoggerConfig { + /// Creates a new [`LoggerConfig`] instance. #[must_use] pub fn new( stdout_level: LevelFilter, @@ -146,7 +148,7 @@ impl LoggerConfig { /// configured to filter modules and write up to a specific level only using /// by passing a configuration using the `RUST_LOG` environment variable. -/// Provides a high-performance logger utilizing a MPSC channel under the hood. +/// A high-performance logger utilizing a MPSC channel under the hood. /// /// A separate thead is spawned at initialization which receives [`LogEvent`] structs over the /// channel. @@ -194,6 +196,7 @@ pub struct LogLineWrapper { } impl LogLineWrapper { + /// Creates a new [`LogLineWrapper`] instance. #[must_use] pub fn new(line: LogLine, trader_id: Ustr, timestamp: UnixNanos) -> Self { Self { @@ -479,6 +482,7 @@ pub struct LogGuard { } impl LogGuard { + /// Creates a new [`LogGuard`] instance. #[must_use] pub fn new(handle: Option>) -> Self { Self { handle } @@ -486,6 +490,7 @@ impl LogGuard { } impl Default for LogGuard { + /// Creates a new default [`LogGuard`] instance. fn default() -> Self { Self::new(None) } diff --git a/nautilus_core/common/src/logging/writer.rs b/nautilus_core/common/src/logging/writer.rs index 27bb6732d0b..11876edddf7 100644 --- a/nautilus_core/common/src/logging/writer.rs +++ b/nautilus_core/common/src/logging/writer.rs @@ -42,6 +42,7 @@ pub struct StdoutWriter { } impl StdoutWriter { + /// Creates a new [`StdoutWriter`] instance. #[must_use] pub fn new(level: LevelFilter, is_colored: bool) -> Self { Self { @@ -80,6 +81,7 @@ pub struct StderrWriter { } impl StderrWriter { + /// Creates a new [`StderrWriter`] instance. #[must_use] pub fn new(is_colored: bool) -> Self { Self { @@ -121,6 +123,7 @@ pub struct FileWriterConfig { } impl FileWriterConfig { + /// Creates a new [`FileWriterConfig`] instance. #[must_use] pub fn new( directory: Option, @@ -147,6 +150,7 @@ pub struct FileWriter { } impl FileWriter { + /// Creates a new [`FileWriter`] instance. pub fn new( trader_id: String, instance_id: String, diff --git a/nautilus_core/common/src/msgbus/core.rs b/nautilus_core/common/src/msgbus/core.rs index 441a27123de..dc4b65e5dd5 100644 --- a/nautilus_core/common/src/msgbus/core.rs +++ b/nautilus_core/common/src/msgbus/core.rs @@ -43,6 +43,7 @@ pub struct Subscription { } impl Subscription { + /// Creates a new [`Subscription`] instance. #[must_use] pub fn new( topic: Ustr, @@ -109,7 +110,7 @@ impl fmt::Display for BusMessage { } } -/// Provides a generic message bus to facilitate various messaging patterns. +/// A generic message bus to facilitate various messaging patterns. /// /// The bus provides both a producer and consumer API for Pub/Sub, Req/Rep, as /// well as direct point-to-point messaging to registered endpoints. @@ -165,7 +166,7 @@ pub struct MessageBus { } impl MessageBus { - /// Creates a new `MessageBus` instance. + /// Creates a new [`MessageBus`] instance. pub fn new( trader_id: TraderId, instance_id: UUID4, diff --git a/nautilus_core/common/src/msgbus/database.rs b/nautilus_core/common/src/msgbus/database.rs index 2e81e6a18cb..b8a82e09134 100644 --- a/nautilus_core/common/src/msgbus/database.rs +++ b/nautilus_core/common/src/msgbus/database.rs @@ -18,7 +18,7 @@ use std::collections::HashMap; use nautilus_core::uuid::UUID4; use nautilus_model::identifiers::trader_id::TraderId; -/// Provides a generic message bus database facade. +/// A generic message bus database facade. /// /// The main operations take a consistent `key` and `payload` which should provide enough /// information to implement the message bus database in many different technologies. diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index fdea2c94c8c..9a68f9e9276 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides real-time and test timers for use with `Clock` implementations. +//! Real-time and test timers for use with `Clock` implementations. use std::{ cmp::Ordering, @@ -59,8 +59,10 @@ pub struct TimeEvent { pub ts_init: UnixNanos, } -/// Assumes `name` is a valid string. impl TimeEvent { + /// Creates a new [`TimeEvent`] instance. + /// + /// Assumes `name` is a valid string. #[must_use] pub fn new(name: Ustr, event_id: UUID4, ts_event: UnixNanos, ts_init: UnixNanos) -> Self { Self { @@ -92,7 +94,7 @@ impl PartialEq for TimeEvent { #[derive(Clone, Debug)] /// Represents a time event and its associated handler. pub struct TimeEventHandler { - /// The event. + /// The time event. pub event: TimeEvent, /// The callable raw pointer. pub callback_ptr: *mut c_char, @@ -118,7 +120,7 @@ impl Ord for TimeEventHandler { } } -/// Provides a test timer for user with a `TestClock`. +/// A test timer for user with a `TestClock`. #[derive(Clone, Copy, Debug)] pub struct TestTimer { pub name: Ustr, @@ -130,7 +132,7 @@ pub struct TestTimer { } impl TestTimer { - /// Creates a new `TestTimer`. + /// Creates a new [`TestTimer`] instance. pub fn new( name: &str, interval_ns: u64, @@ -220,7 +222,7 @@ impl Iterator for TestTimer { } } -/// Provides a live timer for use with a `LiveClock`. +/// A live timer for use with a `LiveClock`. pub struct LiveTimer { pub name: Ustr, pub interval_ns: NonZeroU64, @@ -233,7 +235,7 @@ pub struct LiveTimer { } impl LiveTimer { - /// Creates a new `LiveTimer`. + /// Creates a new [`LiveTimer`] instance. pub fn new( name: &str, interval_ns: u64, diff --git a/nautilus_core/common/src/xrate.rs b/nautilus_core/common/src/xrate.rs index 3dade802142..9d47037673e 100644 --- a/nautilus_core/common/src/xrate.rs +++ b/nautilus_core/common/src/xrate.rs @@ -18,7 +18,7 @@ // as its not efficient to be allocating so many structures and doing so many recalculations" // **************************************************************************** -//! Provides exchange rate calculations between currencies. +//! Exchange rate calculations between currencies. //! //! An exchange rate is the value of one asset versus that of another. use std::collections::{HashMap, HashSet}; diff --git a/nautilus_core/core/src/correctness.rs b/nautilus_core/core/src/correctness.rs index e298f813d43..b20d8df4d1a 100644 --- a/nautilus_core/core/src/correctness.rs +++ b/nautilus_core/core/src/correctness.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides static condition checks similar to the *design by contract* philosophy +//! Functions for static condition checks similar to the *design by contract* philosophy //! to help ensure logical correctness. //! //! This module provides validation checking of function or method conditions. @@ -163,10 +163,12 @@ pub fn check_in_range_inclusive_i64(value: i64, l: i64, r: i64, param: &str) -> /// Checks 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<()> { + const EPSILON: f64 = 1e-15; // Epsilon to account for floating-point precision issues + if value.is_nan() || value.is_infinite() { anyhow::bail!("{FAILED} invalid f64 for '{param}', was {value}") } - if value < l || value > r { + if value < l - EPSILON || value > r + EPSILON { anyhow::bail!("{FAILED} invalid f64 for '{param}' not in range [{l}, {r}], was {value}") } Ok(()) @@ -196,7 +198,7 @@ pub fn check_slice_empty(slice: &[T], param: &str) -> anyhow::Result<()> { Ok(()) } -/// Checks the slice is *not* empty. +/// Checks the slice is **not** empty. pub fn check_slice_not_empty(slice: &[T], param: &str) -> anyhow::Result<()> { if slice.is_empty() { anyhow::bail!( @@ -219,7 +221,7 @@ pub fn check_map_empty(map: &HashMap, param: &str) -> anyhow::Result Ok(()) } -/// Checks the map is *not* empty. +/// Checks the map is **not** empty. pub fn check_map_not_empty(map: &HashMap, param: &str) -> anyhow::Result<()> { if map.is_empty() { anyhow::bail!( @@ -478,6 +480,31 @@ mod tests { assert!(check_in_range_inclusive_i64(value, l, r, param).is_ok()); } + #[rstest] + #[case(0.0, 0.0, 0.0, "value")] + #[case(0.0, 0.0, 1.0, "value")] + #[case(1.0, 0.0, 1.0, "value")] + fn test_check_in_range_inclusive_f64_when_in_range( + #[case] value: f64, + #[case] l: f64, + #[case] r: f64, + #[case] param: &str, + ) { + assert!(check_in_range_inclusive_f64(value, l, r, param).is_ok()); + } + + #[rstest] + #[case(-1e16, 0.0, 0.0, "value")] + #[case(1.0 + 1e16, 0.0, 1.0, "value")] + fn test_check_in_range_inclusive_f64_when_out_of_range( + #[case] value: f64, + #[case] l: f64, + #[case] r: f64, + #[case] param: &str, + ) { + assert!(check_in_range_inclusive_f64(value, l, r, param).is_err()); + } + #[rstest] #[case(0, 1, 2, "value")] #[case(3, 1, 2, "value")] diff --git a/nautilus_core/core/src/datetime.rs b/nautilus_core/core/src/datetime.rs index 9ceb2e75d89..7aaf322a4a4 100644 --- a/nautilus_core/core/src/datetime.rs +++ b/nautilus_core/core/src/datetime.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides common data and time functions. +//! Common data and time functions. use std::time::{Duration, UNIX_EPOCH}; diff --git a/nautilus_core/core/src/deserialization.rs b/nautilus_core/core/src/deserialization.rs deleted file mode 100644 index 0658b551db5..00000000000 --- a/nautilus_core/core/src/deserialization.rs +++ /dev/null @@ -1,91 +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::fmt; - -use serde::{ - de::{Unexpected, Visitor}, - Deserializer, -}; - -struct BoolVisitor; - -impl<'de> Visitor<'de> for BoolVisitor { - type Value = u8; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a boolean as u8") - } - - fn visit_bool(self, value: bool) -> Result - where - E: serde::de::Error, - { - Ok(u8::from(value)) - } - - fn visit_u64(self, value: u64) -> Result - where - E: serde::de::Error, - { - if value > u64::from(u8::MAX) { - Err(E::invalid_value(Unexpected::Unsigned(value), &self)) - } else { - Ok(value as u8) - } - } -} - -pub fn from_bool_as_u8<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - deserializer.deserialize_any(BoolVisitor) -} - -#[cfg(test)] -mod tests { - use serde::Deserialize; - - use super::from_bool_as_u8; - - #[derive(Deserialize)] - pub struct TestStruct { - #[serde(deserialize_with = "from_bool_as_u8")] - pub value: u8, - } - - #[test] - fn test_deserialize_bool_as_u8_with_boolean() { - let json_true = r#"{"value": true}"#; - let test_struct: TestStruct = serde_json::from_str(json_true).unwrap(); - assert_eq!(test_struct.value, 1); - - let json_false = r#"{"value": false}"#; - let test_struct: TestStruct = serde_json::from_str(json_false).unwrap(); - assert_eq!(test_struct.value, 0); - } - - #[test] - fn test_deserialize_bool_as_u8_with_u64() { - let json_true = r#"{"value": 1}"#; - let test_struct: TestStruct = serde_json::from_str(json_true).unwrap(); - assert_eq!(test_struct.value, 1); - - let json_false = r#"{"value": 0}"#; - let test_struct: TestStruct = serde_json::from_str(json_false).unwrap(); - assert_eq!(test_struct.value, 0); - } -} diff --git a/nautilus_core/core/src/equality.rs b/nautilus_core/core/src/equality.rs index 49a078d79d5..24360770425 100644 --- a/nautilus_core/core/src/equality.rs +++ b/nautilus_core/core/src/equality.rs @@ -13,9 +13,14 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Functions to evaluation total equality of two instances. + use pretty_assertions::assert_eq; use serde::Serialize; +/// Return whether `a` and `b` are entirely equal (all fields). +/// +/// Serializes the input values to JSON and compares the resulting strings. pub fn entirely_equal(a: T, b: T) { let a_serialized = serde_json::to_string(&a).unwrap(); let b_serialized = serde_json::to_string(&b).unwrap(); diff --git a/nautilus_core/core/src/ffi/mod.rs b/nautilus_core/core/src/ffi/mod.rs index dcd5dcba6e5..a3f4389f9ae 100644 --- a/nautilus_core/core/src/ffi/mod.rs +++ b/nautilus_core/core/src/ffi/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides a C foreign function interface (FFI) from `cbindgen`. +//! C foreign function interface (FFI) from `cbindgen`. pub mod cvec; pub mod datetime; diff --git a/nautilus_core/core/src/lib.rs b/nautilus_core/core/src/lib.rs index 11c7d9026f6..62a04051182 100644 --- a/nautilus_core/core/src/lib.rs +++ b/nautilus_core/core/src/lib.rs @@ -29,7 +29,6 @@ pub mod correctness; pub mod datetime; -pub mod deserialization; pub mod equality; pub mod message; pub mod nanos; diff --git a/nautilus_core/core/src/message.rs b/nautilus_core/core/src/message.rs index 97f5a89d748..9d2d5163470 100644 --- a/nautilus_core/core/src/message.rs +++ b/nautilus_core/core/src/message.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines common message types. +//! Common message types. use crate::{nanos::UnixNanos, uuid::UUID4}; diff --git a/nautilus_core/core/src/nanos.rs b/nautilus_core/core/src/nanos.rs index fb32ba6ef40..54980302535 100644 --- a/nautilus_core/core/src/nanos.rs +++ b/nautilus_core/core/src/nanos.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines `UnixNanos` type for working with UNIX epoch (nanoseconds). +//! A `UnixNanos` type for working with UNIX epoch (nanoseconds). use std::{ cmp::Ordering, diff --git a/nautilus_core/core/src/parsing.rs b/nautilus_core/core/src/parsing.rs index 46fe3e8094a..7e5d55eacb9 100644 --- a/nautilus_core/core/src/parsing.rs +++ b/nautilus_core/core/src/parsing.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides core parsing functions. +//! Core parsing functions. /// Returns the decimal precision inferred from the given string. #[must_use] diff --git a/nautilus_core/core/src/serialization.rs b/nautilus_core/core/src/serialization.rs index fc1af358a15..07483c2e419 100644 --- a/nautilus_core/core/src/serialization.rs +++ b/nautilus_core/core/src/serialization.rs @@ -13,8 +13,16 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines common serialization traits. +//! Common serialization traits and functions. +use std::fmt; + +use serde::{ + de::{Unexpected, Visitor}, + Deserializer, +}; + +struct BoolVisitor; use serde::{Deserialize, Serialize}; /// Represents types which are serializable for JSON and `MsgPack` specifications. @@ -39,3 +47,69 @@ pub trait Serializable: Serialize + for<'de> Deserialize<'de> { rmp_serde::to_vec_named(self) } } + +impl<'de> Visitor<'de> for BoolVisitor { + type Value = u8; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a boolean as u8") + } + + fn visit_bool(self, value: bool) -> Result + where + E: serde::de::Error, + { + Ok(u8::from(value)) + } + + fn visit_u64(self, value: u64) -> Result + where + E: serde::de::Error, + { + if value > u64::from(u8::MAX) { + Err(E::invalid_value(Unexpected::Unsigned(value), &self)) + } else { + Ok(value as u8) + } + } +} + +pub fn from_bool_as_u8<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(BoolVisitor) +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod tests { + use rstest::*; + use serde::Deserialize; + + use super::from_bool_as_u8; + + #[derive(Deserialize)] + pub struct TestStruct { + #[serde(deserialize_with = "from_bool_as_u8")] + pub value: u8, + } + + #[rstest] + #[case(r#"{"value": true}"#, 1)] + #[case(r#"{"value": false}"#, 0)] + fn test_deserialize_bool_as_u8_with_boolean(#[case] json_str: &str, #[case] expected: u8) { + let test_struct: TestStruct = serde_json::from_str(json_str).unwrap(); + assert_eq!(test_struct.value, expected); + } + + #[rstest] + #[case(r#"{"value": 1}"#, 1)] + #[case(r#"{"value": 0}"#, 0)] + fn test_deserialize_bool_as_u8_with_u64(#[case] json_str: &str, #[case] expected: u8) { + let test_struct: TestStruct = serde_json::from_str(json_str).unwrap(); + assert_eq!(test_struct.value, expected); + } +} diff --git a/nautilus_core/core/src/time.rs b/nautilus_core/core/src/time.rs index 680ad87a892..d2b06d3c69f 100644 --- a/nautilus_core/core/src/time.rs +++ b/nautilus_core/core/src/time.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides the core `AtomicTime` real-time and static clocks. +//! The core `AtomicTime` real-time and static clocks. use std::{ ops::Deref, @@ -29,10 +29,10 @@ use crate::{ nanos::UnixNanos, }; -/// Provides a global atomic time in real-time mode for use across the system. +/// Global atomic time in real-time mode for use across the system. pub static ATOMIC_CLOCK_REALTIME: OnceLock = OnceLock::new(); -/// Provides a global atomic time in static mode for use across the system. +/// Global atomic time in static mode for use across the system. pub static ATOMIC_CLOCK_STATIC: OnceLock = OnceLock::new(); /// Returns a static reference to the global atomic clock in real-time mode. @@ -85,13 +85,17 @@ impl Deref for AtomicTime { } impl Default for AtomicTime { + /// Creates a new default [`AtomicTime`] instance. fn default() -> Self { Self::new(true, UnixNanos::default()) } } impl AtomicTime { - /// New atomic clock set with the given UNIX time (nanoseconds). + /// Creates a new [`AtomicTime`] instance. + /// + /// The `realtime` flag will determine whether the atomic time is based off system time. + /// The time will be set to the given UNIX `time` (nanoseconds). #[must_use] pub fn new(realtime: bool, time: UnixNanos) -> Self { Self { diff --git a/nautilus_core/core/src/uuid.rs b/nautilus_core/core/src/uuid.rs index e2ebfc1ab6c..3e9ede04fc7 100644 --- a/nautilus_core/core/src/uuid.rs +++ b/nautilus_core/core/src/uuid.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! A core `UUID4` universally unique identifier (UUID) version 4 based on a 128-bit +//! A `UUID4` universally unique identifier (UUID) version 4 based on a 128-bit //! label (RFC 4122). use std::{ @@ -43,7 +43,7 @@ pub struct UUID4 { } impl UUID4 { - /// Creates a new `UUID4`. + /// Creates a new [`UUID4`] instance. #[must_use] pub fn new() -> Self { let uuid = Uuid::new_v4(); @@ -84,6 +84,7 @@ impl From<&str> for UUID4 { } impl Default for UUID4 { + /// Creates a new default [`UUID4`] instance. fn default() -> Self { Self::new() } diff --git a/nautilus_core/execution/src/client.rs b/nautilus_core/execution/src/client.rs index 39615710481..d4f7135fbb5 100644 --- a/nautilus_core/execution/src/client.rs +++ b/nautilus_core/execution/src/client.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides execution client base functionality. +//! Base execution client functionality. // Under development #![allow(dead_code)] diff --git a/nautilus_core/execution/src/matching_core.rs b/nautilus_core/execution/src/matching_core.rs index deac0486b23..17eaa1db705 100644 --- a/nautilus_core/execution/src/matching_core.rs +++ b/nautilus_core/execution/src/matching_core.rs @@ -31,7 +31,7 @@ use nautilus_model::{ types::price::Price, }; -/// Provides a generic order matching core. +/// A generic order matching core. pub struct OrderMatchingCore { /// The instrument ID for the matching core. pub instrument_id: InstrumentId, @@ -51,6 +51,7 @@ pub struct OrderMatchingCore { } impl OrderMatchingCore { + // Creates a new [`OrderMatchingCore`] instance. #[must_use] pub fn new( instrument_id: InstrumentId, diff --git a/nautilus_core/execution/src/messages/cancel.rs b/nautilus_core/execution/src/messages/cancel.rs index fa6b9de42ea..37fd1798382 100644 --- a/nautilus_core/execution/src/messages/cancel.rs +++ b/nautilus_core/execution/src/messages/cancel.rs @@ -38,6 +38,7 @@ pub struct CancelOrder { } impl CancelOrder { + /// Creates a new [`CancelOrder`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/execution/src/messages/cancel_all.rs b/nautilus_core/execution/src/messages/cancel_all.rs index 5b7d1f9d6c3..58f9a242ab5 100644 --- a/nautilus_core/execution/src/messages/cancel_all.rs +++ b/nautilus_core/execution/src/messages/cancel_all.rs @@ -40,6 +40,7 @@ pub struct CancelAllOrders { } impl CancelAllOrders { + /// Creates a new [`CancelAllOrders`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/execution/src/messages/cancel_batch.rs b/nautilus_core/execution/src/messages/cancel_batch.rs index 6553231ebf9..35e43e3db52 100644 --- a/nautilus_core/execution/src/messages/cancel_batch.rs +++ b/nautilus_core/execution/src/messages/cancel_batch.rs @@ -38,6 +38,7 @@ pub struct BatchCancelOrders { } impl BatchCancelOrders { + /// Creates a new [`BatchCancelOrders`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/execution/src/messages/mod.rs b/nautilus_core/execution/src/messages/mod.rs index c4f34ebd76d..96dec2e4c7c 100644 --- a/nautilus_core/execution/src/messages/mod.rs +++ b/nautilus_core/execution/src/messages/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines execution specific messages such as order commands. +//! Execution specific messages such as order commands. use nautilus_model::identifiers::{client_id::ClientId, instrument_id::InstrumentId}; use strum::Display; diff --git a/nautilus_core/execution/src/messages/modify.rs b/nautilus_core/execution/src/messages/modify.rs index 65796b43ba1..f5ac3d4584b 100644 --- a/nautilus_core/execution/src/messages/modify.rs +++ b/nautilus_core/execution/src/messages/modify.rs @@ -44,6 +44,7 @@ pub struct ModifyOrder { } impl ModifyOrder { + /// Creates a new [`ModifyOrder`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/execution/src/messages/query.rs b/nautilus_core/execution/src/messages/query.rs index dbf788744a7..e4539eb3694 100644 --- a/nautilus_core/execution/src/messages/query.rs +++ b/nautilus_core/execution/src/messages/query.rs @@ -38,6 +38,7 @@ pub struct QueryOrder { } impl QueryOrder { + /// Creates a new [`QueryOrder`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/execution/src/messages/submit.rs b/nautilus_core/execution/src/messages/submit.rs index 9117e77641c..99f4c74445f 100644 --- a/nautilus_core/execution/src/messages/submit.rs +++ b/nautilus_core/execution/src/messages/submit.rs @@ -42,6 +42,7 @@ pub struct SubmitOrder { } impl SubmitOrder { + /// Creates a new [`SubmitOrder`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/execution/src/messages/submit_list.rs b/nautilus_core/execution/src/messages/submit_list.rs index dc8bf73c49e..c0f535c5e69 100644 --- a/nautilus_core/execution/src/messages/submit_list.rs +++ b/nautilus_core/execution/src/messages/submit_list.rs @@ -43,6 +43,7 @@ pub struct SubmitOrderList { } impl SubmitOrderList { + /// Creates a new [`SubmitOrderList`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/indicators/src/average/ama.rs b/nautilus_core/indicators/src/average/ama.rs index 52246dd4949..53b4d6a7296 100644 --- a/nautilus_core/indicators/src/average/ama.rs +++ b/nautilus_core/indicators/src/average/ama.rs @@ -104,6 +104,7 @@ impl Indicator for AdaptiveMovingAverage { } impl AdaptiveMovingAverage { + /// Creates a new [`AdaptiveMovingAverage`] instance. pub fn new( period_efficiency_ratio: usize, period_fast: usize, diff --git a/nautilus_core/indicators/src/average/dema.rs b/nautilus_core/indicators/src/average/dema.rs index 3c465db7f04..05728e1bd80 100644 --- a/nautilus_core/indicators/src/average/dema.rs +++ b/nautilus_core/indicators/src/average/dema.rs @@ -87,6 +87,7 @@ impl Indicator for DoubleExponentialMovingAverage { } impl DoubleExponentialMovingAverage { + /// Creates a new [`DoubleExponentialMovingAverage`] instance. pub fn new(period: usize, price_type: Option) -> anyhow::Result { Ok(Self { period, diff --git a/nautilus_core/indicators/src/average/ema.rs b/nautilus_core/indicators/src/average/ema.rs index 63829e68036..e43f57371e4 100644 --- a/nautilus_core/indicators/src/average/ema.rs +++ b/nautilus_core/indicators/src/average/ema.rs @@ -78,6 +78,7 @@ impl Indicator for ExponentialMovingAverage { } impl ExponentialMovingAverage { + /// Creates a new [`ExponentialMovingAverage`] instance. 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. diff --git a/nautilus_core/indicators/src/average/hma.rs b/nautilus_core/indicators/src/average/hma.rs index cc025cf6e1e..f331328b448 100644 --- a/nautilus_core/indicators/src/average/hma.rs +++ b/nautilus_core/indicators/src/average/hma.rs @@ -96,6 +96,7 @@ fn _get_weights(size: usize) -> Vec { } impl HullMovingAverage { + /// Creates a new [`HullMovingAverage`] instance. 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/mod.rs b/nautilus_core/indicators/src/average/mod.rs index 95db06550fb..1cb9448a77f 100644 --- a/nautilus_core/indicators/src/average/mod.rs +++ b/nautilus_core/indicators/src/average/mod.rs @@ -33,6 +33,7 @@ pub mod hma; pub mod rma; pub mod sma; pub mod vidya; +pub mod vwap; pub mod wma; #[repr(C)] diff --git a/nautilus_core/indicators/src/average/rma.rs b/nautilus_core/indicators/src/average/rma.rs index fc43d102a68..ab735e50e93 100644 --- a/nautilus_core/indicators/src/average/rma.rs +++ b/nautilus_core/indicators/src/average/rma.rs @@ -78,6 +78,7 @@ impl Indicator for WilderMovingAverage { } impl WilderMovingAverage { + /// Creates a new [`WilderMovingAverage`] instance. 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. diff --git a/nautilus_core/indicators/src/average/sma.rs b/nautilus_core/indicators/src/average/sma.rs index 705ace65122..a8cc57d2e12 100644 --- a/nautilus_core/indicators/src/average/sma.rs +++ b/nautilus_core/indicators/src/average/sma.rs @@ -77,6 +77,7 @@ impl Indicator for SimpleMovingAverage { } impl SimpleMovingAverage { + /// Creates a new [`SimpleMovingAverage`] instance. 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. diff --git a/nautilus_core/indicators/src/average/vidya.rs b/nautilus_core/indicators/src/average/vidya.rs index eb780edb152..ee94e76a61d 100644 --- a/nautilus_core/indicators/src/average/vidya.rs +++ b/nautilus_core/indicators/src/average/vidya.rs @@ -41,7 +41,7 @@ pub struct VariableIndexDynamicAverage { pub initialized: bool, has_inputs: bool, pub cmo: ChandeMomentumOscillator, - cmo_pct: f64, + pub cmo_pct: f64, } impl Display for VariableIndexDynamicAverage { @@ -86,6 +86,7 @@ impl Indicator for VariableIndexDynamicAverage { } impl VariableIndexDynamicAverage { + /// Creates a new [`VariableIndexDynamicAverage`] instance. pub fn new( period: usize, price_type: Option, diff --git a/nautilus_core/indicators/src/average/vwap.rs b/nautilus_core/indicators/src/average/vwap.rs new file mode 100644 index 00000000000..ea4f52330ed --- /dev/null +++ b/nautilus_core/indicators/src/average/vwap.rs @@ -0,0 +1,189 @@ +// ------------------------------------------------------------------------------------------------- +// 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::fmt::{Display, Formatter}; + +use nautilus_model::data::bar::Bar; + +use crate::indicator::Indicator; + +#[repr(C)] +#[derive(Debug)] +#[cfg_attr( + feature = "python", + pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators") +)] +pub struct VolumeWeightedAveragePrice { + pub value: f64, + pub initialized: bool, + has_inputs: bool, + price_volume: f64, + volume_total: f64, + day: f64, +} + +impl Display for VolumeWeightedAveragePrice { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name()) + } +} + +impl Indicator for VolumeWeightedAveragePrice { + fn name(&self) -> String { + stringify!(VolumeWeightedAveragePrice).to_string() + } + + fn has_inputs(&self) -> bool { + self.has_inputs + } + + fn initialized(&self) -> bool { + self.initialized + } + + fn handle_bar(&mut self, bar: &Bar) { + self.update_raw( + (&bar.close).into(), + (&bar.volume).into(), + bar.ts_init.as_f64(), + ); + } + + fn reset(&mut self) { + self.value = 0.0; + self.has_inputs = false; + self.initialized = false; + self.day = 0.0; + self.price_volume = 0.0; + self.volume_total = 0.0; + self.value = 0.0; + } +} + +impl VolumeWeightedAveragePrice { + /// Creates a new [`VolumeWeightedAveragePrice`] instance. + pub fn new() -> anyhow::Result { + Ok(Self { + value: 0.0, + has_inputs: false, + initialized: false, + price_volume: 0.0, + volume_total: 0.0, + day: 0.0, + }) + } + + pub fn update_raw(&mut self, price: f64, volume: f64, timestamp: f64) { + if timestamp != self.day { + self.reset(); + self.day = timestamp; + self.value = price; + } + + if !self.initialized { + self.has_inputs = true; + self.initialized = true; + } + + if volume == 0.0 { + return; + } + + self.price_volume += price * volume; + self.volume_total += volume; + self.value = self.price_volume / self.volume_total; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod tests { + use nautilus_model::data::bar::Bar; + use rstest::rstest; + + use crate::{average::vwap::VolumeWeightedAveragePrice, indicator::Indicator, stubs::*}; + + #[rstest] + fn test_vwap_initialized(indicator_vwap: VolumeWeightedAveragePrice) { + let display_st = format!("{indicator_vwap}"); + assert_eq!(display_st, "VolumeWeightedAveragePrice"); + assert!(!indicator_vwap.initialized()); + assert!(!indicator_vwap.has_inputs()); + } + + #[rstest] + fn test_value_with_one_input(mut indicator_vwap: VolumeWeightedAveragePrice) { + indicator_vwap.update_raw(10.0, 10.0, 10.0); + assert_eq!(indicator_vwap.value, 10.0); + } + + #[rstest] + fn test_value_with_three_inputs_on_the_same_day( + mut indicator_vwap: VolumeWeightedAveragePrice, + ) { + indicator_vwap.update_raw(10.0, 10.0, 10.0); + indicator_vwap.update_raw(20.0, 20.0, 10.0); + indicator_vwap.update_raw(30.0, 30.0, 10.0); + assert_eq!(indicator_vwap.value, 23.333_333_333_333_332); + } + + #[rstest] + fn test_value_with_three_inputs_on_different_days( + mut indicator_vwap: VolumeWeightedAveragePrice, + ) { + indicator_vwap.update_raw(10.0, 10.0, 10.0); + indicator_vwap.update_raw(20.0, 20.0, 20.0); + indicator_vwap.update_raw(30.0, 30.0, 10.0); + assert_eq!(indicator_vwap.value, 30.0); + } + + #[rstest] + fn test_value_with_ten_inputs(mut indicator_vwap: VolumeWeightedAveragePrice) { + indicator_vwap.update_raw(1.00000, 1.00000, 10.0); + indicator_vwap.update_raw(1.00010, 2.00000, 10.0); + indicator_vwap.update_raw(1.00020, 3.00000, 10.0); + indicator_vwap.update_raw(1.00030, 1.00000, 10.0); + indicator_vwap.update_raw(1.00040, 2.00000, 10.0); + indicator_vwap.update_raw(1.00050, 3.00000, 10.0); + indicator_vwap.update_raw(1.00040, 1.00000, 10.0); + indicator_vwap.update_raw(1.00030, 2.00000, 10.0); + indicator_vwap.update_raw(1.00020, 3.00000, 10.0); + indicator_vwap.update_raw(1.00010, 1.00000, 10.0); + indicator_vwap.update_raw(1.00000, 2.00000, 10.0); + assert_eq!(indicator_vwap.value, 1.000_242_857_142_857); + } + + #[rstest] + fn test_handle_bar( + mut indicator_vwap: VolumeWeightedAveragePrice, + bar_ethusdt_binance_minute_bid: Bar, + ) { + indicator_vwap.handle_bar(&bar_ethusdt_binance_minute_bid); + assert_eq!(indicator_vwap.value, 1522.0); + assert!(indicator_vwap.initialized); + } + + #[rstest] + fn test_reset(mut indicator_vwap: VolumeWeightedAveragePrice) { + indicator_vwap.update_raw(10.0, 10.0, 10.0); + assert_eq!(indicator_vwap.value, 10.0); + indicator_vwap.reset(); + assert_eq!(indicator_vwap.value, 0.0); + assert!(!indicator_vwap.has_inputs); + assert!(!indicator_vwap.initialized); + } +} diff --git a/nautilus_core/indicators/src/average/wma.rs b/nautilus_core/indicators/src/average/wma.rs index b2bb5ee9453..c11a0bb2368 100644 --- a/nautilus_core/indicators/src/average/wma.rs +++ b/nautilus_core/indicators/src/average/wma.rs @@ -52,6 +52,7 @@ impl Display for WeightedMovingAverage { } impl WeightedMovingAverage { + /// Creates a new [`WeightedMovingAverage`] instance. pub fn new( period: usize, weights: Vec, diff --git a/nautilus_core/indicators/src/book/imbalance.rs b/nautilus_core/indicators/src/book/imbalance.rs index f2f78151302..0c8835af4d2 100644 --- a/nautilus_core/indicators/src/book/imbalance.rs +++ b/nautilus_core/indicators/src/book/imbalance.rs @@ -64,6 +64,7 @@ impl Indicator for BookImbalanceRatio { } impl BookImbalanceRatio { + /// Creates a new [`BookImbalanceRatio`] instance. pub fn new() -> anyhow::Result { // Inputs don't require validation, however we return a `Result` // to standardize with other indicators which do need validation. diff --git a/nautilus_core/indicators/src/indicator.rs b/nautilus_core/indicators/src/indicator.rs index 99b6d73020b..dabc96a6a9b 100644 --- a/nautilus_core/indicators/src/indicator.rs +++ b/nautilus_core/indicators/src/indicator.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines a common `Indicator` trait. +//! A common `Indicator` trait. use std::{fmt, fmt::Debug}; diff --git a/nautilus_core/indicators/src/momentum/aroon.rs b/nautilus_core/indicators/src/momentum/aroon.rs index 365f83f31f7..51c14d0100f 100644 --- a/nautilus_core/indicators/src/momentum/aroon.rs +++ b/nautilus_core/indicators/src/momentum/aroon.rs @@ -91,6 +91,7 @@ impl Indicator for AroonOscillator { } impl AroonOscillator { + /// Creates a new [`AroonOscillator`] instance. pub fn new(period: usize) -> anyhow::Result { Ok(Self { period, diff --git a/nautilus_core/indicators/src/momentum/bias.rs b/nautilus_core/indicators/src/momentum/bias.rs index 2101bb79a54..0969ee9a193 100644 --- a/nautilus_core/indicators/src/momentum/bias.rs +++ b/nautilus_core/indicators/src/momentum/bias.rs @@ -73,6 +73,7 @@ impl Indicator for Bias { } impl Bias { + /// Creates a new [`Bias`] instance. pub fn new(period: usize, ma_type: Option) -> anyhow::Result { Ok(Self { period, @@ -80,7 +81,7 @@ impl Bias { value: 0.0, count: 0, previous_close: 0.0, - ma: MovingAverageFactory::create(MovingAverageType::Simple, period), + ma: MovingAverageFactory::create(ma_type.unwrap_or(MovingAverageType::Simple), period), has_inputs: false, initialized: false, }) diff --git a/nautilus_core/indicators/src/momentum/cmo.rs b/nautilus_core/indicators/src/momentum/cmo.rs index d94dc6f5721..d2443548bde 100644 --- a/nautilus_core/indicators/src/momentum/cmo.rs +++ b/nautilus_core/indicators/src/momentum/cmo.rs @@ -81,6 +81,7 @@ impl Indicator for ChandeMomentumOscillator { } impl ChandeMomentumOscillator { + /// Creates a new [`ChandeMomentumOscillator`] instance. pub fn new(period: usize, ma_type: Option) -> anyhow::Result { Ok(Self { period, diff --git a/nautilus_core/indicators/src/momentum/mod.rs b/nautilus_core/indicators/src/momentum/mod.rs index 6a11fd173c0..2c50d306a53 100644 --- a/nautilus_core/indicators/src/momentum/mod.rs +++ b/nautilus_core/indicators/src/momentum/mod.rs @@ -19,3 +19,4 @@ pub mod aroon; pub mod bias; pub mod cmo; pub mod rsi; +pub mod vhf; diff --git a/nautilus_core/indicators/src/momentum/rsi.rs b/nautilus_core/indicators/src/momentum/rsi.rs index ea0f2c5ea9b..3be10825ebc 100644 --- a/nautilus_core/indicators/src/momentum/rsi.rs +++ b/nautilus_core/indicators/src/momentum/rsi.rs @@ -86,6 +86,7 @@ impl Indicator for RelativeStrengthIndex { } impl RelativeStrengthIndex { + /// Creates a new [`RelativeStrengthIndex`] instance. pub fn new(period: usize, ma_type: Option) -> anyhow::Result { Ok(Self { period, diff --git a/nautilus_core/indicators/src/momentum/vhf.rs b/nautilus_core/indicators/src/momentum/vhf.rs new file mode 100644 index 00000000000..f344dc74a38 --- /dev/null +++ b/nautilus_core/indicators/src/momentum/vhf.rs @@ -0,0 +1,204 @@ +// ------------------------------------------------------------------------------------------------- +// 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::VecDeque, + fmt::{Debug, Display}, +}; + +use nautilus_model::data::bar::Bar; + +use crate::{ + average::{MovingAverageFactory, MovingAverageType}, + indicator::{Indicator, MovingAverage}, +}; + +#[repr(C)] +#[derive(Debug)] +#[cfg_attr( + feature = "python", + pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators") +)] + +pub struct VerticalHorizontalFilter { + pub period: usize, + pub ma_type: MovingAverageType, + pub value: f64, + pub initialized: bool, + ma: Box, + has_inputs: bool, + previous_close: f64, + prices: VecDeque, +} + +impl Display for VerticalHorizontalFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}({},{})", self.name(), self.period, self.ma_type,) + } +} + +impl Indicator for VerticalHorizontalFilter { + fn name(&self) -> String { + stringify!(VerticalHorizontalFilter).to_string() + } + + fn has_inputs(&self) -> bool { + self.has_inputs + } + + fn initialized(&self) -> bool { + self.initialized + } + + fn handle_bar(&mut self, bar: &Bar) { + self.update_raw((&bar.close).into()); + } + + fn reset(&mut self) { + self.prices.clear(); + self.ma.reset(); + self.previous_close = 0.0; + self.value = 0.0; + self.has_inputs = false; + self.initialized = false; + } +} + +impl VerticalHorizontalFilter { + /// Creates a new [`VerticalHorizontalFilter`] instance. + pub fn new(period: usize, ma_type: Option) -> anyhow::Result { + Ok(Self { + period, + ma_type: ma_type.unwrap_or(MovingAverageType::Simple), + value: 0.0, + previous_close: 0.0, + ma: MovingAverageFactory::create(ma_type.unwrap_or(MovingAverageType::Simple), period), + has_inputs: false, + initialized: false, + prices: VecDeque::with_capacity(period), + }) + } + + pub fn update_raw(&mut self, close: f64) { + if !self.has_inputs { + self.previous_close = close; + } + self.prices.push_back(close); + + let max_price = self + .prices + .iter() + .copied() + .fold(f64::NEG_INFINITY, f64::max); + + let min_price = self.prices.iter().copied().fold(f64::INFINITY, f64::min); + + self.ma.update_raw(f64::abs(close - self.previous_close)); + if self.initialized { + self.value = f64::abs(max_price - min_price) / self.period as f64 / self.ma.value(); + } + self.previous_close = close; + + self._check_initialized(); + } + + pub fn _check_initialized(&mut self) { + if !self.initialized { + self.has_inputs = true; + if self.ma.initialized() { + self.initialized = true; + } + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod tests { + use nautilus_model::data::bar::Bar; + use rstest::rstest; + + use crate::{indicator::Indicator, momentum::vhf::VerticalHorizontalFilter, stubs::*}; + + #[rstest] + fn test_dema_initialized(vhf_10: VerticalHorizontalFilter) { + let display_str = format!("{vhf_10}"); + assert_eq!(display_str, "VerticalHorizontalFilter(10,SIMPLE)"); + assert_eq!(vhf_10.period, 10); + assert!(!vhf_10.initialized); + assert!(!vhf_10.has_inputs); + } + + #[rstest] + fn test_value_with_one_input(mut vhf_10: VerticalHorizontalFilter) { + vhf_10.update_raw(1.0); + assert_eq!(vhf_10.value, 0.0); + } + + #[rstest] + fn test_value_with_three_inputs(mut vhf_10: VerticalHorizontalFilter) { + vhf_10.update_raw(1.0); + vhf_10.update_raw(2.0); + vhf_10.update_raw(3.0); + assert_eq!(vhf_10.value, 0.0); + } + + #[rstest] + fn test_value_with_ten_inputs(mut vhf_10: VerticalHorizontalFilter) { + vhf_10.update_raw(1.00000); + vhf_10.update_raw(1.00010); + vhf_10.update_raw(1.00020); + vhf_10.update_raw(1.00030); + vhf_10.update_raw(1.00040); + vhf_10.update_raw(1.00050); + vhf_10.update_raw(1.00040); + vhf_10.update_raw(1.00030); + vhf_10.update_raw(1.00020); + vhf_10.update_raw(1.00010); + vhf_10.update_raw(1.00000); + assert_eq!(vhf_10.value, 0.5); + } + + #[rstest] + fn test_initialized_with_required_input(mut vhf_10: VerticalHorizontalFilter) { + for i in 1..10 { + vhf_10.update_raw(f64::from(i)); + } + assert!(!vhf_10.initialized); + vhf_10.update_raw(10.0); + assert!(vhf_10.initialized); + } + + #[rstest] + fn test_handle_bar(mut vhf_10: VerticalHorizontalFilter, bar_ethusdt_binance_minute_bid: Bar) { + vhf_10.handle_bar(&bar_ethusdt_binance_minute_bid); + assert_eq!(vhf_10.value, 0.0); + assert!(vhf_10.has_inputs); + assert!(!vhf_10.initialized); + } + + #[rstest] + fn test_reset(mut vhf_10: VerticalHorizontalFilter) { + vhf_10.update_raw(1.0); + assert_eq!(vhf_10.prices.len(), 1); + vhf_10.reset(); + assert_eq!(vhf_10.value, 0.0); + assert_eq!(vhf_10.prices.len(), 0); + assert!(!vhf_10.has_inputs); + assert!(!vhf_10.initialized); + } +} diff --git a/nautilus_core/indicators/src/python/average/mod.rs b/nautilus_core/indicators/src/python/average/mod.rs index dc1c7f2f8bf..16fc0a9f2bf 100644 --- a/nautilus_core/indicators/src/python/average/mod.rs +++ b/nautilus_core/indicators/src/python/average/mod.rs @@ -19,4 +19,6 @@ pub mod ema; pub mod hma; pub mod rma; pub mod sma; +pub mod vidya; +pub mod vwap; pub mod wma; diff --git a/nautilus_core/indicators/src/python/average/vidya.rs b/nautilus_core/indicators/src/python/average/vidya.rs index 443cadf8fc0..a66b859926f 100644 --- a/nautilus_core/indicators/src/python/average/vidya.rs +++ b/nautilus_core/indicators/src/python/average/vidya.rs @@ -21,7 +21,7 @@ use nautilus_model::{ use pyo3::prelude::*; use crate::{ - average::vidya::VariableIndexDynamicAverage, + average::{vidya::VariableIndexDynamicAverage, MovingAverageType}, indicator::{Indicator, MovingAverage}, }; @@ -30,14 +30,17 @@ impl VariableIndexDynamicAverage { #[new] pub fn py_new( period: usize, - weights: Vec, price_type: Option, + cmo_ma_type: Option, ) -> PyResult { - Self::new(period, weights, price_type).map_err(to_pyvalue_err) + Self::new(period, price_type, cmo_ma_type).map_err(to_pyvalue_err) } fn __repr__(&self) -> String { - format!("VariableIndexDynamicAverage({},{:?})", self.period, self.weights) + format!( + "VariableIndexDynamicAverage({},{:?})", + self.period, self.price_type + ) } #[getter] @@ -60,8 +63,8 @@ impl VariableIndexDynamicAverage { #[getter] #[pyo3(name = "alpha")] - fn py_alpha(&self) -> usize { - self.alpha() + fn py_alpha(&self) -> f64 { + self.alpha } #[getter] @@ -83,12 +86,11 @@ impl VariableIndexDynamicAverage { } #[getter] - #[pyo3(name = "cmo")] - fn py_cmo(&self) -> ChandeMomentumOscillator { - self.cmo + #[pyo3(name = "value")] + fn py_value(&self) -> f64 { + self.value } - #[pyo3(name = "handle_quote_tick")] fn py_handle_quote_tick(&mut self, tick: &QuoteTick) { self.py_update_raw(tick.extract_price(self.price_type).into()); diff --git a/nautilus_core/indicators/src/python/average/vwap.rs b/nautilus_core/indicators/src/python/average/vwap.rs new file mode 100644 index 00000000000..41df2d4faee --- /dev/null +++ b/nautilus_core/indicators/src/python/average/vwap.rs @@ -0,0 +1,81 @@ +// ------------------------------------------------------------------------------------------------- +// 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 nautilus_core::{nanos::UnixNanos, python::to_pyvalue_err}; +use nautilus_model::{ + data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, + enums::PriceType, +}; +use pyo3::prelude::*; + +use crate::{ + average::vwap::VolumeWeightedAveragePrice, + indicator::{Indicator, MovingAverage}, +}; + +#[pymethods] +impl VolumeWeightedAveragePrice { + #[new] + pub fn py_new() -> PyResult { + Self::new().map_err(to_pyvalue_err) + } + + fn __repr__(&self) -> String { + format!("VolumeWeightedAveragePrice") + } + + #[getter] + #[pyo3(name = "name")] + fn py_name(&self) -> String { + self.name() + } + + #[getter] + #[pyo3(name = "has_inputs")] + fn py_has_inputs(&self) -> bool { + self.has_inputs() + } + + #[getter] + #[pyo3(name = "value")] + fn py_value(&self) -> f64 { + self.value + } + + #[getter] + #[pyo3(name = "initialized")] + fn py_initialized(&self) -> bool { + self.initialized + } + + #[pyo3(name = "handle_bar")] + fn py_handle_bar(&mut self, bar: &Bar) { + self.py_update_raw( + (&bar.close).into(), + (&bar.volume).into(), + bar.ts_init.as_f64(), + ); + } + + #[pyo3(name = "reset")] + fn py_reset(&mut self) { + self.reset(); + } + + #[pyo3(name = "update_raw")] + fn py_update_raw(&mut self, value: f64, volume: f64, ts: f64) { + self.update_raw(value, volume, ts); + } +} diff --git a/nautilus_core/indicators/src/python/mod.rs b/nautilus_core/indicators/src/python/mod.rs index ed63431a324..4a30d1f6919 100644 --- a/nautilus_core/indicators/src/python/mod.rs +++ b/nautilus_core/indicators/src/python/mod.rs @@ -35,6 +35,7 @@ pub fn indicators(_: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; // book m.add_class::()?; // ratio @@ -44,6 +45,7 @@ pub fn indicators(_: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; // volatility m.add_class::()?; Ok(()) diff --git a/nautilus_core/indicators/src/python/momentum/mod.rs b/nautilus_core/indicators/src/python/momentum/mod.rs index 7ace632bf2d..b89c07bf626 100644 --- a/nautilus_core/indicators/src/python/momentum/mod.rs +++ b/nautilus_core/indicators/src/python/momentum/mod.rs @@ -17,3 +17,4 @@ pub mod aroon; pub mod bias; pub mod cmo; pub mod rsi; +pub mod vhf; diff --git a/nautilus_core/indicators/src/python/momentum/vhf.rs b/nautilus_core/indicators/src/python/momentum/vhf.rs new file mode 100644 index 00000000000..0a6b512302f --- /dev/null +++ b/nautilus_core/indicators/src/python/momentum/vhf.rs @@ -0,0 +1,89 @@ +// ------------------------------------------------------------------------------------------------- +// 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 nautilus_core::python::to_pyvalue_err; +use nautilus_model::data::{bar::Bar, quote::QuoteTick, trade::TradeTick}; +use pyo3::prelude::*; + +use crate::{ + average::MovingAverageType, indicator::Indicator, momentum::vhf::VerticalHorizontalFilter, +}; + +#[pymethods] +impl VerticalHorizontalFilter { + #[new] + pub fn py_new(period: usize, ma_type: Option) -> PyResult { + Self::new(period, ma_type).map_err(to_pyvalue_err) + } + + fn __repr__(&self) -> String { + format!("VerticalHorizontalFilter({})", self.period) + } + + #[getter] + #[pyo3(name = "name")] + fn py_name(&self) -> String { + self.name() + } + + #[getter] + #[pyo3(name = "period")] + fn py_period(&self) -> usize { + self.period + } + + #[getter] + #[pyo3(name = "has_inputs")] + fn py_has_inputs(&self) -> bool { + self.has_inputs() + } + + #[getter] + #[pyo3(name = "value")] + fn py_value(&self) -> f64 { + self.value + } + + #[getter] + #[pyo3(name = "initialized")] + fn py_initialized(&self) -> bool { + self.initialized + } + + #[pyo3(name = "update_raw")] + fn py_update_raw(&mut self, close: f64) { + self.update_raw(close); + } + + #[pyo3(name = "handle_quote_tick")] + fn py_handle_quote_tick(&mut self, _tick: &QuoteTick) { + // Function body intentionally left blank. + } + + #[pyo3(name = "handle_trade_tick")] + fn py_handle_trade_tick(&mut self, _tick: &TradeTick) { + // Function body intentionally left blank. + } + + #[pyo3(name = "handle_bar")] + fn py_handle_bar(&mut self, bar: &Bar) { + self.update_raw((&bar.close).into()); + } + + #[pyo3(name = "reset")] + fn py_reset(&mut self) { + self.reset(); + } +} diff --git a/nautilus_core/indicators/src/ratio/efficiency_ratio.rs b/nautilus_core/indicators/src/ratio/efficiency_ratio.rs index 0cda086051d..c039c060c01 100644 --- a/nautilus_core/indicators/src/ratio/efficiency_ratio.rs +++ b/nautilus_core/indicators/src/ratio/efficiency_ratio.rs @@ -79,6 +79,7 @@ impl Indicator for EfficiencyRatio { } impl EfficiencyRatio { + /// Creates a new [`EfficiencyRatio`] instance. pub fn new(period: usize, price_type: Option) -> anyhow::Result { Ok(Self { period, diff --git a/nautilus_core/indicators/src/stubs.rs b/nautilus_core/indicators/src/stubs.rs index 610b90805ab..0d4a87692f3 100644 --- a/nautilus_core/indicators/src/stubs.rs +++ b/nautilus_core/indicators/src/stubs.rs @@ -31,10 +31,13 @@ use crate::{ average::{ ama::AdaptiveMovingAverage, dema::DoubleExponentialMovingAverage, ema::ExponentialMovingAverage, hma::HullMovingAverage, rma::WilderMovingAverage, - sma::SimpleMovingAverage, vidya::VariableIndexDynamicAverage, wma::WeightedMovingAverage, - MovingAverageType, + sma::SimpleMovingAverage, vidya::VariableIndexDynamicAverage, + vwap::VolumeWeightedAveragePrice, wma::WeightedMovingAverage, MovingAverageType, + }, + momentum::{ + bias::Bias, cmo::ChandeMomentumOscillator, rsi::RelativeStrengthIndex, + vhf::VerticalHorizontalFilter, }, - momentum::{bias::Bias, cmo::ChandeMomentumOscillator, rsi::RelativeStrengthIndex}, ratio::efficiency_ratio::EfficiencyRatio, }; @@ -137,6 +140,11 @@ pub fn indicator_vidya_10() -> VariableIndexDynamicAverage { .unwrap() } +#[fixture] +pub fn indicator_vwap() -> VolumeWeightedAveragePrice { + VolumeWeightedAveragePrice::new().unwrap() +} + #[fixture] pub fn indicator_wma_10() -> WeightedMovingAverage { let weights = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]; @@ -168,3 +176,8 @@ pub fn cmo_10() -> ChandeMomentumOscillator { pub fn bias_10() -> Bias { Bias::new(10, Some(MovingAverageType::Wilder)).unwrap() } + +#[fixture] +pub fn vhf_10() -> VerticalHorizontalFilter { + VerticalHorizontalFilter::new(10, Some(MovingAverageType::Simple)).unwrap() +} diff --git a/nautilus_core/indicators/src/volatility/atr.rs b/nautilus_core/indicators/src/volatility/atr.rs index c6d41113687..f8d62f9e8aa 100644 --- a/nautilus_core/indicators/src/volatility/atr.rs +++ b/nautilus_core/indicators/src/volatility/atr.rs @@ -83,6 +83,7 @@ impl Indicator for AverageTrueRange { } impl AverageTrueRange { + /// Creates a new [`AverageTrueRange`] instance. pub fn new( period: usize, ma_type: Option, diff --git a/nautilus_core/infrastructure/Cargo.toml b/nautilus_core/infrastructure/Cargo.toml index b523ae13821..2b0cedbd025 100644 --- a/nautilus_core/infrastructure/Cargo.toml +++ b/nautilus_core/infrastructure/Cargo.toml @@ -24,7 +24,7 @@ serde_json = { workspace = true } tokio = { workspace = true } tracing = {workspace = true } ustr = { workspace = true } -redis = { version = "0.25.3", features = [ +redis = { version = "0.25.4", features = [ "connection-manager", "keep-alive", "tls-rustls", diff --git a/nautilus_core/infrastructure/src/python/redis/cache.rs b/nautilus_core/infrastructure/src/python/redis/cache.rs index a3a21208fe8..2f1ef677898 100644 --- a/nautilus_core/infrastructure/src/python/redis/cache.rs +++ b/nautilus_core/infrastructure/src/python/redis/cache.rs @@ -15,7 +15,6 @@ use std::collections::HashMap; -use nautilus_common::cache::database::CacheDatabase; use nautilus_core::{ python::{to_pyruntime_err, to_pyvalue_err}, uuid::UUID4, diff --git a/nautilus_core/infrastructure/src/redis/cache.rs b/nautilus_core/infrastructure/src/redis/cache.rs index 44b15a9bf0a..c16cbd73318 100644 --- a/nautilus_core/infrastructure/src/redis/cache.rs +++ b/nautilus_core/infrastructure/src/redis/cache.rs @@ -20,12 +20,26 @@ use std::{ time::{Duration, Instant}, }; -use nautilus_common::cache::database::{CacheDatabase, DatabaseCommand, DatabaseOperation}; -use nautilus_core::{correctness::check_slice_not_empty, uuid::UUID4}; -use nautilus_model::identifiers::trader_id::TraderId; +use nautilus_common::{ + cache::database::CacheDatabaseAdapter, enums::SerializationEncoding, + interface::account::Account, +}; +use nautilus_core::{correctness::check_slice_not_empty, nanos::UnixNanos, uuid::UUID4}; +use nautilus_model::{ + identifiers::{ + account_id::AccountId, client_id::ClientId, client_order_id::ClientOrderId, + component_id::ComponentId, instrument_id::InstrumentId, position_id::PositionId, + strategy_id::StrategyId, trader_id::TraderId, venue_order_id::VenueOrderId, + }, + instruments::{any::InstrumentAny, synthetic::SyntheticInstrument}, + orders::any::OrderAny, + position::Position, + types::currency::Currency, +}; use redis::{Commands, Connection, Pipeline}; use serde_json::{json, Value}; use tracing::{debug, error}; +use ustr::Ustr; use crate::redis::{create_redis_connection, get_buffer_interval}; @@ -63,6 +77,48 @@ const INDEX_POSITIONS: &str = "index:positions"; const INDEX_POSITIONS_OPEN: &str = "index:positions_open"; const INDEX_POSITIONS_CLOSED: &str = "index:positions_closed"; +/// A type of database operation. +#[derive(Clone, Debug)] +pub enum DatabaseOperation { + Insert, + Update, + Delete, + Close, +} + +/// Represents a database command to be performed which may be executed in another thread. +#[derive(Clone, Debug)] +pub struct DatabaseCommand { + /// The database operation type. + pub op_type: DatabaseOperation, + /// The primary key for the operation. + pub key: Option, + /// The data payload for the operation. + pub payload: Option>>, +} + +impl DatabaseCommand { + /// Creates a new [`DatabaseCommand`] instance. + #[must_use] + pub fn new(op_type: DatabaseOperation, key: String, payload: Option>>) -> Self { + Self { + op_type, + key: Some(key), + payload, + } + } + + /// Initialize a `Close` database command, this is meant to close the database cache channel. + #[must_use] + pub fn close() -> Self { + Self { + op_type: DatabaseOperation::Close, + key: None, + payload: None, + } + } +} + #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.infrastructure") @@ -75,10 +131,9 @@ pub struct RedisCacheDatabase { handle: Option>, } -impl CacheDatabase for RedisCacheDatabase { - type DatabaseType = RedisCacheDatabase; - - fn new( +impl RedisCacheDatabase { + /// Creates a new [`RedisCacheDatabase`] instance. + pub fn new( trader_id: TraderId, instance_id: UUID4, config: HashMap, @@ -109,7 +164,7 @@ impl CacheDatabase for RedisCacheDatabase { }) } - fn close(&mut self) -> anyhow::Result<()> { + pub fn close(&mut self) -> anyhow::Result<()> { debug!("Closing cache database adapter"); self.tx .send(DatabaseCommand::close()) @@ -123,14 +178,14 @@ impl CacheDatabase for RedisCacheDatabase { } } - fn flushdb(&mut self) -> anyhow::Result<()> { + pub 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) -> anyhow::Result> { + pub 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) { @@ -139,7 +194,7 @@ impl CacheDatabase for RedisCacheDatabase { } } - fn read(&mut self, key: &str) -> anyhow::Result>> { + pub fn read(&mut self, key: &str) -> anyhow::Result>> { let collection = get_collection_key(key)?; let key = format!("{}{DELIMITER}{}", self.trader_key, key); @@ -158,7 +213,7 @@ impl CacheDatabase for RedisCacheDatabase { } } - fn insert(&mut self, key: String, payload: Option>>) -> anyhow::Result<()> { + pub 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(()), @@ -166,7 +221,7 @@ impl CacheDatabase for RedisCacheDatabase { } } - fn update(&mut self, key: String, payload: Option>>) -> anyhow::Result<()> { + pub 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(()), @@ -174,7 +229,7 @@ impl CacheDatabase for RedisCacheDatabase { } } - fn delete(&mut self, key: String, payload: Option>>) -> anyhow::Result<()> { + pub 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(()), @@ -620,6 +675,198 @@ fn deserialize_payload( } } +#[allow(dead_code)] // Under development +pub struct RedisCacheDatabaseAdapter { + pub encoding: SerializationEncoding, + database: RedisCacheDatabase, +} + +#[allow(dead_code)] // Under development +#[allow(unused)] // Under development +impl CacheDatabaseAdapter for RedisCacheDatabaseAdapter { + fn close(&mut self) -> anyhow::Result<()> { + self.database.close() + } + + fn flush(&mut self) -> anyhow::Result<()> { + self.database.flushdb() + } + + fn load(&mut self) -> anyhow::Result>> { + // self.database.load() + Ok(HashMap::new()) // TODO + } + + fn load_currencies(&mut self) -> anyhow::Result> { + let mut currencies = HashMap::new(); + + for key in self.database.keys(&format!("{CURRENCIES}*"))? { + let parts: Vec<&str> = key.as_str().rsplitn(2, ':').collect(); + let currency_code = Ustr::from(parts.first().unwrap()); + let currency = self.load_currency(¤cy_code)?; + currencies.insert(currency_code, currency); + } + + Ok(currencies) + } + + fn load_instruments(&mut self) -> anyhow::Result> { + todo!() + } + + fn load_synthetics(&mut self) -> anyhow::Result> { + todo!() + } + + fn load_accounts(&mut self) -> anyhow::Result>> { + todo!() + } + + fn load_orders(&mut self) -> anyhow::Result> { + todo!() + } + + fn load_positions(&mut self) -> anyhow::Result> { + todo!() + } + + fn load_index_order_position(&mut self) -> anyhow::Result> { + todo!() + } + + fn load_index_order_client(&mut self) -> anyhow::Result> { + todo!() + } + + fn load_currency(&mut self, code: &Ustr) -> anyhow::Result { + todo!() + } + + fn load_instrument(&mut self, instrument_id: &InstrumentId) -> anyhow::Result { + todo!() + } + + fn load_synthetic( + &mut self, + instrument_id: &InstrumentId, + ) -> anyhow::Result { + todo!() + } + + fn load_account(&mut self, account_id: &AccountId) -> anyhow::Result<()> { + todo!() + } + + fn load_order(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result { + todo!() + } + + fn load_position(&mut self, position_id: &PositionId) -> anyhow::Result { + todo!() + } + + fn load_actor( + &mut self, + component_id: &ComponentId, + ) -> anyhow::Result>> { + todo!() + } + + fn delete_actor(&mut self, component_id: &ComponentId) -> anyhow::Result<()> { + todo!() + } + + fn load_strategy( + &mut self, + strategy_id: &StrategyId, + ) -> anyhow::Result>> { + todo!() + } + + fn delete_strategy(&mut self, component_id: &StrategyId) -> anyhow::Result<()> { + todo!() + } + + fn add(&mut self, key: String, value: Vec) -> anyhow::Result<()> { + todo!() + } + + fn add_currency(&mut self, currency: &Currency) -> anyhow::Result<()> { + todo!() + } + + fn add_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> { + todo!() + } + + fn add_synthetic(&mut self, synthetic: &SyntheticInstrument) -> anyhow::Result<()> { + todo!() + } + + fn add_account(&mut self, account: &dyn Account) -> anyhow::Result> { + todo!() + } + + fn add_order(&mut self, order: &OrderAny) -> anyhow::Result<()> { + todo!() + } + + fn add_position(&mut self, position: &Position) -> anyhow::Result<()> { + todo!() + } + + fn index_venue_order_id( + &mut self, + client_order_id: ClientOrderId, + venue_order_id: VenueOrderId, + ) -> anyhow::Result<()> { + todo!() + } + + fn index_order_position( + &mut self, + client_order_id: ClientOrderId, + position_id: PositionId, + ) -> anyhow::Result<()> { + todo!() + } + + fn update_actor(&mut self) -> anyhow::Result<()> { + todo!() + } + + fn update_strategy(&mut self) -> anyhow::Result<()> { + todo!() + } + + fn update_account(&mut self, account: &dyn Account) -> anyhow::Result<()> { + todo!() + } + + fn update_order(&mut self, order: &OrderAny) -> anyhow::Result<()> { + todo!() + } + + fn update_position(&mut self, position: &Position) -> anyhow::Result<()> { + todo!() + } + + fn snapshot_order_state(&mut self, order: &OrderAny) -> anyhow::Result<()> { + todo!() + } + + fn snapshot_position_state(&mut self, position: &Position) -> anyhow::Result<()> { + todo!() + } + + fn heartbeat(&mut self, timestamp: UnixNanos) -> anyhow::Result<()> { + todo!() + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod tests { use std::collections::HashMap; diff --git a/nautilus_core/infrastructure/src/sql/pg.rs b/nautilus_core/infrastructure/src/sql/pg.rs index 3a68c871cad..639bf6249ca 100644 --- a/nautilus_core/infrastructure/src/sql/pg.rs +++ b/nautilus_core/infrastructure/src/sql/pg.rs @@ -28,6 +28,7 @@ pub struct PostgresConnectOptions { } impl PostgresConnectOptions { + /// Creates a new [`PostgresConnectOptions`] instance. pub fn new( host: String, port: u16, diff --git a/nautilus_core/model/src/currencies.rs b/nautilus_core/model/src/currencies.rs index 19ddc0bae4f..f7c3ba614b4 100644 --- a/nautilus_core/model/src/currencies.rs +++ b/nautilus_core/model/src/currencies.rs @@ -988,7 +988,7 @@ impl Currency { } } -/// Provides a map of built-in `Currency` constants. +/// A map of built-in `Currency` constants. pub static CURRENCY_MAP: Lazy>> = Lazy::new(|| { let mut map = HashMap::new(); /////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/data/bar.rs b/nautilus_core/model/src/data/bar.rs index 44c8d4d997f..dd91adc35e8 100644 --- a/nautilus_core/model/src/data/bar.rs +++ b/nautilus_core/model/src/data/bar.rs @@ -53,6 +53,7 @@ pub struct BarSpecification { } impl BarSpecification { + /// Creates a new [`BarSpecification`] instance. #[must_use] pub fn new(step: usize, aggregation: BarAggregation, price_type: PriceType) -> Self { Self { @@ -87,6 +88,7 @@ pub struct BarType { } impl BarType { + /// Creates a new [`BarType`] instance. #[must_use] pub fn new( instrument_id: InstrumentId, @@ -230,6 +232,7 @@ pub struct Bar { } impl Bar { + /// Creates a new [`Bar`] instance. #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( diff --git a/nautilus_core/model/src/data/delta.rs b/nautilus_core/model/src/data/delta.rs index a86113b96e5..8380f5403a0 100644 --- a/nautilus_core/model/src/data/delta.rs +++ b/nautilus_core/model/src/data/delta.rs @@ -59,6 +59,7 @@ pub struct OrderBookDelta { } impl OrderBookDelta { + /// Creates a new [`OrderBookDelta`] instance. #[allow(clippy::too_many_arguments)] #[must_use] pub fn new( diff --git a/nautilus_core/model/src/data/deltas.rs b/nautilus_core/model/src/data/deltas.rs index 7f7d478e531..702bd80fec6 100644 --- a/nautilus_core/model/src/data/deltas.rs +++ b/nautilus_core/model/src/data/deltas.rs @@ -50,6 +50,7 @@ pub struct OrderBookDeltas { } impl OrderBookDeltas { + /// Creates a new [`OrderBookDeltas`] instance. #[allow(clippy::too_many_arguments)] #[must_use] pub fn new(instrument_id: InstrumentId, deltas: Vec) -> Self { @@ -111,7 +112,7 @@ impl GetTsInit for OrderBookDeltas { } } -/// Provides a C compatible Foreign Function Interface (FFI) for an underlying [`OrderBookDeltas`]. +/// C compatible Foreign Function Interface (FFI) for an underlying [`OrderBookDeltas`]. /// /// This struct wraps `OrderBookDeltas` in a way that makes it compatible with C function /// calls, enabling interaction with `OrderBookDeltas` in a C environment. diff --git a/nautilus_core/model/src/data/depth.rs b/nautilus_core/model/src/data/depth.rs index fbc85ced4f5..490c1880078 100644 --- a/nautilus_core/model/src/data/depth.rs +++ b/nautilus_core/model/src/data/depth.rs @@ -67,6 +67,7 @@ pub struct OrderBookDepth10 { } impl OrderBookDepth10 { + /// Creates a new [`OrderBookDepth10`] instance. #[allow(clippy::too_many_arguments)] #[must_use] pub fn new( diff --git a/nautilus_core/model/src/data/mod.rs b/nautilus_core/model/src/data/mod.rs index 451abf6aae7..7d4936f772a 100644 --- a/nautilus_core/model/src/data/mod.rs +++ b/nautilus_core/model/src/data/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines `Data` types for the trading domain model. +//! Data types for the trading domain model. pub mod bar; pub mod delta; diff --git a/nautilus_core/model/src/data/order.rs b/nautilus_core/model/src/data/order.rs index cc10eee94fb..6854cec2b23 100644 --- a/nautilus_core/model/src/data/order.rs +++ b/nautilus_core/model/src/data/order.rs @@ -64,6 +64,7 @@ pub struct BookOrder { } impl BookOrder { + /// Creates a new [`BookOrder`] instance. #[must_use] pub fn new(side: OrderSide, price: Price, size: Quantity, order_id: u64) -> Self { Self { @@ -95,6 +96,7 @@ impl BookOrder { } impl Default for BookOrder { + /// Creates a NULL [`BookOrder`] instance. fn default() -> Self { NULL_ORDER } diff --git a/nautilus_core/model/src/data/quote.rs b/nautilus_core/model/src/data/quote.rs index 64a283bcb7c..3bd2c63a924 100644 --- a/nautilus_core/model/src/data/quote.rs +++ b/nautilus_core/model/src/data/quote.rs @@ -61,6 +61,7 @@ pub struct QuoteTick { } impl QuoteTick { + /// Creates a new [`QuoteTick`] instance. pub fn new( instrument_id: InstrumentId, bid_price: Price, diff --git a/nautilus_core/model/src/data/stubs.rs b/nautilus_core/model/src/data/stubs.rs index d38b62f925d..8fe0cba852a 100644 --- a/nautilus_core/model/src/data/stubs.rs +++ b/nautilus_core/model/src/data/stubs.rs @@ -34,6 +34,7 @@ use crate::{ }; impl Default for QuoteTick { + /// Creates a new default [`QuoteTick`] instance for testing. fn default() -> Self { Self { instrument_id: InstrumentId::from("AUDUSD.SIM"), @@ -48,6 +49,7 @@ impl Default for QuoteTick { } impl Default for TradeTick { + /// Creates a new default [`TradeTick`] instance for testing. fn default() -> Self { TradeTick { instrument_id: InstrumentId::from("AUDUSD.SIM"), @@ -62,6 +64,7 @@ impl Default for TradeTick { } impl Default for Bar { + /// Creates a new default [`Bar`] instance for testing. fn default() -> Self { Self { bar_type: BarType::from("AUDUSD.SIM-1-MINUTE-LAST-INTERNAL"), diff --git a/nautilus_core/model/src/data/trade.rs b/nautilus_core/model/src/data/trade.rs index f7f419d995b..5a995d06a09 100644 --- a/nautilus_core/model/src/data/trade.rs +++ b/nautilus_core/model/src/data/trade.rs @@ -60,6 +60,7 @@ pub struct TradeTick { } impl TradeTick { + /// Creates a new [`TradeTick`] instance. #[must_use] pub fn new( instrument_id: InstrumentId, diff --git a/nautilus_core/model/src/enums.rs b/nautilus_core/model/src/enums.rs index 2c861edfca0..802ccb9b242 100644 --- a/nautilus_core/model/src/enums.rs +++ b/nautilus_core/model/src/enums.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines enumerations for the trading domain model. +//! Enumerations for the trading domain model. use std::str::FromStr; @@ -355,9 +355,9 @@ impl FromU8 for BookAction { pub enum BookType { /// Top-of-book best bid/ask, one level per side. L1_MBP = 1, - /// Market by price, one order per level (aggregated). + /// Market-by-price, one order per level (aggregated). L2_MBP = 2, - /// Market by order, multiple orders per level (full granularity). + /// Market-by-order, multiple orders per level (full granularity). L3_MBO = 3, } @@ -932,7 +932,7 @@ pub enum PriceType { )] #[allow(non_camel_case_types)] pub enum RecordFlag { - /// Last message in the packet from the venue for a given `instrument_id`. + /// Last message in the book event or packet from the venue for a given `instrument_id`. F_LAST = 1 << 7, // 128 /// Top-of-book message, not an individual order. F_TOB = 1 << 6, // 64 diff --git a/nautilus_core/model/src/events/account/state.rs b/nautilus_core/model/src/events/account/state.rs index 2366b8f0f0f..6a2c540d5f6 100644 --- a/nautilus_core/model/src/events/account/state.rs +++ b/nautilus_core/model/src/events/account/state.rs @@ -46,6 +46,7 @@ pub struct AccountState { } impl AccountState { + /// Creates a new [`AccountState`] instance. #[allow(clippy::too_many_arguments)] pub fn new( account_id: AccountId, diff --git a/nautilus_core/model/src/events/mod.rs b/nautilus_core/model/src/events/mod.rs index bc27a5283ee..8ebb3e74093 100644 --- a/nautilus_core/model/src/events/mod.rs +++ b/nautilus_core/model/src/events/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines order, position and account events for the trading domain model. +//! Events for the trading domain model. pub mod account; pub mod order; diff --git a/nautilus_core/model/src/events/order/accepted.rs b/nautilus_core/model/src/events/order/accepted.rs index 60403674a77..1e387a3556e 100644 --- a/nautilus_core/model/src/events/order/accepted.rs +++ b/nautilus_core/model/src/events/order/accepted.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{nanos::UnixNanos, serialization::from_bool_as_u8, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -58,6 +58,7 @@ pub struct OrderAccepted { } impl OrderAccepted { + /// Creates a new [`OrderAccepted`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/events/order/cancel_rejected.rs b/nautilus_core/model/src/events/order/cancel_rejected.rs index 6bb0c7e832b..2fe30fe716d 100644 --- a/nautilus_core/model/src/events/order/cancel_rejected.rs +++ b/nautilus_core/model/src/events/order/cancel_rejected.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{nanos::UnixNanos, serialization::from_bool_as_u8, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -59,6 +59,7 @@ pub struct OrderCancelRejected { } impl OrderCancelRejected { + /// Creates a new [`OrderCancelRejected`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/events/order/canceled.rs b/nautilus_core/model/src/events/order/canceled.rs index 4aef943ab44..8dcd693bb73 100644 --- a/nautilus_core/model/src/events/order/canceled.rs +++ b/nautilus_core/model/src/events/order/canceled.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{nanos::UnixNanos, serialization::from_bool_as_u8, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -58,6 +58,7 @@ pub struct OrderCanceled { } impl OrderCanceled { + /// Creates a new [`OrderCanceled`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/events/order/denied.rs b/nautilus_core/model/src/events/order/denied.rs index 7819a1196a3..d545408fd49 100644 --- a/nautilus_core/model/src/events/order/denied.rs +++ b/nautilus_core/model/src/events/order/denied.rs @@ -55,6 +55,7 @@ pub struct OrderDenied { } impl OrderDenied { + /// Creates a new [`OrderDenied`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/events/order/emulated.rs b/nautilus_core/model/src/events/order/emulated.rs index e17a5eb185a..5e720e8abed 100644 --- a/nautilus_core/model/src/events/order/emulated.rs +++ b/nautilus_core/model/src/events/order/emulated.rs @@ -54,6 +54,7 @@ pub struct OrderEmulated { } impl OrderEmulated { + /// Creates a new [`OrderEmulated`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/events/order/expired.rs b/nautilus_core/model/src/events/order/expired.rs index 9671d87ce4d..36c6921f645 100644 --- a/nautilus_core/model/src/events/order/expired.rs +++ b/nautilus_core/model/src/events/order/expired.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{nanos::UnixNanos, serialization::from_bool_as_u8, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -58,6 +58,7 @@ pub struct OrderExpired { } impl OrderExpired { + /// Creates a new [`OrderExpired`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/events/order/filled.rs b/nautilus_core/model/src/events/order/filled.rs index 251a58ceb46..2768b622f43 100644 --- a/nautilus_core/model/src/events/order/filled.rs +++ b/nautilus_core/model/src/events/order/filled.rs @@ -66,6 +66,7 @@ pub struct OrderFilled { } impl OrderFilled { + /// Creates a new [`OrderFilled`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, @@ -123,6 +124,7 @@ impl OrderFilled { } impl Default for OrderFilled { + /// Creates a new default [`OrderFilled`] instance for testing. fn default() -> Self { Self { trader_id: TraderId::default(), diff --git a/nautilus_core/model/src/events/order/initialized.rs b/nautilus_core/model/src/events/order/initialized.rs index e52e41afa90..9bc19c687b5 100644 --- a/nautilus_core/model/src/events/order/initialized.rs +++ b/nautilus_core/model/src/events/order/initialized.rs @@ -84,6 +84,7 @@ pub struct OrderInitialized { } impl Default for OrderInitialized { + /// Creates a new default [`OrderInitialized`] instance for testing. fn default() -> Self { Self { trader_id: TraderId::default(), @@ -124,6 +125,7 @@ impl Default for OrderInitialized { } impl OrderInitialized { + /// Creates a new [`OrderInitialized`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/events/order/modify_rejected.rs b/nautilus_core/model/src/events/order/modify_rejected.rs index a59a3d74190..2b91df16593 100644 --- a/nautilus_core/model/src/events/order/modify_rejected.rs +++ b/nautilus_core/model/src/events/order/modify_rejected.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{nanos::UnixNanos, serialization::from_bool_as_u8, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -59,6 +59,7 @@ pub struct OrderModifyRejected { } impl OrderModifyRejected { + /// Creates a new [`OrderModifyRejected`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/events/order/pending_cancel.rs b/nautilus_core/model/src/events/order/pending_cancel.rs index 1ce6f4dc0b6..ebb8a58c6f2 100644 --- a/nautilus_core/model/src/events/order/pending_cancel.rs +++ b/nautilus_core/model/src/events/order/pending_cancel.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{nanos::UnixNanos, serialization::from_bool_as_u8, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -58,6 +58,7 @@ pub struct OrderPendingCancel { } impl OrderPendingCancel { + /// Creates a new [`OrderPendingCancel`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/events/order/pending_update.rs b/nautilus_core/model/src/events/order/pending_update.rs index e1926644161..8b3e50b00d1 100644 --- a/nautilus_core/model/src/events/order/pending_update.rs +++ b/nautilus_core/model/src/events/order/pending_update.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{nanos::UnixNanos, serialization::from_bool_as_u8, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -58,6 +58,7 @@ pub struct OrderPendingUpdate { } impl OrderPendingUpdate { + /// Creates a new [`OrderPendingUpdate`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/events/order/rejected.rs b/nautilus_core/model/src/events/order/rejected.rs index 9c276f3babb..b7151f31a3c 100644 --- a/nautilus_core/model/src/events/order/rejected.rs +++ b/nautilus_core/model/src/events/order/rejected.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{nanos::UnixNanos, serialization::from_bool_as_u8, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -58,6 +58,7 @@ pub struct OrderRejected { } impl OrderRejected { + /// Creates a new [`OrderRejected`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/events/order/released.rs b/nautilus_core/model/src/events/order/released.rs index 1f23481e44f..11af4b0427f 100644 --- a/nautilus_core/model/src/events/order/released.rs +++ b/nautilus_core/model/src/events/order/released.rs @@ -55,6 +55,7 @@ pub struct OrderReleased { } impl OrderReleased { + /// Creates a new [`OrderReleased`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/events/order/submitted.rs b/nautilus_core/model/src/events/order/submitted.rs index 2b2566a9238..38a8618ea7d 100644 --- a/nautilus_core/model/src/events/order/submitted.rs +++ b/nautilus_core/model/src/events/order/submitted.rs @@ -55,6 +55,7 @@ pub struct OrderSubmitted { } impl OrderSubmitted { + /// Creates a new [`OrderSubmitted`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/events/order/triggered.rs b/nautilus_core/model/src/events/order/triggered.rs index 6d98b85a2a2..5b0ddae8514 100644 --- a/nautilus_core/model/src/events/order/triggered.rs +++ b/nautilus_core/model/src/events/order/triggered.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{nanos::UnixNanos, serialization::from_bool_as_u8, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -58,6 +58,7 @@ pub struct OrderTriggered { } impl OrderTriggered { + /// Creates a new [`OrderTriggered`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/events/order/updated.rs b/nautilus_core/model/src/events/order/updated.rs index d0eaba1e25a..f6830cc92c2 100644 --- a/nautilus_core/model/src/events/order/updated.rs +++ b/nautilus_core/model/src/events/order/updated.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{nanos::UnixNanos, serialization::from_bool_as_u8, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -61,6 +61,7 @@ pub struct OrderUpdated { } impl OrderUpdated { + /// Creates a new [`OrderUpdated`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/ffi/instruments/synthetic.rs b/nautilus_core/model/src/ffi/instruments/synthetic.rs index 19bef1a27a8..c9a9d28a296 100644 --- a/nautilus_core/model/src/ffi/instruments/synthetic.rs +++ b/nautilus_core/model/src/ffi/instruments/synthetic.rs @@ -33,7 +33,7 @@ use crate::{ types::price::{Price, ERROR_PRICE}, }; -/// Provides a C compatible Foreign Function Interface (FFI) for an underlying +/// C compatible Foreign Function Interface (FFI) for an underlying /// [`SyntheticInstrument`]. /// /// This struct wraps `SyntheticInstrument` in a way that makes it compatible with C function diff --git a/nautilus_core/model/src/ffi/mod.rs b/nautilus_core/model/src/ffi/mod.rs index c0a94de1cfc..49f3e6caa9d 100644 --- a/nautilus_core/model/src/ffi/mod.rs +++ b/nautilus_core/model/src/ffi/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides a C foreign function interface (FFI) from `cbindgen`. +//! C foreign function interface (FFI) from `cbindgen`. pub mod data; pub mod enums; diff --git a/nautilus_core/model/src/ffi/orderbook/book.rs b/nautilus_core/model/src/ffi/orderbook/book.rs index f7726e024d9..96a1372f9aa 100644 --- a/nautilus_core/model/src/ffi/orderbook/book.rs +++ b/nautilus_core/model/src/ffi/orderbook/book.rs @@ -36,7 +36,7 @@ use crate::{ types::{price::Price, quantity::Quantity}, }; -/// Provides a C compatible Foreign Function Interface (FFI) for an underlying `OrderBook`. +/// C compatible Foreign Function Interface (FFI) for an underlying `OrderBook`. /// /// This struct wraps `OrderBook` in a way that makes it compatible with C function /// calls, enabling interaction with `OrderBook` in a C environment. diff --git a/nautilus_core/model/src/ffi/orderbook/level.rs b/nautilus_core/model/src/ffi/orderbook/level.rs index 35cc3e32298..fba03259fef 100644 --- a/nautilus_core/model/src/ffi/orderbook/level.rs +++ b/nautilus_core/model/src/ffi/orderbook/level.rs @@ -24,7 +24,7 @@ use crate::{ types::price::Price, }; -/// Provides a C compatible Foreign Function Interface (FFI) for an underlying order book[`Level`]. +/// C compatible Foreign Function Interface (FFI) for an underlying order book[`Level`]. /// /// This struct wraps `Level` in a way that makes it compatible with C function /// calls, enabling interaction with `Level` in a C environment. diff --git a/nautilus_core/model/src/identifiers/account_id.rs b/nautilus_core/model/src/identifiers/account_id.rs index c1586d0930f..e2dcf75dbae 100644 --- a/nautilus_core/model/src/identifiers/account_id.rs +++ b/nautilus_core/model/src/identifiers/account_id.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Represents a valid account ID. + use std::{ fmt::{Debug, Display, Formatter}, hash::Hash, @@ -24,12 +26,6 @@ use ustr::Ustr; use super::venue::Venue; /// Represents a valid account ID. -/// -/// Must be correctly formatted with two valid strings either side of a hyphen '-'. -/// It is expected an account ID is the name of the issuer with an account number -/// separated by a hyphen. -/// -/// Example: "IB-D02851908". #[repr(C)] #[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] #[cfg_attr( @@ -39,11 +35,16 @@ use super::venue::Venue; pub struct AccountId(Ustr); impl AccountId { - /// Creates a new `AccountId` instance from the given identifier value. + /// Creates a new [`AccountId`] instance. + /// + /// Must be correctly formatted with two valid strings either side of a hyphen '-'. + /// It is expected an account ID is the name of the issuer with an account number + /// separated by a hyphen. /// + /// Example: "IB-D02851908". /// # Panics /// - /// Panics if the value is not a valid string, or does not contain a hyphen '-' separator. + /// Panics if `value` is not a valid string, or does not contain a hyphen '-' separator. pub fn new(value: &str) -> anyhow::Result { check_valid_string(value, stringify!(value))?; check_string_contains(value, "-", stringify!(value))?; diff --git a/nautilus_core/model/src/identifiers/client_id.rs b/nautilus_core/model/src/identifiers/client_id.rs index a0c680e08d6..0cb742ec52c 100644 --- a/nautilus_core/model/src/identifiers/client_id.rs +++ b/nautilus_core/model/src/identifiers/client_id.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Represents a system client ID. + use std::{ fmt::{Debug, Display, Formatter}, hash::Hash, @@ -31,11 +33,11 @@ use ustr::Ustr; pub struct ClientId(Ustr); impl ClientId { - /// Creates a new `ClientId` instance from the given identifier value. + /// Creates a new [`ClientId`] instance. /// /// # Panics /// - /// Panics if the value is not a valid string. + /// Panics if `value` is not a valid string. pub fn new(value: &str) -> anyhow::Result { check_valid_string(value, stringify!(value))?; diff --git a/nautilus_core/model/src/identifiers/client_order_id.rs b/nautilus_core/model/src/identifiers/client_order_id.rs index d99e6f199b3..2f323fbfa4c 100644 --- a/nautilus_core/model/src/identifiers/client_order_id.rs +++ b/nautilus_core/model/src/identifiers/client_order_id.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Represents a valid client order ID (assigned by the Nautilus system). + use std::{ fmt::{Debug, Display, Formatter}, hash::Hash, @@ -31,11 +33,11 @@ use ustr::Ustr; pub struct ClientOrderId(Ustr); impl ClientOrderId { - /// Creates a new `ClientOrderId` instance from the given identifier value. + /// Creates a new [`ClientOrderId`] instance. /// /// # Panics /// - /// Panics if the value is not a valid string. + /// Panics if `value` is not a valid string. pub fn new(value: &str) -> anyhow::Result { check_valid_string(value, stringify!(value))?; diff --git a/nautilus_core/model/src/identifiers/component_id.rs b/nautilus_core/model/src/identifiers/component_id.rs index 1212783c9eb..119c25d5cf5 100644 --- a/nautilus_core/model/src/identifiers/component_id.rs +++ b/nautilus_core/model/src/identifiers/component_id.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Represents a valid component ID. + use std::{ fmt::{Debug, Display, Formatter}, hash::Hash, @@ -31,11 +33,11 @@ use ustr::Ustr; pub struct ComponentId(Ustr); impl ComponentId { - /// Creates a new `ComponentId` instance from the given identifier value. + /// Creates a new [`ComponentId`] instance. /// /// # Panics /// - /// Panics if the value is not a valid string. + /// Panics if `value` is not a valid string. pub fn new(value: &str) -> anyhow::Result { check_valid_string(value, stringify!(value))?; diff --git a/nautilus_core/model/src/identifiers/exec_algorithm_id.rs b/nautilus_core/model/src/identifiers/exec_algorithm_id.rs index ecd57d6bd38..de79b2fbb69 100644 --- a/nautilus_core/model/src/identifiers/exec_algorithm_id.rs +++ b/nautilus_core/model/src/identifiers/exec_algorithm_id.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Represents a valid execution algorithm ID. + use std::{ fmt::{Debug, Display, Formatter}, hash::Hash, @@ -31,11 +33,11 @@ use ustr::Ustr; pub struct ExecAlgorithmId(Ustr); impl ExecAlgorithmId { - /// Creates a new `ExecAlgorithmId` instance from the given identifier value. + /// Creates a new [`ExecAlgorithmId`] instance. /// /// # Panics /// - /// Panics if the value is not a valid string. + /// Panics if `value` is not a valid string. pub fn new(value: &str) -> anyhow::Result { check_valid_string(value, stringify!(value))?; diff --git a/nautilus_core/model/src/identifiers/instrument_id.rs b/nautilus_core/model/src/identifiers/instrument_id.rs index 07fec16c60d..34789b9a8e8 100644 --- a/nautilus_core/model/src/identifiers/instrument_id.rs +++ b/nautilus_core/model/src/identifiers/instrument_id.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Represents a valid instrument ID. + use std::{ fmt::{Debug, Display, Formatter}, hash::Hash, @@ -40,7 +42,7 @@ pub struct InstrumentId { } impl InstrumentId { - /// Creates a new `InstrumentId` instance from the given `Symbol` and `Venue`. + /// Creates a new [`InstrumentId`] instance. #[must_use] pub fn new(symbol: Symbol, venue: Venue) -> Self { Self { symbol, venue } diff --git a/nautilus_core/model/src/identifiers/macros.rs b/nautilus_core/model/src/identifiers/macros.rs index 9b9194d15a7..a9372b4f8f9 100644 --- a/nautilus_core/model/src/identifiers/macros.rs +++ b/nautilus_core/model/src/identifiers/macros.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides macros for generating identifier functionality. + macro_rules! impl_serialization_for_identifier { ($ty:ty) => { impl Serialize for $ty { diff --git a/nautilus_core/model/src/identifiers/mod.rs b/nautilus_core/model/src/identifiers/mod.rs index 2439bb01993..52b0a2fd2d8 100644 --- a/nautilus_core/model/src/identifiers/mod.rs +++ b/nautilus_core/model/src/identifiers/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines identifiers for the trading domain models. +//! Identifiers for the trading domain model. use std::str::FromStr; diff --git a/nautilus_core/model/src/identifiers/order_list_id.rs b/nautilus_core/model/src/identifiers/order_list_id.rs index a136eb7b193..d59eb228c2e 100644 --- a/nautilus_core/model/src/identifiers/order_list_id.rs +++ b/nautilus_core/model/src/identifiers/order_list_id.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Represents a valid order list ID (assigned by the Nautilus system). + use std::{ fmt::{Debug, Display, Formatter}, hash::Hash, @@ -31,11 +33,11 @@ use ustr::Ustr; pub struct OrderListId(Ustr); impl OrderListId { - /// Creates a new `OrderListId` instance from the given identifier value. + /// Creates a new [`OrderListId`] instance. /// /// # Panics /// - /// Panics if the value is not a valid string. + /// Panics if `value` is not a valid string. pub fn new(value: &str) -> anyhow::Result { check_valid_string(value, stringify!(value))?; diff --git a/nautilus_core/model/src/identifiers/position_id.rs b/nautilus_core/model/src/identifiers/position_id.rs index 618b0ad7a14..3beeca15e3f 100644 --- a/nautilus_core/model/src/identifiers/position_id.rs +++ b/nautilus_core/model/src/identifiers/position_id.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Represents a valid position ID. + use std::{ fmt::{Debug, Display, Formatter}, hash::Hash, @@ -31,11 +33,11 @@ use ustr::Ustr; pub struct PositionId(Ustr); impl PositionId { - /// Creates a new `PositionId` instance from the given identifier value. + /// Creates a new [`PositionId`] instance. /// /// # Panics /// - /// Panics if the value is not a valid string. + /// Panics if `value` is not a valid string. pub fn new(value: &str) -> anyhow::Result { check_valid_string(value, stringify!(value))?; diff --git a/nautilus_core/model/src/identifiers/strategy_id.rs b/nautilus_core/model/src/identifiers/strategy_id.rs index 137a8063e3c..c062cd26156 100644 --- a/nautilus_core/model/src/identifiers/strategy_id.rs +++ b/nautilus_core/model/src/identifiers/strategy_id.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Represents a valid strategy ID. + use std::fmt::{Debug, Display, Formatter}; use nautilus_core::correctness::{check_string_contains, check_valid_string}; @@ -22,15 +24,6 @@ use ustr::Ustr; const EXTERNAL_STRATEGY_ID: &str = "EXTERNAL"; /// Represents a valid strategy ID. -/// -/// Must be correctly formatted with two valid strings either side of a hyphen. -/// It is expected a strategy ID is the class name of the strategy, -/// with an order ID tag number separated by a hyphen. -/// -/// Example: "EMACross-001". -/// -/// The reason for the numerical component of the ID is so that order and position IDs -/// do not collide with those from another strategy within the node instance. #[repr(C)] #[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] #[cfg_attr( @@ -40,11 +33,20 @@ const EXTERNAL_STRATEGY_ID: &str = "EXTERNAL"; pub struct StrategyId(Ustr); impl StrategyId { - /// Creates a new `StrategyId` instance from the given identifier value. + /// Creates a new [`StrategyId`] instance. + /// + /// Must be correctly formatted with two valid strings either side of a hyphen. + /// It is expected a strategy ID is the class name of the strategy, + /// with an order ID tag number separated by a hyphen. + /// + /// Example: "EMACross-001". + /// + /// The reason for the numerical component of the ID is so that order and position IDs + /// do not collide with those from another strategy within the node instance. /// /// # Panics /// - /// Panics if the value is not a valid string, or does not contain a hyphen '-' separator. + /// Panics if `value` is not a valid string, or does not contain a hyphen '-' separator. pub fn new(value: &str) -> anyhow::Result { check_valid_string(value, stringify!(value))?; if value != EXTERNAL_STRATEGY_ID { diff --git a/nautilus_core/model/src/identifiers/stubs.rs b/nautilus_core/model/src/identifiers/stubs.rs index bc45115fa25..bdf30048558 100644 --- a/nautilus_core/model/src/identifiers/stubs.rs +++ b/nautilus_core/model/src/identifiers/stubs.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Default implementations and fixture functions to provide stub identifiers for testing. + use nautilus_core::uuid::UUID4; use rstest::fixture; @@ -24,60 +26,70 @@ use crate::identifiers::{ }; impl Default for AccountId { + /// Creates a new default [`AccountId`] instance for testing. fn default() -> Self { Self::from("SIM-001") } } impl Default for ClientId { + /// Creates a new default [`ClientId`] instance for testing. fn default() -> Self { Self::from("SIM") } } impl Default for ClientOrderId { + /// Creates a new default [`ClientOrderId`] instance for testing. fn default() -> Self { Self::from("O-19700101-0000-000-001-1") } } impl Default for PositionId { + /// Creates a new default [`PositionId`] instance for testing. fn default() -> Self { Self::from("P-001") } } impl Default for StrategyId { + /// Creates a new default [`StrategyId`] instance for testing. fn default() -> Self { Self::from("S-001") } } impl Default for Symbol { + /// Creates a new default [`Symbol`] instance for testing. fn default() -> Self { Self::from("AUD/USD") } } impl Default for TradeId { + /// Creates a new default [`TradeId`] instance for testing. fn default() -> Self { Self::from("1") } } impl Default for TraderId { + /// Creates a new default [`TraderId`] instance for testing. fn default() -> Self { Self::from("TRADER-000") } } impl Default for Venue { + /// Creates a new default [`Venue`] instance for testing. fn default() -> Self { Self::from("SIM") } } impl Default for VenueOrderId { + /// Creates a new default [`VenueOrderId`] instance for testing. fn default() -> Self { Self::from("001") } diff --git a/nautilus_core/model/src/identifiers/symbol.rs b/nautilus_core/model/src/identifiers/symbol.rs index dacbb5d1051..2ad075483d9 100644 --- a/nautilus_core/model/src/identifiers/symbol.rs +++ b/nautilus_core/model/src/identifiers/symbol.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Represents a valid ticker symbol ID for a tradable instrument. + use std::{ fmt::{Debug, Display, Formatter}, hash::Hash, @@ -31,11 +33,11 @@ use ustr::Ustr; pub struct Symbol(Ustr); impl Symbol { - /// Creates a new `Symbol` instance from the given identifier value. + /// Creates a new [`Symbol`] instance. /// /// # Panics /// - /// Panics if the value is not a valid string. + /// Panics if `value` is not a valid string. pub fn new(value: &str) -> anyhow::Result { check_valid_string(value, stringify!(value))?; diff --git a/nautilus_core/model/src/identifiers/trade_id.rs b/nautilus_core/model/src/identifiers/trade_id.rs index 8ab6adbac51..1f6ff7d4805 100644 --- a/nautilus_core/model/src/identifiers/trade_id.rs +++ b/nautilus_core/model/src/identifiers/trade_id.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Represents a valid trade match ID (assigned by a trading venue). + use std::{ ffi::{CStr, CString}, fmt::{Debug, Display, Formatter}, @@ -27,11 +29,6 @@ const TRADE_ID_LEN: usize = 37; /// Represents a valid trade match ID (assigned by a trading venue). /// -/// Maximum length is 36 characters. -/// -/// 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)] @@ -45,11 +42,16 @@ pub struct TradeId { } impl TradeId { - /// Creates a new `TradeId` instance from the given identifier value. + /// Creates a new [`TradeId`] instance. + /// + /// Maximum length is 36 characters. + /// + /// The unique ID assigned to the trade entity once it is received or matched by + /// the exchange or central counterparty. /// /// # Panics /// - /// Panics if the value is not a valid string, or value length is greater than 36. + /// Panics if `value` is not a valid string, or value length is greater than 36. pub fn new(value: &str) -> anyhow::Result { let cstr = CString::new(value).expect("`CString` conversion failed"); Self::from_cstr(cstr) diff --git a/nautilus_core/model/src/identifiers/trader_id.rs b/nautilus_core/model/src/identifiers/trader_id.rs index 2b1de86aa54..5568fd22e2f 100644 --- a/nautilus_core/model/src/identifiers/trader_id.rs +++ b/nautilus_core/model/src/identifiers/trader_id.rs @@ -13,21 +13,14 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Represents a valid trader ID. + use std::fmt::{Debug, Display, Formatter}; use nautilus_core::correctness::{check_string_contains, check_valid_string}; use ustr::Ustr; /// Represents a valid trader ID. -/// -/// Must be correctly formatted with two valid strings either side of a hyphen. -/// It is expected a trader ID is the abbreviated name of the trader -/// with an order ID tag number separated by a hyphen. -/// -/// Example: "TESTER-001". - -/// The reason for the numerical component of the ID is so that order and position IDs -/// do not collide with those from another node instance. #[repr(C)] #[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] #[cfg_attr( @@ -37,11 +30,20 @@ use ustr::Ustr; pub struct TraderId(Ustr); impl TraderId { - /// Creates a new `TraderId` instance from the given identifier value. + /// Creates a new [`TraderId`] instance. + /// + /// Must be correctly formatted with two valid strings either side of a hyphen. + /// It is expected a trader ID is the abbreviated name of the trader + /// with an order ID tag number separated by a hyphen. + /// + /// Example: "TESTER-001". + /// + /// The reason for the numerical component of the ID is so that order and position IDs + /// do not collide with those from another node instance. /// /// # Panics /// - /// Panics if the value is not a valid string, or does not contain a hyphen '-' separator. + /// Panics if `value` is not a valid string, or does not contain a hyphen '-' separator. pub fn new(value: &str) -> anyhow::Result { check_valid_string(value, stringify!(value))?; check_string_contains(value, "-", stringify!(value))?; diff --git a/nautilus_core/model/src/identifiers/venue.rs b/nautilus_core/model/src/identifiers/venue.rs index 347a2763cc6..b0c7e882bdb 100644 --- a/nautilus_core/model/src/identifiers/venue.rs +++ b/nautilus_core/model/src/identifiers/venue.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Represents a valid trading venue ID. + use std::{ fmt::{Debug, Display, Formatter}, hash::Hash, @@ -35,11 +37,11 @@ pub const SYNTHETIC_VENUE: &str = "SYNTH"; pub struct Venue(Ustr); impl Venue { - /// Creates a new `Venue` instance from the given identifier value. + /// Creates a new [`Venue`] instance. /// /// # Panics /// - /// Panics if the value is not a valid string. + /// Panics if `value` is not a valid string. pub fn new(value: &str) -> anyhow::Result { check_valid_string(value, stringify!(value))?; diff --git a/nautilus_core/model/src/identifiers/venue_order_id.rs b/nautilus_core/model/src/identifiers/venue_order_id.rs index f1ce60d7152..ad3189dea78 100644 --- a/nautilus_core/model/src/identifiers/venue_order_id.rs +++ b/nautilus_core/model/src/identifiers/venue_order_id.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Represents a valid venue order ID (assigned by a trading venue). + use std::{ fmt::{Debug, Display, Formatter}, hash::Hash, @@ -31,11 +33,11 @@ use ustr::Ustr; pub struct VenueOrderId(Ustr); impl VenueOrderId { - /// Creates a new `VenueOrderId` instance from the given identifier value. + /// Creates a new [`VenueOrderId`] instance. /// /// # Panics /// - /// Panics if the value is not a valid string. + /// Panics if `value` is not a valid string. pub fn new(value: &str) -> anyhow::Result { check_valid_string(value, stringify!(value))?; diff --git a/nautilus_core/model/src/instruments/crypto_future.rs b/nautilus_core/model/src/instruments/crypto_future.rs index 1759275c315..2647b06a913 100644 --- a/nautilus_core/model/src/instruments/crypto_future.rs +++ b/nautilus_core/model/src/instruments/crypto_future.rs @@ -66,6 +66,7 @@ pub struct CryptoFuture { } impl CryptoFuture { + /// Creates a new [`CryptoFuture`] instance. #[allow(clippy::too_many_arguments)] pub fn new( id: InstrumentId, diff --git a/nautilus_core/model/src/instruments/crypto_perpetual.rs b/nautilus_core/model/src/instruments/crypto_perpetual.rs index 14996cfc813..0b033d73a10 100644 --- a/nautilus_core/model/src/instruments/crypto_perpetual.rs +++ b/nautilus_core/model/src/instruments/crypto_perpetual.rs @@ -65,6 +65,7 @@ pub struct CryptoPerpetual { } impl CryptoPerpetual { + /// Creates a new [`CryptoPerpetual`] instance. #[allow(clippy::too_many_arguments)] pub fn new( id: InstrumentId, diff --git a/nautilus_core/model/src/instruments/currency_pair.rs b/nautilus_core/model/src/instruments/currency_pair.rs index 46dec3a4066..10f5e70e58f 100644 --- a/nautilus_core/model/src/instruments/currency_pair.rs +++ b/nautilus_core/model/src/instruments/currency_pair.rs @@ -62,6 +62,7 @@ pub struct CurrencyPair { } impl CurrencyPair { + /// Creates a new [`CurrencyPair`] instance. #[allow(clippy::too_many_arguments)] pub fn new( id: InstrumentId, diff --git a/nautilus_core/model/src/instruments/equity.rs b/nautilus_core/model/src/instruments/equity.rs index f3365f6e015..2815636e8c5 100644 --- a/nautilus_core/model/src/instruments/equity.rs +++ b/nautilus_core/model/src/instruments/equity.rs @@ -59,6 +59,7 @@ pub struct Equity { } impl Equity { + /// Creates a new [`Equity`] instance. #[allow(clippy::too_many_arguments)] pub fn new( id: InstrumentId, diff --git a/nautilus_core/model/src/instruments/futures_contract.rs b/nautilus_core/model/src/instruments/futures_contract.rs index c4204fede10..75682413a95 100644 --- a/nautilus_core/model/src/instruments/futures_contract.rs +++ b/nautilus_core/model/src/instruments/futures_contract.rs @@ -66,6 +66,7 @@ pub struct FuturesContract { } impl FuturesContract { + /// Creates a new [`FuturesContract`] instance. #[allow(clippy::too_many_arguments)] pub fn new( id: InstrumentId, diff --git a/nautilus_core/model/src/instruments/futures_spread.rs b/nautilus_core/model/src/instruments/futures_spread.rs index f8f8940c9f3..ef90a3159e3 100644 --- a/nautilus_core/model/src/instruments/futures_spread.rs +++ b/nautilus_core/model/src/instruments/futures_spread.rs @@ -67,6 +67,7 @@ pub struct FuturesSpread { } impl FuturesSpread { + /// Creates a new [`FuturesSpread`] instance. #[allow(clippy::too_many_arguments)] pub fn new( id: InstrumentId, diff --git a/nautilus_core/model/src/instruments/mod.rs b/nautilus_core/model/src/instruments/mod.rs index 1e0dd634009..64b8ca02ded 100644 --- a/nautilus_core/model/src/instruments/mod.rs +++ b/nautilus_core/model/src/instruments/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines instrument definitions for the trading domain models. +//! Instrument definitions for the trading domain model. pub mod any; pub mod crypto_future; diff --git a/nautilus_core/model/src/instruments/options_contract.rs b/nautilus_core/model/src/instruments/options_contract.rs index 8233fe5d214..667fb5221e4 100644 --- a/nautilus_core/model/src/instruments/options_contract.rs +++ b/nautilus_core/model/src/instruments/options_contract.rs @@ -68,6 +68,7 @@ pub struct OptionsContract { } impl OptionsContract { + /// Creates a new [`OptionsContract`] instance. #[allow(clippy::too_many_arguments)] pub fn new( id: InstrumentId, diff --git a/nautilus_core/model/src/instruments/options_spread.rs b/nautilus_core/model/src/instruments/options_spread.rs index 3db07bb8744..e235bdda2d0 100644 --- a/nautilus_core/model/src/instruments/options_spread.rs +++ b/nautilus_core/model/src/instruments/options_spread.rs @@ -67,6 +67,7 @@ pub struct OptionsSpread { } impl OptionsSpread { + /// Creates a new [`OptionsSpread`] instance. #[allow(clippy::too_many_arguments)] pub fn new( id: InstrumentId, diff --git a/nautilus_core/model/src/instruments/stubs.rs b/nautilus_core/model/src/instruments/stubs.rs index 8bdb5ae42e7..dcf46e25d84 100644 --- a/nautilus_core/model/src/instruments/stubs.rs +++ b/nautilus_core/model/src/instruments/stubs.rs @@ -34,6 +34,7 @@ use crate::{ }; impl Default for SyntheticInstrument { + /// Creates a new default [`SyntheticInstrument`] instance for testing. fn default() -> Self { let btc_binance = InstrumentId::from("BTC.BINANCE"); let ltc_binance = InstrumentId::from("LTC.BINANCE"); diff --git a/nautilus_core/model/src/instruments/synthetic.rs b/nautilus_core/model/src/instruments/synthetic.rs index 139ec894589..cb2e48782e1 100644 --- a/nautilus_core/model/src/instruments/synthetic.rs +++ b/nautilus_core/model/src/instruments/synthetic.rs @@ -48,6 +48,7 @@ pub struct SyntheticInstrument { } impl SyntheticInstrument { + /// Creates a new [`SyntheticInstrument`] instance. pub fn new( symbol: Symbol, price_precision: u8, diff --git a/nautilus_core/model/src/orderbook/aggregation.rs b/nautilus_core/model/src/orderbook/aggregation.rs index aa85d7b49ca..dfa59d795fe 100644 --- a/nautilus_core/model/src/orderbook/aggregation.rs +++ b/nautilus_core/model/src/orderbook/aggregation.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Functions related to normalizing and processing top-of-book events. + use nautilus_core::nanos::UnixNanos; use super::{book::OrderBook, error::InvalidBookOperation}; diff --git a/nautilus_core/model/src/orderbook/analysis.rs b/nautilus_core/model/src/orderbook/analysis.rs index e411cce7486..250c7fed91c 100644 --- a/nautilus_core/model/src/orderbook/analysis.rs +++ b/nautilus_core/model/src/orderbook/analysis.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Functions related to order book analysis. + use std::collections::BTreeMap; use super::{book::OrderBook, ladder::BookPrice, level::Level}; diff --git a/nautilus_core/model/src/orderbook/book.rs b/nautilus_core/model/src/orderbook/book.rs index b7056d0bcc3..d1e86971b4f 100644 --- a/nautilus_core/model/src/orderbook/book.rs +++ b/nautilus_core/model/src/orderbook/book.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! A performant, generic, multi-purpose order book. + use nautilus_core::nanos::UnixNanos; use super::{aggregation::pre_process_order, analysis, display::pprint_book, level::Level}; @@ -26,12 +28,12 @@ use crate::{ types::{price::Price, quantity::Quantity}, }; -/// Provides an order book. +/// Provides a performant, generic, multi-purpose order book. /// /// Can handle the following granularity data: -/// - MBO (market by order) / L3 -/// - MBP (market by price) / L2 aggregated order per level -/// - MBP (market by price) / L1 top-of-book only +/// - MBO (market-by-order) / L3 +/// - MBP (market-by-price) / L2 aggregated order per level +/// - MBP (market-by-price) / L1 top-of-book only #[derive(Clone, Debug)] #[cfg_attr( feature = "python", @@ -53,6 +55,7 @@ pub struct OrderBook { } impl OrderBook { + /// Creates a new [`OrderBook`] instance. #[must_use] pub fn new(book_type: BookType, instrument_id: InstrumentId) -> Self { Self { diff --git a/nautilus_core/model/src/orderbook/display.rs b/nautilus_core/model/src/orderbook/display.rs index c84734ee92d..61bf6eb31e7 100644 --- a/nautilus_core/model/src/orderbook/display.rs +++ b/nautilus_core/model/src/orderbook/display.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Functions related to order book display. + use tabled::{settings::Style, Table, Tabled}; use super::{ladder::BookPrice, level::Level}; diff --git a/nautilus_core/model/src/orderbook/error.rs b/nautilus_core/model/src/orderbook/error.rs index 8bc78b163ec..316228229f6 100644 --- a/nautilus_core/model/src/orderbook/error.rs +++ b/nautilus_core/model/src/orderbook/error.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Errors associated with order book operations and integrity. + use nautilus_core::nanos::UnixNanos; use super::ladder::BookPrice; diff --git a/nautilus_core/model/src/orderbook/ladder.rs b/nautilus_core/model/src/orderbook/ladder.rs index 1bf717cea89..034aa97a12e 100644 --- a/nautilus_core/model/src/orderbook/ladder.rs +++ b/nautilus_core/model/src/orderbook/ladder.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Represents a ladder of price levels for one side of an order book. + use std::{ cmp::Ordering, collections::{BTreeMap, HashMap}, @@ -75,7 +77,7 @@ impl Display for BookPrice { } } -/// Represents one side of an order book as a ladder of price levels. +/// Represents a ladder of price levels for one side of an order book. #[derive(Clone, Debug)] pub struct Ladder { pub side: OrderSide, diff --git a/nautilus_core/model/src/orderbook/level.rs b/nautilus_core/model/src/orderbook/level.rs index 77e12ddf584..79e5a39d40a 100644 --- a/nautilus_core/model/src/orderbook/level.rs +++ b/nautilus_core/model/src/orderbook/level.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Represents a discrete price level in an order book. + use std::{cmp::Ordering, collections::BTreeMap}; use nautilus_core::nanos::UnixNanos; diff --git a/nautilus_core/model/src/orderbook/mod.rs b/nautilus_core/model/src/orderbook/mod.rs index d42aff96dbe..fe246463180 100644 --- a/nautilus_core/model/src/orderbook/mod.rs +++ b/nautilus_core/model/src/orderbook/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides a generic order book which can handle L1/L2/L3 data. +//! Order book components which can handle L1/L2/L3 data. pub mod aggregation; pub mod analysis; diff --git a/nautilus_core/model/src/orders/any.rs b/nautilus_core/model/src/orders/any.rs index 8caba511645..982a7dcd813 100644 --- a/nautilus_core/model/src/orders/any.rs +++ b/nautilus_core/model/src/orders/any.rs @@ -29,7 +29,7 @@ use super::{ trailing_stop_market::TrailingStopMarketOrder, }; use crate::{ - enums::{OrderSide, OrderSideSpecified, TriggerType}, + enums::{OrderSide, OrderSideSpecified, OrderStatus, TriggerType}, events::order::event::OrderEventAny, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, @@ -39,8 +39,9 @@ use crate::{ polymorphism::{ ApplyOrderEventAny, GetAccountId, GetClientOrderId, GetEmulationTrigger, GetExecAlgorithmId, GetExecSpawnId, GetInstrumentId, GetLimitPrice, GetOrderFilledQty, - GetOrderLeavesQty, GetOrderQuantity, GetOrderSide, GetOrderSideSpecified, GetPositionId, - GetStopPrice, GetStrategyId, GetTraderId, GetVenueOrderId, IsClosed, IsInflight, IsOpen, + GetOrderLeavesQty, GetOrderQuantity, GetOrderSide, GetOrderSideSpecified, GetOrderStatus, + GetPositionId, GetStopPrice, GetStrategyId, GetTraderId, GetVenueOrderId, IsClosed, + IsInflight, IsOpen, }, types::{price::Price, quantity::Quantity}, }; @@ -312,6 +313,22 @@ impl GetOrderQuantity for OrderAny { } } +impl GetOrderStatus for OrderAny { + fn status(&self) -> OrderStatus { + match self { + Self::Limit(order) => order.status, + Self::LimitIfTouched(order) => order.status, + Self::Market(order) => order.status, + Self::MarketIfTouched(order) => order.status, + Self::MarketToLimit(order) => order.status, + Self::StopLimit(order) => order.status, + Self::StopMarket(order) => order.status, + Self::TrailingStopLimit(order) => order.status, + Self::TrailingStopMarket(order) => order.status, + } + } +} + impl GetOrderFilledQty for OrderAny { fn filled_qty(&self) -> Quantity { match self { diff --git a/nautilus_core/model/src/orders/base.rs b/nautilus_core/model/src/orders/base.rs index 533a4fc43fd..a1d2ec5c70e 100644 --- a/nautilus_core/model/src/orders/base.rs +++ b/nautilus_core/model/src/orders/base.rs @@ -315,11 +315,16 @@ pub trait Order: 'static + Send { } fn is_inflight(&self) -> bool { - self.emulation_trigger().is_none() - && matches!( - self.status(), - OrderStatus::Submitted | OrderStatus::PendingCancel | OrderStatus::PendingUpdate - ) + if let Some(emulation_trigger) = self.emulation_trigger() { + if emulation_trigger != TriggerType::NoTrigger { + return false; + } + } + + matches!( + self.status(), + OrderStatus::Submitted | OrderStatus::PendingCancel | OrderStatus::PendingUpdate + ) } fn is_pending_update(&self) -> bool { @@ -331,8 +336,9 @@ pub trait Order: 'static + Send { } fn is_spawned(&self) -> bool { - self.exec_algorithm_id().is_some() - && self.exec_spawn_id().unwrap() != self.client_order_id() + self.exec_spawn_id().map_or(false, |exec_spawn_id| { + exec_spawn_id != self.client_order_id() + }) } } @@ -421,6 +427,7 @@ pub struct OrderCore { } impl OrderCore { + /// Creates a new [`OrderCore`] instance. pub fn new(init: OrderInitialized) -> anyhow::Result { let events: Vec = vec![OrderEventAny::Initialized(init.clone())]; Ok(Self { diff --git a/nautilus_core/model/src/orders/default.rs b/nautilus_core/model/src/orders/default.rs index 40900286e4a..2a93e51dbb3 100644 --- a/nautilus_core/model/src/orders/default.rs +++ b/nautilus_core/model/src/orders/default.rs @@ -30,8 +30,8 @@ use crate::{ types::{price::Price, quantity::Quantity}, }; -/// Provides a default [`LimitOrder`] used for testing. impl Default for LimitOrder { + /// Creates a new default [`LimitOrder`] instance for testing. fn default() -> Self { Self::new( TraderId::default(), @@ -64,8 +64,8 @@ impl Default for LimitOrder { } } -/// Provides a default [`LimitIfTouchedOrder`] used for testing. impl Default for LimitIfTouchedOrder { + /// Creates a new default [`LimitIfTouchedOrder`] instance for testing. fn default() -> Self { Self::new( TraderId::default(), @@ -100,8 +100,8 @@ impl Default for LimitIfTouchedOrder { } } -/// Provides a default [`MarketOrder`] used for testing. impl Default for MarketOrder { + /// Creates a new default [`MarketOrder`] instance for testing. fn default() -> Self { Self::new( TraderId::default(), @@ -128,8 +128,8 @@ impl Default for MarketOrder { } } -/// Provides a default [`MarketIfTouchedOrder`] used for testing. impl Default for MarketIfTouchedOrder { + /// Creates a new default [`MarketIfTouchedOrder`] instance for testing. fn default() -> Self { Self::new( TraderId::default(), @@ -162,8 +162,8 @@ impl Default for MarketIfTouchedOrder { } } -/// Provides a default [`MarketToLimitOrder`] used for testing. impl Default for MarketToLimitOrder { + /// Creates a new default [`MarketToLimitOrder`] instance for testing. fn default() -> Self { Self::new( TraderId::default(), @@ -193,8 +193,8 @@ impl Default for MarketToLimitOrder { } } -/// Provides a default [`StopLimitOrder`] used for testing. impl Default for StopLimitOrder { + /// Creates a new default [`StopLimitOrder`] instance for testing. fn default() -> Self { Self::new( TraderId::default(), @@ -229,8 +229,8 @@ impl Default for StopLimitOrder { } } -/// Provides a default [`StopMarketOrder`] used for testing. impl Default for StopMarketOrder { + /// Creates a new default [`StopMarketOrder`] instance for testing. fn default() -> Self { Self::new( TraderId::default(), @@ -263,8 +263,8 @@ impl Default for StopMarketOrder { } } -/// Provides a default [`TrailingStopLimitOrder`] used for testing. impl Default for TrailingStopLimitOrder { + /// Creates a new default [`TrailingStopLimitOrder`] instance for testing. fn default() -> Self { Self::new( TraderId::default(), @@ -302,8 +302,8 @@ impl Default for TrailingStopLimitOrder { } } -/// Provides a default [`TrailingStopMarketOrder`] used for testing. impl Default for TrailingStopMarketOrder { + /// Creates a new default [`TrailingStopMarketOrder`] instance for testing. fn default() -> Self { Self::new( TraderId::default(), diff --git a/nautilus_core/model/src/orders/limit.rs b/nautilus_core/model/src/orders/limit.rs index 84d0d325a53..b0924d8bb6e 100644 --- a/nautilus_core/model/src/orders/limit.rs +++ b/nautilus_core/model/src/orders/limit.rs @@ -61,6 +61,7 @@ pub struct LimitOrder { } impl LimitOrder { + /// Creates a new [`LimitOrder`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/orders/limit_if_touched.rs b/nautilus_core/model/src/orders/limit_if_touched.rs index e9a1a312017..67aaf493d7c 100644 --- a/nautilus_core/model/src/orders/limit_if_touched.rs +++ b/nautilus_core/model/src/orders/limit_if_touched.rs @@ -60,6 +60,7 @@ pub struct LimitIfTouchedOrder { } impl LimitIfTouchedOrder { + /// Creates a new [`LimitIfTouchedOrder`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/orders/list.rs b/nautilus_core/model/src/orders/list.rs index b98722b5a0a..cb114fe6a57 100644 --- a/nautilus_core/model/src/orders/list.rs +++ b/nautilus_core/model/src/orders/list.rs @@ -40,6 +40,7 @@ pub struct OrderList { } impl OrderList { + /// Creates a new [`OrderList`] instance. pub fn new( order_list_id: OrderListId, instrument_id: InstrumentId, diff --git a/nautilus_core/model/src/orders/market.rs b/nautilus_core/model/src/orders/market.rs index 456312f159b..788e45b7c57 100644 --- a/nautilus_core/model/src/orders/market.rs +++ b/nautilus_core/model/src/orders/market.rs @@ -56,6 +56,7 @@ pub struct MarketOrder { } impl MarketOrder { + /// Creates a new [`MarketOrder`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/orders/market_if_touched.rs b/nautilus_core/model/src/orders/market_if_touched.rs index b7bc900f984..7b70b919ff7 100644 --- a/nautilus_core/model/src/orders/market_if_touched.rs +++ b/nautilus_core/model/src/orders/market_if_touched.rs @@ -58,6 +58,7 @@ pub struct MarketIfTouchedOrder { } impl MarketIfTouchedOrder { + /// Creates a new [`MarketIfTouchedOrder`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/orders/market_to_limit.rs b/nautilus_core/model/src/orders/market_to_limit.rs index 2342c71f14d..263eafd6fd5 100644 --- a/nautilus_core/model/src/orders/market_to_limit.rs +++ b/nautilus_core/model/src/orders/market_to_limit.rs @@ -56,6 +56,7 @@ pub struct MarketToLimitOrder { } impl MarketToLimitOrder { + /// Creates a new [`MarketToLimitOrder`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/orders/mod.rs b/nautilus_core/model/src/orders/mod.rs index b0b48809ff4..6d1746a0478 100644 --- a/nautilus_core/model/src/orders/mod.rs +++ b/nautilus_core/model/src/orders/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines order types for the trading domain model. +//! Order types for the trading domain model. #![allow(dead_code)] diff --git a/nautilus_core/model/src/orders/stop_limit.rs b/nautilus_core/model/src/orders/stop_limit.rs index ba13d0167eb..3ef9ff22200 100644 --- a/nautilus_core/model/src/orders/stop_limit.rs +++ b/nautilus_core/model/src/orders/stop_limit.rs @@ -61,6 +61,7 @@ pub struct StopLimitOrder { } impl StopLimitOrder { + /// Creates a new [`StopLimitOrder`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/orders/stop_market.rs b/nautilus_core/model/src/orders/stop_market.rs index 4b9c91402d5..241de2acbd5 100644 --- a/nautilus_core/model/src/orders/stop_market.rs +++ b/nautilus_core/model/src/orders/stop_market.rs @@ -59,6 +59,7 @@ pub struct StopMarketOrder { } impl StopMarketOrder { + /// Creates a new [`StopMarketOrder`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/orders/trailing_stop_limit.rs b/nautilus_core/model/src/orders/trailing_stop_limit.rs index d51bd02dd68..99aa108e6e1 100644 --- a/nautilus_core/model/src/orders/trailing_stop_limit.rs +++ b/nautilus_core/model/src/orders/trailing_stop_limit.rs @@ -63,6 +63,7 @@ pub struct TrailingStopLimitOrder { } impl TrailingStopLimitOrder { + /// Creates a new [`TrailingStopLimitOrder`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/orders/trailing_stop_market.rs b/nautilus_core/model/src/orders/trailing_stop_market.rs index d7dbf5ba46f..ed9748cb4d6 100644 --- a/nautilus_core/model/src/orders/trailing_stop_market.rs +++ b/nautilus_core/model/src/orders/trailing_stop_market.rs @@ -61,6 +61,7 @@ pub struct TrailingStopMarketOrder { } impl TrailingStopMarketOrder { + /// Creates a new [`TrailingStopMarketOrder`] instance. #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, diff --git a/nautilus_core/model/src/polymorphism.rs b/nautilus_core/model/src/polymorphism.rs index 61ff2972fbf..19b303e4706 100644 --- a/nautilus_core/model/src/polymorphism.rs +++ b/nautilus_core/model/src/polymorphism.rs @@ -13,12 +13,12 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines traits to faciliate polymorphism. +//! Traits to faciliate polymorphism. use nautilus_core::nanos::UnixNanos; use crate::{ - enums::{OrderSide, OrderSideSpecified, TriggerType}, + enums::{OrderSide, OrderSideSpecified, OrderStatus, TriggerType}, events::order::event::OrderEventAny, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, @@ -77,6 +77,10 @@ pub trait GetOrderQuantity { fn quantity(&self) -> Quantity; } +pub trait GetOrderStatus { + fn status(&self) -> OrderStatus; +} + pub trait GetOrderFilledQty { fn filled_qty(&self) -> Quantity; } diff --git a/nautilus_core/model/src/position.rs b/nautilus_core/model/src/position.rs index efc51bec401..ccedca93238 100644 --- a/nautilus_core/model/src/position.rs +++ b/nautilus_core/model/src/position.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines a `Position` for the trading domain model. +//! A `Position` for the trading domain model. use std::{ collections::{HashMap, HashSet}, diff --git a/nautilus_core/model/src/python/data/mod.rs b/nautilus_core/model/src/python/data/mod.rs index a9293ad58f9..61a77d3eb0f 100644 --- a/nautilus_core/model/src/python/data/mod.rs +++ b/nautilus_core/model/src/python/data/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines data types for the trading domain model. +//! Data types for the trading domain model. pub mod bar; pub mod delta; diff --git a/nautilus_core/model/src/python/enums.rs b/nautilus_core/model/src/python/enums.rs index 0fbce073b17..fbca1f2fdfd 100644 --- a/nautilus_core/model/src/python/enums.rs +++ b/nautilus_core/model/src/python/enums.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines enumerations for the trading domain model. +//! Enumerations for the trading domain model. use std::str::FromStr; diff --git a/nautilus_core/model/src/python/events/mod.rs b/nautilus_core/model/src/python/events/mod.rs index 816b152aa56..1e86eda35b5 100644 --- a/nautilus_core/model/src/python/events/mod.rs +++ b/nautilus_core/model/src/python/events/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines events for the trading domain model. +//! Events for the trading domain model. pub mod account; pub mod order; diff --git a/nautilus_core/model/src/python/identifiers/mod.rs b/nautilus_core/model/src/python/identifiers/mod.rs index d652eceaa48..f891e5e96b0 100644 --- a/nautilus_core/model/src/python/identifiers/mod.rs +++ b/nautilus_core/model/src/python/identifiers/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines identifiers the trading domain model. +//! Identifiers for the trading domain model. use std::str::FromStr; diff --git a/nautilus_core/model/src/python/instruments/mod.rs b/nautilus_core/model/src/python/instruments/mod.rs index a3dc1560ddf..9a1c6f05d49 100644 --- a/nautilus_core/model/src/python/instruments/mod.rs +++ b/nautilus_core/model/src/python/instruments/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines instrument definitions the trading domain model. +//! Instrument definitions the trading domain model. use nautilus_core::python::to_pyvalue_err; use pyo3::{IntoPy, PyObject, PyResult, Python}; diff --git a/nautilus_core/model/src/python/types/mod.rs b/nautilus_core/model/src/python/types/mod.rs index 1fa3ba70755..8310e7fde6c 100644 --- a/nautilus_core/model/src/python/types/mod.rs +++ b/nautilus_core/model/src/python/types/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines value types such as `Price`, `Quantity` and `Money` for the trading domain model. +//! Value types such as `Price`, `Quantity` and `Money` for the trading domain model. pub mod balance; pub mod currency; diff --git a/nautilus_core/model/src/stubs.rs b/nautilus_core/model/src/stubs.rs index 107498b9b56..43e619a27e5 100644 --- a/nautilus_core/model/src/stubs.rs +++ b/nautilus_core/model/src/stubs.rs @@ -154,7 +154,7 @@ pub fn stub_order_book_mbp( OrderSide::Buy, price, size, - 0, // order_id not applicable for MBP (market by price) books + 0, // order_id not applicable for MBP (market-by-price) books ); book.add(order, 0, 1, 2.into()); } @@ -175,7 +175,7 @@ pub fn stub_order_book_mbp( OrderSide::Sell, price, size, - 0, // order_id not applicable for MBP (market by price) books + 0, // order_id not applicable for MBP (market-by-price) books ); book.add(order, 0, 1, 2.into()); } diff --git a/nautilus_core/model/src/types/currency.rs b/nautilus_core/model/src/types/currency.rs index 257ae565204..04e1a4272b2 100644 --- a/nautilus_core/model/src/types/currency.rs +++ b/nautilus_core/model/src/types/currency.rs @@ -41,6 +41,7 @@ pub struct Currency { } impl Currency { + /// Creates a new [`Currency`] instance. pub fn new( code: &str, precision: u8, diff --git a/nautilus_core/model/src/types/mod.rs b/nautilus_core/model/src/types/mod.rs index 3d34dffa31c..830d7e86e54 100644 --- a/nautilus_core/model/src/types/mod.rs +++ b/nautilus_core/model/src/types/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines value types for the trading domain model such as `Price`, `Quantity` and `Money`. +//! Value types for the trading domain model such as `Price`, `Quantity` and `Money`. pub mod balance; pub mod currency; diff --git a/nautilus_core/model/src/types/money.rs b/nautilus_core/model/src/types/money.rs index e9f07ccd094..fdc28944f76 100644 --- a/nautilus_core/model/src/types/money.rs +++ b/nautilus_core/model/src/types/money.rs @@ -47,6 +47,7 @@ pub struct Money { } impl Money { + /// Creates a new [`Money`] instance. pub fn new(amount: f64, currency: Currency) -> anyhow::Result { check_in_range_inclusive_f64(amount, MONEY_MIN, MONEY_MAX, "amount")?; diff --git a/nautilus_core/model/src/types/quantity.rs b/nautilus_core/model/src/types/quantity.rs index 00665c17c6b..cf67272ceb9 100644 --- a/nautilus_core/model/src/types/quantity.rs +++ b/nautilus_core/model/src/types/quantity.rs @@ -44,6 +44,7 @@ pub struct Quantity { } impl Quantity { + /// Creates a new [`Quantity`] instance. pub fn new(value: f64, precision: u8) -> anyhow::Result { check_in_range_inclusive_f64(value, QUANTITY_MIN, QUANTITY_MAX, "value")?; check_fixed_precision(precision)?; diff --git a/nautilus_core/network/src/http.rs b/nautilus_core/network/src/http.rs index 2116c7d60de..dc0ea9db1a4 100644 --- a/nautilus_core/network/src/http.rs +++ b/nautilus_core/network/src/http.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides a high-performance HTTP client implementation. +//! A high-performance HTTP client implementation. use std::{ collections::{hash_map::DefaultHasher, HashMap}, @@ -31,7 +31,7 @@ use tracing::trace; use crate::ratelimiter::{clock::MonotonicClock, quota::Quota, RateLimiter}; -/// Provides a high-performance `HttpClient` for HTTP requests. +/// A high-performance `HttpClient` for HTTP requests. /// /// The client is backed by a hyper Client which keeps connections alive and /// can be cloned cheaply. The client also has a list of header fields to @@ -148,6 +148,7 @@ pub struct HttpResponse { } impl Default for InnerHttpClient { + /// Creates a new default [`InnerHttpClient`] instance. fn default() -> Self { let client = reqwest::Client::new(); Self { diff --git a/nautilus_core/network/src/ratelimiter/gcra.rs b/nautilus_core/network/src/ratelimiter/gcra.rs index dd014dafbe5..601f8d61bc0 100644 --- a/nautilus_core/network/src/ratelimiter/gcra.rs +++ b/nautilus_core/network/src/ratelimiter/gcra.rs @@ -34,6 +34,7 @@ pub struct StateSnapshot { } impl StateSnapshot { + /// Creates a new [`StateSnapshot`] instance. #[inline] pub(crate) fn new(t: Nanos, tau: Nanos, time_of_measurement: Nanos, tat: Nanos) -> Self { Self { diff --git a/nautilus_core/network/src/socket.rs b/nautilus_core/network/src/socket.rs index 9db1bfbde90..ce804e4eba7 100644 --- a/nautilus_core/network/src/socket.rs +++ b/nautilus_core/network/src/socket.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides a high-performance raw TCP client implementation with TLS capability. +//! A high-performance raw TCP client implementation with TLS capability. use std::{sync::Arc, time::Duration}; diff --git a/nautilus_core/network/src/websocket.rs b/nautilus_core/network/src/websocket.rs index a0dda5dca21..11c95572e0f 100644 --- a/nautilus_core/network/src/websocket.rs +++ b/nautilus_core/network/src/websocket.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides a high-performance WebSocket client implementation. +//! A high-performance WebSocket client implementation. use std::{str::FromStr, sync::Arc, time::Duration}; diff --git a/nautilus_core/network/tokio-tungstenite/CHANGELOG.md b/nautilus_core/network/tokio-tungstenite/CHANGELOG.md index 9002abbafca..a07463c8766 100644 --- a/nautilus_core/network/tokio-tungstenite/CHANGELOG.md +++ b/nautilus_core/network/tokio-tungstenite/CHANGELOG.md @@ -13,7 +13,7 @@ - Make `Origin` header case-sensitive (to keep compatibility with poorely-written servers that don't accept lowercase `Origin` header). - Make semantics of the reading form the `WebSocketStream` more reasonable (return `None` instead of an error when the stream is normally closed). -- Imrpove the way `poll_close()` works by properly driving the close of the stream till completion. +- Improve the way `poll_close()` works by properly driving the close of the stream till completion. # 0.17.1 diff --git a/nautilus_core/persistence/src/backend/kmerge_batch.rs b/nautilus_core/persistence/src/backend/kmerge_batch.rs index 7e9834e7625..bfc14d55e20 100644 --- a/nautilus_core/persistence/src/backend/kmerge_batch.rs +++ b/nautilus_core/persistence/src/backend/kmerge_batch.rs @@ -107,6 +107,7 @@ where I: Iterator>, C: Compare>, { + /// Creates a new [`KMerge`] instance. pub fn new(cmp: C) -> Self { Self { heap: BinaryHeap::from_vec_cmp(Vec::new(), cmp), diff --git a/nautilus_core/persistence/src/backend/session.rs b/nautilus_core/persistence/src/backend/session.rs index 5ec6ba3a243..8360877e593 100644 --- a/nautilus_core/persistence/src/backend/session.rs +++ b/nautilus_core/persistence/src/backend/session.rs @@ -64,6 +64,7 @@ pub struct DataBackendSession { } impl DataBackendSession { + /// Creates a new [`DataBackendSession`] instance. #[must_use] pub fn new(chunk_size: usize) -> Self { let runtime = tokio::runtime::Builder::new_multi_thread() @@ -183,6 +184,7 @@ pub struct DataQueryResult { } impl DataQueryResult { + /// Creates a new [`DataQueryResult`] instance. #[must_use] pub fn new(result: QueryResult, size: usize) -> Self { Self { diff --git a/nautilus_trader/adapters/binance/futures/providers.py b/nautilus_trader/adapters/binance/futures/providers.py index 6ac22213169..5e5f1d6db9f 100644 --- a/nautilus_trader/adapters/binance/futures/providers.py +++ b/nautilus_trader/adapters/binance/futures/providers.py @@ -251,8 +251,8 @@ def _parse_instrument( BinanceSymbolFilterType.MIN_NOTIONAL, ) - tick_size = price_filter.tickSize.rstrip("0") - step_size = lot_size_filter.stepSize.rstrip("0") + tick_size = price_filter.tickSize + step_size = lot_size_filter.stepSize PyCondition.in_range(float(tick_size), PRICE_MIN, PRICE_MAX, "tick_size") PyCondition.in_range(float(step_size), QUANTITY_MIN, QUANTITY_MAX, "step_size") diff --git a/nautilus_trader/adapters/binance/spot/providers.py b/nautilus_trader/adapters/binance/spot/providers.py index 76631cdd3f9..1595476d593 100644 --- a/nautilus_trader/adapters/binance/spot/providers.py +++ b/nautilus_trader/adapters/binance/spot/providers.py @@ -237,8 +237,8 @@ def _parse_instrument( ) # market_lot_size_filter = symbol_filters.get("MARKET_LOT_SIZE") - tick_size = price_filter.tickSize.rstrip("0") - step_size = lot_size_filter.stepSize.rstrip("0") + tick_size = price_filter.tickSize + step_size = lot_size_filter.stepSize PyCondition.in_range(float(tick_size), PRICE_MIN, PRICE_MAX, "tick_size") PyCondition.in_range(float(step_size), QUANTITY_MIN, QUANTITY_MAX, "step_size") diff --git a/nautilus_trader/adapters/binance/websocket/client.py b/nautilus_trader/adapters/binance/websocket/client.py index 24e9d6c391d..152e5635506 100644 --- a/nautilus_trader/adapters/binance/websocket/client.py +++ b/nautilus_trader/adapters/binance/websocket/client.py @@ -496,7 +496,7 @@ async def _subscribe_all(self) -> None: async def _unsubscribe(self, stream: str) -> None: if stream not in self._streams: - self._log.warning(f"Cannot unsubscribe from {stream}: never subscribed") + self._log.warning(f"Cannot unsubscribe from {stream}: not subscribed") return # Not subscribed self._streams.remove(stream) diff --git a/nautilus_trader/adapters/bybit/common/parsing.py b/nautilus_trader/adapters/bybit/common/parsing.py index 5e5eef82eaa..0fcdb932c55 100644 --- a/nautilus_trader/adapters/bybit/common/parsing.py +++ b/nautilus_trader/adapters/bybit/common/parsing.py @@ -47,6 +47,7 @@ def parse_bybit_delta( ts_event: int, ts_init: int, is_snapshot: bool, + flags: int = 0, ) -> OrderBookDelta: price = values[0] size = values[1] @@ -64,7 +65,7 @@ def parse_bybit_delta( size=size, order_id=update_id, ), - flags=0, # Not applicable + flags=flags, sequence=sequence, ts_event=ts_event, ts_init=ts_init, diff --git a/nautilus_trader/adapters/bybit/data.py b/nautilus_trader/adapters/bybit/data.py index c4e07b0f8d8..9af05a415d4 100644 --- a/nautilus_trader/adapters/bybit/data.py +++ b/nautilus_trader/adapters/bybit/data.py @@ -291,14 +291,15 @@ async def _subscribe_order_book_deltas( if instrument_id in self._tob_quotes: if depth == 1: - self._log.debug( + self._log.warning( f"Already subscribed to {instrument_id} top-of-book", LogColor.MAGENTA, ) return # Already subscribed - raise RuntimeError( - "Cannot subscribe to both top-of-book quotes and order book", - ) + + if instrument_id in self._depths: + self._log.warning(f"Already subscribed to {instrument_id} order book deltas") + return self._depths[instrument_id] = depth ws_client = self._ws_clients[bybit_symbol.product_type] @@ -576,7 +577,7 @@ def _handle_ws_message(self, product_type: BybitProductType, raw: bytes) -> None return if "orderbook" in ws_message.topic: - self._handle_orderbook(product_type, raw) + self._handle_orderbook(product_type, raw, ws_message.topic) elif "publicTrade" in ws_message.topic: self._handle_trade(product_type, raw) elif "tickers" in ws_message.topic: @@ -588,7 +589,7 @@ def _handle_ws_message(self, product_type: BybitProductType, raw: bytes) -> None except Exception as e: self._log.error(f"Failed to parse websocket message: {raw.decode()} with error {e}") - def _handle_orderbook(self, product_type: BybitProductType, raw: bytes) -> None: + def _handle_orderbook(self, product_type: BybitProductType, raw: bytes, topic: str) -> None: msg = self._decoder_ws_orderbook.decode(raw) symbol = msg.data.s + f"-{product_type.value.upper()}" instrument_id: InstrumentId = self._get_cached_instrument_id(symbol) @@ -598,7 +599,7 @@ def _handle_orderbook(self, product_type: BybitProductType, raw: bytes) -> None: self._log.error(f"Cannot parse order book data: no instrument for {instrument_id}") return - if instrument_id in self._tob_quotes: + if instrument_id in self._tob_quotes and topic.startswith("orderbook.1."): quote = msg.data.parse_to_quote_tick( instrument_id=instrument_id, last_quote=self._last_quotes.get(instrument_id), diff --git a/nautilus_trader/adapters/bybit/schemas/ws.py b/nautilus_trader/adapters/bybit/schemas/ws.py index 4e5d11c02fa..b180a755973 100644 --- a/nautilus_trader/adapters/bybit/schemas/ws.py +++ b/nautilus_trader/adapters/bybit/schemas/ws.py @@ -40,6 +40,7 @@ from nautilus_trader.model.data import TradeTick from nautilus_trader.model.enums import AggressorSide from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.enums import RecordFlag from nautilus_trader.model.identifiers import AccountId from nautilus_trader.model.identifiers import ClientOrderId from nautilus_trader.model.identifiers import InstrumentId @@ -172,8 +173,17 @@ def parse_to_snapshot( ts_init=ts_init, ) deltas.append(clear) + num_bids_raw = len(bids_raw) + num_asks_raw = len(asks_raw) + + for bid_id, bid in enumerate(bids_raw): + flags = 0 + + if bid_id == num_bids_raw - 1 and num_asks_raw == 0: + # F_LAST, 1 << 7 + # Last message in the packet from the venue for a given `instrument_id` + flags = RecordFlag.F_LAST - for bid in bids_raw: delta = parse_bybit_delta( instrument_id=instrument_id, values=bid, @@ -183,10 +193,18 @@ def parse_to_snapshot( ts_event=ts_event, ts_init=ts_init, is_snapshot=True, + flags=flags, ) deltas.append(delta) - for ask in asks_raw: + for ask_id, ask in enumerate(asks_raw): + flags = 0 + + if ask_id == num_asks_raw - 1: + # F_LAST, 1 << 7 + # Last message in the packet from the venue for a given `instrument_id` + flags = RecordFlag.F_LAST + delta = parse_bybit_delta( instrument_id=instrument_id, values=ask, @@ -196,6 +214,7 @@ def parse_to_snapshot( ts_event=ts_event, ts_init=ts_init, is_snapshot=True, + flags=flags, ) deltas.append(delta) @@ -224,8 +243,17 @@ def parse_to_deltas( for d in self.a ] deltas: list[OrderBookDelta] = [] + num_bids_raw = len(bids_raw) + num_asks_raw = len(asks_raw) + + for bid_id, bid in enumerate(bids_raw): + flags = 0 + + if bid_id == num_bids_raw - 1 and num_asks_raw == 0: + # F_LAST, 1 << 7 + # Last message in the packet from the venue for a given `instrument_id` + flags = RecordFlag.F_LAST - for bid in bids_raw: delta = parse_bybit_delta( instrument_id=instrument_id, values=bid, @@ -235,11 +263,18 @@ def parse_to_deltas( ts_event=ts_event, ts_init=ts_init, is_snapshot=False, + flags=flags, ) deltas.append(delta) - deltas.append(delta) - for ask in asks_raw: + for ask_id, ask in enumerate(asks_raw): + flags = 0 + + if ask_id == num_asks_raw - 1: + # F_LAST, 1 << 7 + # Last message in the packet from the venue for a given `instrument_id` + flags = RecordFlag.F_LAST + delta = parse_bybit_delta( instrument_id=instrument_id, values=ask, @@ -249,6 +284,7 @@ def parse_to_deltas( ts_event=ts_event, ts_init=ts_init, is_snapshot=False, + flags=flags, ) deltas.append(delta) diff --git a/nautilus_trader/adapters/sandbox/execution.py b/nautilus_trader/adapters/sandbox/execution.py index 47145542ad1..d8772c5df1d 100644 --- a/nautilus_trader/adapters/sandbox/execution.py +++ b/nautilus_trader/adapters/sandbox/execution.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- import asyncio -from typing import ClassVar import pandas as pd @@ -47,7 +46,6 @@ from nautilus_trader.model.identifiers import InstrumentId from nautilus_trader.model.identifiers import Venue from nautilus_trader.model.identifiers import VenueOrderId -from nautilus_trader.model.instruments import Instrument from nautilus_trader.model.objects import Currency from nautilus_trader.model.objects import Money from nautilus_trader.portfolio.base import PortfolioFacade @@ -72,8 +70,6 @@ class SandboxExecutionClient(LiveExecutionClient): """ - INSTRUMENTS: ClassVar[list[Instrument]] = [] - def __init__( self, loop: asyncio.AbstractEventLoop, @@ -111,7 +107,6 @@ def __init__( base_currency=base_currency, default_leverage=config.default_leverage, leverages=config.leverages or {}, - instruments=self.INSTRUMENTS, modules=[], portfolio=portfolio, msgbus=self._msgbus, @@ -129,7 +124,9 @@ def __init__( use_position_ids=config.use_position_ids, use_random_ids=config.use_random_ids, use_reduce_only=config.use_reduce_only, + use_message_queue=False, # Do not use internal message queue for real-time ) + self._client = BacktestExecClient( exchange=self.exchange, msgbus=msgbus, @@ -145,6 +142,11 @@ def connect(self) -> None: """ self._log.info("Connecting...") self._msgbus.subscribe("data.*", handler=self.on_data) + + # Load all instruments for venue + for instrument in self.exchange.cache.instruments(venue=self.venue): + self.exchange.add_instrument(instrument) + self._client._set_connected(True) self._set_connected(True) self._log.info("Connected") diff --git a/nautilus_trader/backtest/engine.pyx b/nautilus_trader/backtest/engine.pyx index 96ccc1b5beb..b836d853497 100644 --- a/nautilus_trader/backtest/engine.pyx +++ b/nautilus_trader/backtest/engine.pyx @@ -465,7 +465,6 @@ cdef class BacktestEngine: base_currency=base_currency, default_leverage=default_leverage, leverages=leverages or {}, - instruments=[], modules=modules, portfolio=self.kernel.portfolio, msgbus=self.kernel.msgbus, diff --git a/nautilus_trader/backtest/exchange.pxd b/nautilus_trader/backtest/exchange.pxd index e9049dc3c0c..a1221699dd7 100644 --- a/nautilus_trader/backtest/exchange.pxd +++ b/nautilus_trader/backtest/exchange.pxd @@ -95,6 +95,8 @@ cdef class SimulatedExchange: """If venue order and position IDs will be randomly generated UUID4s.\n\n:returns: `bool`""" cdef readonly bint use_reduce_only """If the `reduce_only` option on orders will be honored.\n\n:returns: `bool`""" + cdef readonly bint use_message_queue + """If an internal message queue is being used to sequentially process incoming trading commands.\n\n:returns: `bool`""" cdef readonly list modules """The simulation modules registered with the exchange.\n\n:returns: `list[SimulationModule]`""" cdef readonly dict instruments @@ -141,6 +143,8 @@ cdef class SimulatedExchange: cpdef void process(self, uint64_t ts_now) cpdef void reset(self) + cdef void _process_trading_command(self, TradingCommand command) + # -- EVENT GENERATORS ----------------------------------------------------------------------------- cdef void _generate_fresh_account_state(self) diff --git a/nautilus_trader/backtest/exchange.pyx b/nautilus_trader/backtest/exchange.pyx index 1b27f5a576d..8e21fd4eebe 100644 --- a/nautilus_trader/backtest/exchange.pyx +++ b/nautilus_trader/backtest/exchange.pyx @@ -82,20 +82,22 @@ cdef class SimulatedExchange: The account default leverage (for margin accounts). leverages : dict[InstrumentId, Decimal] The instrument specific leverage configuration (for margin accounts). + modules : list[SimulatedModule] + The simulation modules for the exchange. portfolio : PortfolioFacade The read-only portfolio for the exchange. msgbus : MessageBus The message bus for the exchange. cache : CacheFacade The read-only cache for the exchange. + clock : TestClock + The clock for the exchange. fill_model : FillModel The fill model for the exchange. fee_model : FeeModel The fee model for the exchange. latency_model : LatencyModel, optional The latency model for the exchange. - clock : TestClock - The clock for the exchange. book_type : BookType The order book type for the exchange. frozen_account : bool, default False @@ -105,16 +107,21 @@ cdef class SimulatedExchange: reject_stop_orders : bool, default True If stop orders are rejected on submission if in the market. support_gtd_orders : bool, default True - If orders with GTD time in force will be supported by the venue. + If orders with GTD time in force will be supported by the exchange. support_contingent_orders : bool, default True - If contingent orders will be supported/respected by the venue. + If contingent orders will be supported/respected by the exchange. If False then its expected the strategy will be managing any contingent orders. use_position_ids : bool, default True If venue position IDs will be generated on order fills. use_random_ids : bool, default False - If all venue generated identifiers will be random UUID4's. + If all exchange generated identifiers will be random UUID4's. use_reduce_only : bool, default True If the `reduce_only` execution instruction on orders will be honored. + use_message_queue : bool, default True + If an internal message queue should be used to process trading commands in sequence after + they have initially arrived. Setting this to False would be appropriate for real-time + sandbox environments, where we don't want to introduce additional latency of waiting for + the next data event before processing the trading command. Raises ------ @@ -130,6 +137,7 @@ cdef class SimulatedExchange: If `base_currency` and multiple starting balances. ValueError If `modules` contains a type other than `SimulationModule`. + """ def __init__( @@ -141,7 +149,6 @@ cdef class SimulatedExchange: Currency base_currency: Currency | None, default_leverage not None: Decimal, leverages not None: dict[InstrumentId, Decimal], - list instruments not None, list modules not None, PortfolioFacade portfolio not None, MessageBus msgbus not None, @@ -159,8 +166,8 @@ cdef class SimulatedExchange: bint use_position_ids = True, bint use_random_ids = False, bint use_reduce_only = True, + bint use_message_queue = True, ) -> None: - Condition.list_type(instruments, Instrument, "instruments", "Instrument") Condition.not_empty(starting_balances, "starting_balances") Condition.list_type(starting_balances, Money, "starting_balances") Condition.list_type(modules, SimulationModule, "modules", "SimulationModule") @@ -197,6 +204,7 @@ cdef class SimulatedExchange: self.use_position_ids = use_position_ids self.use_random_ids = use_random_ids self.use_reduce_only = use_reduce_only + self.use_message_queue = use_message_queue self.fill_model = fill_model self.fee_model = fee_model self.latency_model = latency_model @@ -216,16 +224,12 @@ cdef class SimulatedExchange: self._log.info(f"Loaded {module}") # Markets - self._matching_engines: dict[InstrumentId, OrderMatchingEngine] = {} - - # Load instruments self.instruments: dict[InstrumentId, Instrument] = {} - for instrument in instruments: - self.add_instrument(instrument) + self._matching_engines: dict[InstrumentId, OrderMatchingEngine] = {} self._message_queue = deque() self._inflight_queue: list[tuple[(uint64_t, uint64_t), TradingCommand]] = [] - self._inflight_counter: dict[uint64_t, int] = {} + self._inflight_counter: dict[uint64_t, uint64_t] = {} def __repr__(self) -> str: return ( @@ -294,14 +298,13 @@ cdef class SimulatedExchange: cpdef void initialize_account(self): """ Initialize the account to the starting balances. + """ self._generate_fresh_account_state() cpdef void add_instrument(self, Instrument instrument): """ - Add the given instrument to the venue. - - A random and unique 32-bit unsigned integer raw ID will be generated. + Add the given instrument to the exchange. Parameters ---------- @@ -621,7 +624,9 @@ cdef class SimulatedExchange: """ Condition.not_none(command, "command") - if self.latency_model is None: + if not self.use_message_queue: + self._process_trading_command(command) + elif self.latency_model is None: self._message_queue.appendleft(command) else: heappush(self._inflight_queue, self.generate_inflight_command(command)) @@ -660,7 +665,11 @@ cdef class SimulatedExchange: cdef OrderMatchingEngine matching_engine = self._matching_engines.get(delta.instrument_id) if matching_engine is None: - raise RuntimeError(f"No matching engine found for {delta.instrument_id}") + instrument = self.cache.instrument(delta.instrument_id) + if instrument is None: + raise RuntimeError(f"No matching engine found for {delta.instrument_id}") + self.add_instrument(instrument) + matching_engine = self._matching_engines[delta.instrument_id] matching_engine.process_order_book_delta(delta) @@ -682,7 +691,11 @@ cdef class SimulatedExchange: cdef OrderMatchingEngine matching_engine = self._matching_engines.get(deltas.instrument_id) if matching_engine is None: - raise RuntimeError(f"No matching engine found for {deltas.instrument_id}") + instrument = self.cache.instrument(deltas.instrument_id) + if instrument is None: + raise RuntimeError(f"No matching engine found for {deltas.instrument_id}") + self.add_instrument(instrument) + matching_engine = self._matching_engines[deltas.instrument_id] matching_engine.process_order_book_deltas(deltas) @@ -706,7 +719,11 @@ cdef class SimulatedExchange: cdef OrderMatchingEngine matching_engine = self._matching_engines.get(tick.instrument_id) if matching_engine is None: - raise RuntimeError(f"No matching engine found for {tick.instrument_id}") + instrument = self.cache.instrument(tick.instrument_id) + if instrument is None: + raise RuntimeError(f"No matching engine found for {tick.instrument_id}") + self.add_instrument(instrument) + matching_engine = self._matching_engines[tick.instrument_id] matching_engine.process_quote_tick(tick) @@ -730,7 +747,11 @@ cdef class SimulatedExchange: cdef OrderMatchingEngine matching_engine = self._matching_engines.get(tick.instrument_id) if matching_engine is None: - raise RuntimeError(f"No matching engine found for {tick.instrument_id}") + instrument = self.cache.instrument(tick.instrument_id) + if instrument is None: + raise RuntimeError(f"No matching engine found for {tick.instrument_id}") + self.add_instrument(instrument) + matching_engine = self._matching_engines[tick.instrument_id] matching_engine.process_trade_tick(tick) @@ -754,7 +775,11 @@ cdef class SimulatedExchange: cdef OrderMatchingEngine matching_engine = self._matching_engines.get(bar.bar_type.instrument_id) if matching_engine is None: - raise RuntimeError(f"No matching engine found for {bar.bar_type.instrument_id}") + instrument = self.cache.instrument(bar.bar_type.instrument_id) + if instrument is None: + raise RuntimeError(f"No matching engine found for {bar.bar_type.instrument_id}") + self.add_instrument(instrument) + matching_engine = self._matching_engines[bar.bar_type.instrument_id] matching_engine.process_bar(bar) @@ -796,13 +821,17 @@ cdef class SimulatedExchange: cdef OrderMatchingEngine matching_engine = self._matching_engines.get(data.instrument_id) if matching_engine is None: - raise RuntimeError(f"No matching engine found for {data.instrument_id}") + instrument = self.cache.instrument(data.instrument_id) + if instrument is None: + raise RuntimeError(f"No matching engine found for {data.instrument_id}") + self.add_instrument(instrument) + matching_engine = self._matching_engines[data.instrument_id] matching_engine.process_status(data.status) cpdef void process(self, uint64_t ts_now): """ - Process the exchange to the gives time. + Process the exchange to the given time. All pending commands will be processed along with all simulation modules. @@ -826,25 +855,10 @@ cdef class SimulatedExchange: else: break - cdef: - TradingCommand command - Order order - list orders + cdef TradingCommand command while self._message_queue: command = self._message_queue.pop() - if isinstance(command, SubmitOrder): - self._matching_engines[command.instrument_id].process_order(command.order, self.exec_client.account_id) - elif isinstance(command, SubmitOrderList): - for order in command.order_list.orders: - self._matching_engines[command.instrument_id].process_order(order, self.exec_client.account_id) - elif isinstance(command, ModifyOrder): - self._matching_engines[command.instrument_id].process_modify(command, self.exec_client.account_id) - elif isinstance(command, CancelOrder): - self._matching_engines[command.instrument_id].process_cancel(command, self.exec_client.account_id) - elif isinstance(command, CancelAllOrders): - self._matching_engines[command.instrument_id].process_cancel_all(command, self.exec_client.account_id) - elif isinstance(command, BatchCancelOrders): - self._matching_engines[command.instrument_id].process_batch_cancel(command, self.exec_client.account_id) + self._process_trading_command(command) # Iterate over modules cdef SimulationModule module @@ -873,6 +887,28 @@ cdef class SimulatedExchange: self._log.info("Reset") + cdef void _process_trading_command(self, TradingCommand command): + cdef OrderMatchingEngine matching_engine = self._matching_engines.get(command.instrument_id) + if matching_engine is None: + raise RuntimeError(f"Cannot process command: no matching engine for {command.instrument_id}") + + cdef: + Order order + list[Order] orders + if isinstance(command, SubmitOrder): + matching_engine.process_order(command.order, self.exec_client.account_id) + elif isinstance(command, SubmitOrderList): + for order in command.order_list.orders: + matching_engine.process_order(order, self.exec_client.account_id) + elif isinstance(command, ModifyOrder): + matching_engine.process_modify(command, self.exec_client.account_id) + elif isinstance(command, CancelOrder): + matching_engine.process_cancel(command, self.exec_client.account_id) + elif isinstance(command, CancelAllOrders): + matching_engine.process_cancel_all(command, self.exec_client.account_id) + elif isinstance(command, BatchCancelOrders): + matching_engine.process_batch_cancel(command, self.exec_client.account_id) + # -- EVENT GENERATORS ----------------------------------------------------------------------------- cdef void _generate_fresh_account_state(self): diff --git a/nautilus_trader/common/config.py b/nautilus_trader/common/config.py index df7bc5a2cf0..eb0dfc9750f 100644 --- a/nautilus_trader/common/config.py +++ b/nautilus_trader/common/config.py @@ -320,7 +320,7 @@ class MessageBusConfig(NautilusConfig, frozen=True): If `use_trader_id` and `use_instance_id` are *both* false, then it becomes possible for many traders to be configured to write to the same streams. types_filter : list[type], optional - A list of serializable types *not* to publish externally. + A list of serializable types **not** to publish externally. """ diff --git a/nautilus_trader/core/correctness.pyx b/nautilus_trader/core/correctness.pyx index 3b60cc33e50..f61100a9f02 100644 --- a/nautilus_trader/core/correctness.pyx +++ b/nautilus_trader/core/correctness.pyx @@ -698,6 +698,9 @@ cdef class Condition: """ Check the real number value is within the specified range (inclusive). + This function accounts for potential floating-point precision issues by using a small + epsilon value of 1e-15. + Parameters ---------- value : scalar @@ -717,7 +720,8 @@ cdef class Condition: If `value` is not within the range (inclusive of the end points). """ - if start <= value <= end: + cdef double epsilon = 1e-15 # Epsilon to account for floating-point precision issues + if start - epsilon <= value <= end + epsilon: return # Check passed raise make_exception( diff --git a/nautilus_trader/core/includes/common.h b/nautilus_trader/core/includes/common.h index e6881f4e03e..bec0ae985e2 100644 --- a/nautilus_trader/core/includes/common.h +++ b/nautilus_trader/core/includes/common.h @@ -193,14 +193,24 @@ typedef enum LogLevel { ERROR = 40, } LogLevel; +/** + * A real-time clock which uses system time. + * + * Timestamps are guaranteed to be unique and monotonically increasing. + */ typedef struct LiveClock LiveClock; typedef struct LogGuard LogGuard; +/** + * A static test clock. + * + * Stores the current timestamp internally which can be advanced. + */ typedef struct TestClock TestClock; /** - * Provides a C compatible Foreign Function Interface (FFI) for an underlying [`TestClock`]. + * C compatible Foreign Function Interface (FFI) for an underlying [`TestClock`]. * * This struct wraps `TestClock` in a way that makes it compatible with C function * calls, enabling interaction with `TestClock` in a C environment. @@ -214,7 +224,7 @@ typedef struct TestClock_API { } TestClock_API; /** - * Provides a C compatible Foreign Function Interface (FFI) for an underlying [`LiveClock`]. + * C compatible Foreign Function Interface (FFI) for an underlying [`LiveClock`]. * * This struct wraps `LiveClock` in a way that makes it compatible with C function * calls, enabling interaction with `LiveClock` in a C environment. @@ -229,7 +239,7 @@ typedef struct LiveClock_API { } LiveClock_API; /** - * Provides a C compatible Foreign Function Interface (FFI) for an underlying [`LogGuard`]. + * 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. @@ -269,7 +279,7 @@ typedef struct TimeEvent_t { */ typedef struct TimeEventHandler_t { /** - * The event. + * The time event. */ struct TimeEvent_t event; /** diff --git a/nautilus_trader/core/includes/model.h b/nautilus_trader/core/includes/model.h index dec7b94fcc2..2e89d962a40 100644 --- a/nautilus_trader/core/includes/model.h +++ b/nautilus_trader/core/includes/model.h @@ -138,11 +138,11 @@ typedef enum BookType { */ L1_MBP = 1, /** - * Market by price, one order per level (aggregated). + * Market-by-price, one order per level (aggregated). */ L2_MBP = 2, /** - * Market by order, multiple orders per level (full granularity). + * Market-by-order, multiple orders per level (full granularity). */ L3_MBO = 3, } BookType; @@ -547,7 +547,7 @@ typedef enum PriceType { */ typedef enum RecordFlag { /** - * Last message in the packet from the venue for a given `instrument_id`. + * Last message in the book event or packet from the venue for a given `instrument_id`. */ F_LAST = (1 << 7), /** @@ -705,12 +705,12 @@ typedef enum TriggerType { typedef struct Level Level; /** - * Provides an order book. + * Provides a performant, generic, multi-purpose order book. * * Can handle the following granularity data: - * - MBO (market by order) / L3 - * - MBP (market by price) / L2 aggregated order per level - * - MBP (market by price) / L1 top-of-book only + * - MBO (market-by-order) / L3 + * - MBP (market-by-price) / L2 aggregated order per level + * - MBP (market-by-price) / L1 top-of-book only */ typedef struct OrderBook OrderBook; @@ -824,7 +824,7 @@ typedef struct OrderBookDelta_t { } OrderBookDelta_t; /** - * Provides a C compatible Foreign Function Interface (FFI) for an underlying [`OrderBookDeltas`]. + * C compatible Foreign Function Interface (FFI) for an underlying [`OrderBookDeltas`]. * * This struct wraps `OrderBookDeltas` in a way that makes it compatible with C function * calls, enabling interaction with `OrderBookDeltas` in a C environment. @@ -924,11 +924,6 @@ typedef struct QuoteTick_t { /** * Represents a valid trade match ID (assigned by a trading venue). * - * Maximum length is 36 characters. - * - * 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 { @@ -1089,14 +1084,6 @@ typedef struct Data_t { /** * Represents a valid trader ID. - * - * Must be correctly formatted with two valid strings either side of a hyphen. - * It is expected a trader ID is the abbreviated name of the trader - * with an order ID tag number separated by a hyphen. - * - * Example: "TESTER-001". - * The reason for the numerical component of the ID is so that order and position IDs - * do not collide with those from another node instance. */ typedef struct TraderId_t { char* _0; @@ -1104,15 +1091,6 @@ typedef struct TraderId_t { /** * Represents a valid strategy ID. - * - * Must be correctly formatted with two valid strings either side of a hyphen. - * It is expected a strategy ID is the class name of the strategy, - * with an order ID tag number separated by a hyphen. - * - * Example: "EMACross-001". - * - * The reason for the numerical component of the ID is so that order and position IDs - * do not collide with those from another strategy within the node instance. */ typedef struct StrategyId_t { char* _0; @@ -1159,12 +1137,6 @@ typedef struct OrderReleased_t { /** * Represents a valid account ID. - * - * Must be correctly formatted with two valid strings either side of a hyphen '-'. - * It is expected an account ID is the name of the issuer with an account number - * separated by a hyphen. - * - * Example: "IB-D02851908". */ typedef struct AccountId_t { char* _0; @@ -1250,7 +1222,7 @@ typedef struct PositionId_t { } PositionId_t; /** - * Provides a C compatible Foreign Function Interface (FFI) for an underlying + * C compatible Foreign Function Interface (FFI) for an underlying * [`SyntheticInstrument`]. * * This struct wraps `SyntheticInstrument` in a way that makes it compatible with C function @@ -1265,7 +1237,7 @@ typedef struct SyntheticInstrument_API { } SyntheticInstrument_API; /** - * Provides a C compatible Foreign Function Interface (FFI) for an underlying `OrderBook`. + * C compatible Foreign Function Interface (FFI) for an underlying `OrderBook`. * * This struct wraps `OrderBook` in a way that makes it compatible with C function * calls, enabling interaction with `OrderBook` in a C environment. @@ -1279,7 +1251,7 @@ typedef struct OrderBook_API { } OrderBook_API; /** - * Provides a C compatible Foreign Function Interface (FFI) for an underlying order book[`Level`]. + * C compatible Foreign Function Interface (FFI) for an underlying order book[`Level`]. * * This struct wraps `Level` in a way that makes it compatible with C function * calls, enabling interaction with `Level` in a C environment. diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 1fcabc1948f..2cfd70c9f71 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -2664,6 +2664,42 @@ class VariableIndexDynamicAverage: def handle_bar(self, bar: Bar) -> None: ... def reset(self) -> None: ... +class VolumeWeightedAveragePrice: + def __init__( + self + ) -> None: ... + @property + def name(self) -> str: ... + @property + def initialized(self) -> bool: ... + @property + def has_inputs(self) -> bool: ... + @property + def value(self) -> float: ... + def update_raw(self, price: float, volume: float, timestamp: float ) -> None: ... + def handle_bar(self, bar: Bar) -> None: ... + def reset(self) -> None: ... + +class VerticalHorizontalFilter: + def __init__( + self, + period: int, + ma_type: MovingAverageType = ..., + ) -> None: ... + @property + def name(self) -> str: ... + @property + def period(self) -> int: ... + @property + def initialized(self) -> bool: ... + @property + def has_inputs(self) -> bool: ... + @property + def value(self) -> float: ... + def update_raw(self, close: float) -> None: ... + def handle_bar(self, bar: Bar) -> None: ... + def reset(self) -> None: ... + class ChandeMomentumOscillator: def __init__( self, diff --git a/nautilus_trader/core/rust/common.pxd b/nautilus_trader/core/rust/common.pxd index 899a9273f40..56c50663423 100644 --- a/nautilus_trader/core/rust/common.pxd +++ b/nautilus_trader/core/rust/common.pxd @@ -101,16 +101,22 @@ cdef extern from "../includes/common.h": # The **ERROR** error log level. ERROR # = 40, + # A real-time clock which uses system time. + # + # Timestamps are guaranteed to be unique and monotonically increasing. cdef struct LiveClock: pass cdef struct LogGuard: pass + # A static test clock. + # + # Stores the current timestamp internally which can be advanced. cdef struct TestClock: pass - # Provides a C compatible Foreign Function Interface (FFI) for an underlying [`TestClock`]. + # C compatible Foreign Function Interface (FFI) for an underlying [`TestClock`]. # # This struct wraps `TestClock` in a way that makes it compatible with C function # calls, enabling interaction with `TestClock` in a C environment. @@ -121,7 +127,7 @@ cdef extern from "../includes/common.h": cdef struct TestClock_API: TestClock *_0; - # Provides a C compatible Foreign Function Interface (FFI) for an underlying [`LiveClock`]. + # C compatible Foreign Function Interface (FFI) for an underlying [`LiveClock`]. # # This struct wraps `LiveClock` in a way that makes it compatible with C function # calls, enabling interaction with `LiveClock` in a C environment. @@ -133,7 +139,7 @@ cdef extern from "../includes/common.h": cdef struct LiveClock_API: LiveClock *_0; - # Provides a C compatible Foreign Function Interface (FFI) for an underlying [`LogGuard`]. + # 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. @@ -157,7 +163,7 @@ cdef extern from "../includes/common.h": # Represents a time event and its associated handler. cdef struct TimeEventHandler_t: - # The event. + # The time event. TimeEvent_t event; # The callable raw pointer. char *callback_ptr; diff --git a/nautilus_trader/core/rust/model.pxd b/nautilus_trader/core/rust/model.pxd index 1663a62ee6a..e7e4a353703 100644 --- a/nautilus_trader/core/rust/model.pxd +++ b/nautilus_trader/core/rust/model.pxd @@ -80,9 +80,9 @@ cdef extern from "../includes/model.h": cpdef enum BookType: # Top-of-book best bid/ask, one level per side. L1_MBP # = 1, - # Market by price, one order per level (aggregated). + # Market-by-price, one order per level (aggregated). L2_MBP # = 2, - # Market by order, multiple orders per level (full granularity). + # Market-by-order, multiple orders per level (full granularity). L3_MBO # = 3, # The order contigency type which specifies the behavior of linked orders. @@ -296,7 +296,7 @@ cdef extern from "../includes/model.h": # A record flag bit field, indicating event end and data information. cpdef enum RecordFlag: - # Last message in the packet from the venue for a given `instrument_id`. + # Last message in the book event or packet from the venue for a given `instrument_id`. F_LAST # = (1 << 7), # Top-of-book message, not an individual order. F_TOB # = (1 << 6), @@ -378,12 +378,12 @@ cdef extern from "../includes/model.h": cdef struct Level: pass - # Provides an order book. + # Provides a performant, generic, multi-purpose order book. # # Can handle the following granularity data: - # - MBO (market by order) / L3 - # - MBP (market by price) / L2 aggregated order per level - # - MBP (market by price) / L1 top-of-book only + # - MBO (market-by-order) / L3 + # - MBP (market-by-price) / L2 aggregated order per level + # - MBP (market-by-price) / L1 top-of-book only cdef struct OrderBook: pass @@ -451,7 +451,7 @@ cdef extern from "../includes/model.h": # The UNIX timestamp (nanoseconds) when the struct was initialized. uint64_t ts_init; - # Provides a C compatible Foreign Function Interface (FFI) for an underlying [`OrderBookDeltas`]. + # C compatible Foreign Function Interface (FFI) for an underlying [`OrderBookDeltas`]. # # This struct wraps `OrderBookDeltas` in a way that makes it compatible with C function # calls, enabling interaction with `OrderBookDeltas` in a C environment. @@ -510,11 +510,6 @@ cdef extern from "../includes/model.h": # Represents a valid trade match ID (assigned by a trading venue). # - # Maximum length is 36 characters. - # - # 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 value as a fixed-length C string byte array (includes null terminator). @@ -598,27 +593,10 @@ cdef extern from "../includes/model.h": Bar_t bar; # Represents a valid trader ID. - # - # Must be correctly formatted with two valid strings either side of a hyphen. - # It is expected a trader ID is the abbreviated name of the trader - # with an order ID tag number separated by a hyphen. - # - # Example: "TESTER-001". - # The reason for the numerical component of the ID is so that order and position IDs - # do not collide with those from another node instance. cdef struct TraderId_t: char* _0; # Represents a valid strategy ID. - # - # Must be correctly formatted with two valid strings either side of a hyphen. - # It is expected a strategy ID is the class name of the strategy, - # with an order ID tag number separated by a hyphen. - # - # Example: "EMACross-001". - # - # The reason for the numerical component of the ID is so that order and position IDs - # do not collide with those from another strategy within the node instance. cdef struct StrategyId_t: char* _0; @@ -656,12 +634,6 @@ cdef extern from "../includes/model.h": uint64_t ts_init; # Represents a valid account ID. - # - # Must be correctly formatted with two valid strings either side of a hyphen '-'. - # It is expected an account ID is the name of the issuer with an account number - # separated by a hyphen. - # - # Example: "IB-D02851908". cdef struct AccountId_t: char* _0; @@ -723,7 +695,7 @@ cdef extern from "../includes/model.h": cdef struct PositionId_t: char* _0; - # Provides a C compatible Foreign Function Interface (FFI) for an underlying + # C compatible Foreign Function Interface (FFI) for an underlying # [`SyntheticInstrument`]. # # This struct wraps `SyntheticInstrument` in a way that makes it compatible with C function @@ -735,7 +707,7 @@ cdef extern from "../includes/model.h": cdef struct SyntheticInstrument_API: SyntheticInstrument *_0; - # Provides a C compatible Foreign Function Interface (FFI) for an underlying `OrderBook`. + # C compatible Foreign Function Interface (FFI) for an underlying `OrderBook`. # # This struct wraps `OrderBook` in a way that makes it compatible with C function # calls, enabling interaction with `OrderBook` in a C environment. @@ -746,7 +718,7 @@ cdef extern from "../includes/model.h": cdef struct OrderBook_API: OrderBook *_0; - # Provides a C compatible Foreign Function Interface (FFI) for an underlying order book[`Level`]. + # C compatible Foreign Function Interface (FFI) for an underlying order book[`Level`]. # # This struct wraps `Level` in a way that makes it compatible with C function # calls, enabling interaction with `Level` in a C environment. diff --git a/nautilus_trader/data/config.py b/nautilus_trader/data/config.py index c7ca7da07e8..1aa7f5af824 100644 --- a/nautilus_trader/data/config.py +++ b/nautilus_trader/data/config.py @@ -35,6 +35,8 @@ class DataEngineConfig(NautilusConfig, frozen=True): - 'right-open': start time is included and end time is excluded. validate_data_sequence : bool, default False If data objects timestamp sequencing will be validated and handled. + buffer_deltas : bool, default False + If order book deltas should be buffered until the F_LAST flag is set for a delta. debug : bool, default False If debug mode is active (will provide extra debug logging). @@ -44,4 +46,5 @@ class DataEngineConfig(NautilusConfig, frozen=True): time_bars_timestamp_on_close: bool = True time_bars_interval_type: str = "left-open" validate_data_sequence: bool = False + buffer_deltas: bool = False debug: bool = False diff --git a/nautilus_trader/data/engine.pxd b/nautilus_trader/data/engine.pxd index 6f350633bfd..e153dcaf50f 100644 --- a/nautilus_trader/data/engine.pxd +++ b/nautilus_trader/data/engine.pxd @@ -57,10 +57,12 @@ cdef class DataEngine(Component): cdef readonly dict[InstrumentId, list[SyntheticInstrument]] _synthetic_trade_feeds cdef readonly list[InstrumentId] _subscribed_synthetic_quotes cdef readonly list[InstrumentId] _subscribed_synthetic_trades + cdef readonly dict[InstrumentId, list[OrderBookDelta]] _buffered_deltas_map cdef readonly bint _time_bars_build_with_no_updates cdef readonly bint _time_bars_timestamp_on_close cdef readonly str _time_bars_interval_type cdef readonly bint _validate_data_sequence + cdef readonly bint _buffer_deltas cdef readonly bint debug """If debug mode is active (will provide extra debug logging).\n\n:returns: `bool`""" diff --git a/nautilus_trader/data/engine.pyx b/nautilus_trader/data/engine.pyx index 5762d901c56..d3f7aa0d627 100644 --- a/nautilus_trader/data/engine.pyx +++ b/nautilus_trader/data/engine.pyx @@ -89,6 +89,7 @@ from nautilus_trader.model.instruments.base cimport Instrument from nautilus_trader.model.instruments.synthetic cimport SyntheticInstrument from nautilus_trader.model.objects cimport Price from nautilus_trader.model.objects cimport Quantity +from nautilus_trader.model.enums import RecordFlag cdef class DataEngine(Component): @@ -137,6 +138,7 @@ cdef class DataEngine(Component): self._synthetic_trade_feeds: dict[InstrumentId, list[SyntheticInstrument]] = {} self._subscribed_synthetic_quotes: list[InstrumentId] = [] self._subscribed_synthetic_trades: list[InstrumentId] = [] + self._buffered_deltas_map: dict[InstrumentId, list[OrderBookDelta]] = {} # Settings self.debug = config.debug @@ -144,6 +146,7 @@ cdef class DataEngine(Component): self._time_bars_timestamp_on_close = config.time_bars_timestamp_on_close self._time_bars_interval_type = config.time_bars_interval_type self._validate_data_sequence = config.validate_data_sequence + self._buffer_deltas = config.buffer_deltas # Counters self.command_count = 0 @@ -531,6 +534,7 @@ cdef class DataEngine(Component): self._synthetic_trade_feeds.clear() self._subscribed_synthetic_quotes.clear() self._subscribed_synthetic_trades.clear() + self._buffered_deltas_map.clear() self._clock.cancel_timers() self.command_count = 0 @@ -1055,7 +1059,8 @@ cdef class DataEngine(Component): if instrument_id is None: if not self._msgbus.has_subscribers(f"data.instrument.{client.id.value}.*"): - client.unsubscribe_instruments() + if client.subscribed_instruments(): + client.unsubscribe_instruments() return else: if instrument_id.is_synthetic(): @@ -1067,7 +1072,8 @@ cdef class DataEngine(Component): f".{instrument_id.venue}" f".{instrument_id.symbol}", ): - client.unsubscribe_instrument(instrument_id) + if instrument_id in client.subscribed_instruments(): + client.unsubscribe_instrument(instrument_id) cpdef void _handle_unsubscribe_order_book_deltas( self, @@ -1088,7 +1094,8 @@ cdef class DataEngine(Component): f".{instrument_id.venue}" f".{instrument_id.symbol}", ): - client.unsubscribe_order_book_deltas(instrument_id) + if instrument_id in client.subscribed_order_book_deltas(): + client.unsubscribe_order_book_deltas(instrument_id) cpdef void _handle_unsubscribe_order_book_snapshots( self, @@ -1109,7 +1116,8 @@ cdef class DataEngine(Component): f".{instrument_id.venue}" f".{instrument_id.symbol}", ): - client.unsubscribe_order_book_snapshots(instrument_id) + if instrument_id in client.subscribed_order_book_snapshots(): + client.unsubscribe_order_book_snapshots(instrument_id) cpdef void _handle_unsubscribe_quote_ticks( self, @@ -1124,7 +1132,8 @@ cdef class DataEngine(Component): f".{instrument_id.venue}" f".{instrument_id.symbol}", ): - client.unsubscribe_quote_ticks(instrument_id) + if instrument_id in client.subscribed_quote_ticks(): + client.unsubscribe_quote_ticks(instrument_id) cpdef void _handle_unsubscribe_trade_ticks( self, @@ -1139,7 +1148,8 @@ cdef class DataEngine(Component): f".{instrument_id.venue}" f".{instrument_id.symbol}", ): - client.unsubscribe_trade_ticks(instrument_id) + if instrument_id in client.subscribed_trade_ticks(): + client.unsubscribe_trade_ticks(instrument_id) cpdef void _handle_unsubscribe_bars( self, @@ -1171,7 +1181,8 @@ cdef class DataEngine(Component): try: if not self._msgbus.has_subscribers(f"data.{data_type}"): - client.unsubscribe(data_type) + if data_type in client.subscribed_custom_data(): + client.unsubscribe(data_type) except NotImplementedError: self._log.error( f"Cannot unsubscribe: {client.id.value} " @@ -1374,24 +1385,79 @@ cdef class DataEngine(Component): ) cpdef void _handle_order_book_delta(self, OrderBookDelta delta): - cdef OrderBookDeltas deltas = OrderBookDeltas( - instrument_id=delta.instrument_id, - deltas=[delta] - ) - self._msgbus.publish_c( - topic=f"data.book.deltas" - f".{deltas.instrument_id.venue}" - f".{deltas.instrument_id.symbol}", - msg=deltas, - ) + cdef OrderBookDeltas deltas = None + cdef list[OrderBookDelta] buffer_deltas = None + cdef bint is_last_delta = False + + if self._buffer_deltas: + buffer_deltas = self._buffered_deltas_map.get(delta.instrument_id) + + if buffer_deltas is None: + buffer_deltas = [] + self._buffered_deltas_map[delta.instrument_id] = buffer_deltas + + buffer_deltas.append(delta) + is_last_delta = delta.flags == RecordFlag.F_LAST + + if is_last_delta: + deltas = OrderBookDeltas( + instrument_id=delta.instrument_id, + deltas=buffer_deltas + ) + self._msgbus.publish_c( + topic=f"data.book.deltas" + f".{deltas.instrument_id.venue}" + f".{deltas.instrument_id.symbol}", + msg=deltas, + ) + buffer_deltas.clear() + else: + deltas = OrderBookDeltas( + instrument_id=delta.instrument_id, + deltas=[delta] + ) + self._msgbus.publish_c( + topic=f"data.book.deltas" + f".{deltas.instrument_id.venue}" + f".{deltas.instrument_id.symbol}", + msg=deltas, + ) cpdef void _handle_order_book_deltas(self, OrderBookDeltas deltas): - self._msgbus.publish_c( - topic=f"data.book.deltas" - f".{deltas.instrument_id.venue}" - f".{deltas.instrument_id.symbol}", - msg=deltas, - ) + cdef OrderBookDeltas deltas_to_publish = None + cdef list[OrderBookDelta] buffer_deltas = None + cdef bint is_last_delta = False + + if self._buffer_deltas: + buffer_deltas = self._buffered_deltas_map.get(deltas.instrument_id) + + if buffer_deltas is None: + buffer_deltas = [] + self._buffered_deltas_map[deltas.instrument_id] = buffer_deltas + + for delta in deltas.deltas: + buffer_deltas.append(delta) + is_last_delta = delta.flags == RecordFlag.F_LAST + + if is_last_delta: + deltas_to_publish = OrderBookDeltas( + instrument_id=deltas.instrument_id, + deltas=buffer_deltas, + ) + self._msgbus.publish_c( + topic=f"data.book.deltas" + f".{deltas.instrument_id.venue}" + f".{deltas.instrument_id.symbol}", + msg=deltas_to_publish, + ) + buffer_deltas.clear() + else: + self._msgbus.publish_c( + topic=f"data.book.deltas" + f".{deltas.instrument_id.venue}" + f".{deltas.instrument_id.symbol}", + msg=deltas, + ) cpdef void _handle_order_book_depth(self, OrderBookDepth10 depth): self._msgbus.publish_c( diff --git a/nautilus_trader/model/instruments/base.pyx b/nautilus_trader/model/instruments/base.pyx index 982799a57d2..1d84167f9d3 100644 --- a/nautilus_trader/model/instruments/base.pyx +++ b/nautilus_trader/model/instruments/base.pyx @@ -450,10 +450,14 @@ cdef class Instrument(Data): Raises ------ ValueError - If tick scheme is not registered. + If a tick scheme is not initialized. """ - Condition.not_none(self._tick_scheme, "self._tick_scheme") + if self._tick_scheme is None: + raise ValueError( + f"No tick scheme for instrument {self.id.to_str()}. " + "You can specify a tick scheme by passing a `tick_scheme_name` at initialization." + ) return self._tick_scheme.next_bid_price(value=value, n=num_ticks) @@ -477,10 +481,14 @@ cdef class Instrument(Data): Raises ------ ValueError - If tick scheme is not registered. + If a tick scheme is not initialized. """ - Condition.not_none(self._tick_scheme, "self._tick_scheme") + if self._tick_scheme is None: + raise ValueError( + f"No tick scheme for instrument {self.id.to_str()}. " + "You can specify a tick scheme by passing a `tick_scheme_name` at initialization." + ) return self._tick_scheme.next_ask_price(value=value, n=num_ticks) diff --git a/nautilus_trader/persistence/config.py b/nautilus_trader/persistence/config.py index 67eeb157c8e..7fb5c4ee188 100644 --- a/nautilus_trader/persistence/config.py +++ b/nautilus_trader/persistence/config.py @@ -38,7 +38,7 @@ class StreamingConfig(NautilusConfig, frozen=True): If any existing feather files should be replaced. include_types : list[type], optional A list of Arrow serializable types to write. - If this is specified then *only* the included types will be written. + If this is specified then **only** the included types will be written. """ diff --git a/nautilus_trader/persistence/writer.py b/nautilus_trader/persistence/writer.py index c5799726827..ea75d66a1b7 100644 --- a/nautilus_trader/persistence/writer.py +++ b/nautilus_trader/persistence/writer.py @@ -56,7 +56,7 @@ class StreamingFeatherWriter: If existing files at the given `path` should be replaced. include_types : list[type], optional A list of Arrow serializable types to write. - If this is specified then *only* the included types will be written. + If this is specified then **only** the included types will be written. """ @@ -89,10 +89,9 @@ def __init__( self._writers: dict[str, RecordBatchStreamWriter] = {} self._instrument_writers: dict[tuple[str, str], RecordBatchStreamWriter] = {} self._per_instrument_writers = { - "trade_tick", - "quote_tick", "order_book_delta", - "ticker", + "quote_tick", + "trade_tick", } self._instruments: dict[InstrumentId, Instrument] = {} self._create_writers() @@ -133,6 +132,8 @@ def _create_writer(self, cls: type, table_name: str | None = None) -> None: self._files[table_name] = f self._writers[table_name] = pa.ipc.new_stream(f, schema) + self.logger.info(f"Created writer for table '{table_name}'") + def _create_writers(self) -> None: for cls in self._schemas: self._create_writer(cls=cls) @@ -156,6 +157,8 @@ def _create_instrument_writer(self, cls: type, obj: Any) -> None: self._files[key] = f self._instrument_writers[key] = pa.ipc.new_stream(f, schema) + self.logger.info(f"Created writer for table '{table_name}'") + def _extract_obj_metadata( self, obj: TradeTick | QuoteTick | Bar | OrderBookDelta, @@ -228,9 +231,10 @@ def write(self, obj: object) -> None: # noqa: C901 table += f"_{str(bar.bar_type).lower()}" if table not in self._writers: + self.logger.debug(f"Writer not setup for table '{table}'") if table.startswith("custom_signal"): self._create_writer(cls=cls) - elif table.startswith("bar"): + elif table.startswith(("bar", "binance_bar")): self._create_writer(cls=cls, table_name=table) elif table in self._per_instrument_writers: key = (table, obj.instrument_id.value) # type: ignore @@ -242,13 +246,16 @@ def write(self, obj: object) -> None: # noqa: C901 return else: return + if table in self._per_instrument_writers: writer: RecordBatchStreamWriter = self._instrument_writers[(table, obj.instrument_id.value)] # type: ignore else: writer: RecordBatchStreamWriter = self._writers[table] # type: ignore + serialized = ArrowSerializer.serialize_batch([obj], data_cls=cls) if not serialized: return + try: writer.write_table(serialized) self.check_flush() diff --git a/nautilus_trader/serialization/arrow/serializer.py b/nautilus_trader/serialization/arrow/serializer.py index 638b0dcdabd..438a109dd92 100644 --- a/nautilus_trader/serialization/arrow/serializer.py +++ b/nautilus_trader/serialization/arrow/serializer.py @@ -113,7 +113,10 @@ def _unpack_container_objects(data_cls: type, data: list[Any]) -> list[Data]: return data @staticmethod - def rust_defined_to_record_batch(data: list[Data], data_cls: type) -> pa.Table | pa.RecordBatch: + def rust_defined_to_record_batch( # noqa: C901 (too complex) + data: list[Data], + data_cls: type, + ) -> pa.Table | pa.RecordBatch: data = sorted(data, key=lambda x: x.ts_init) data = ArrowSerializer._unpack_container_objects(data_cls, data) diff --git a/nautilus_trader/test_kit/providers.py b/nautilus_trader/test_kit/providers.py index aa302bd138a..a2a3351b6f7 100644 --- a/nautilus_trader/test_kit/providers.py +++ b/nautilus_trader/test_kit/providers.py @@ -650,7 +650,7 @@ def third_friday_of_month(year: int, month: int) -> dt.date: return third_friday -def get_contract_month_code(expiry_month: int) -> str: +def get_contract_month_code(expiry_month: int) -> str: # noqa: C901 (too complex) match expiry_month: case 1: return "F" diff --git a/nautilus_trader/test_kit/stubs/data.py b/nautilus_trader/test_kit/stubs/data.py index 94f70bc4d93..c79f9e535d2 100644 --- a/nautilus_trader/test_kit/stubs/data.py +++ b/nautilus_trader/test_kit/stubs/data.py @@ -427,10 +427,11 @@ def order_book_delta_clear( def order_book_deltas( instrument_id: InstrumentId | None = None, deltas: list[OrderBookDelta] | None = None, + flags: int = 0, ) -> OrderBookDeltas: return OrderBookDeltas( instrument_id=instrument_id or TestIdStubs.audusd_id(), - deltas=deltas or [TestDataStubs.order_book_delta()], + deltas=deltas or [TestDataStubs.order_book_delta(flags=flags)], ) @staticmethod diff --git a/poetry.lock b/poetry.lock index 4d184f5a2a9..ac2304fb65b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -394,63 +394,63 @@ files = [ [[package]] name = "coverage" -version = "7.5.1" +version = "7.5.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"}, - {file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"}, - {file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"}, - {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"}, - {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"}, - {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"}, - {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"}, - {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"}, - {file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"}, - {file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"}, - {file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"}, - {file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"}, - {file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"}, - {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"}, - {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"}, - {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"}, - {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"}, - {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"}, - {file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"}, - {file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"}, - {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"}, - {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"}, - {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"}, - {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"}, - {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"}, - {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"}, - {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"}, - {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"}, - {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"}, - {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"}, - {file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"}, - {file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"}, - {file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"}, - {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"}, - {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"}, - {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"}, - {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"}, - {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"}, - {file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"}, - {file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"}, - {file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"}, - {file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"}, - {file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"}, - {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"}, - {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"}, - {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"}, - {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"}, - {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"}, - {file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"}, - {file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"}, - {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"}, - {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"}, + {file = "coverage-7.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45"}, + {file = "coverage-7.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c"}, + {file = "coverage-7.5.3-cp310-cp310-win32.whl", hash = "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84"}, + {file = "coverage-7.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac"}, + {file = "coverage-7.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974"}, + {file = "coverage-7.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614"}, + {file = "coverage-7.5.3-cp311-cp311-win32.whl", hash = "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9"}, + {file = "coverage-7.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a"}, + {file = "coverage-7.5.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8"}, + {file = "coverage-7.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84"}, + {file = "coverage-7.5.3-cp312-cp312-win32.whl", hash = "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08"}, + {file = "coverage-7.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb"}, + {file = "coverage-7.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb"}, + {file = "coverage-7.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0"}, + {file = "coverage-7.5.3-cp38-cp38-win32.whl", hash = "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485"}, + {file = "coverage-7.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56"}, + {file = "coverage-7.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85"}, + {file = "coverage-7.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd"}, + {file = "coverage-7.5.3-cp39-cp39-win32.whl", hash = "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d"}, + {file = "coverage-7.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0"}, + {file = "coverage-7.5.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884"}, + {file = "coverage-7.5.3.tar.gz", hash = "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f"}, ] [package.dependencies] @@ -1384,18 +1384,15 @@ files = [ [[package]] name = "nodeenv" -version = "1.8.0" +version = "1.9.0" description = "Node.js virtual environment builder" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ - {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, - {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, + {file = "nodeenv-1.9.0-py2.py3-none-any.whl", hash = "sha256:508ecec98f9f3330b636d4448c0f1a56fc68017c68f1e7857ebc52acf0eb879a"}, + {file = "nodeenv-1.9.0.tar.gz", hash = "sha256:07f144e90dae547bf0d4ee8da0ee42664a42a04e02ed68e06324348dafe4bdb1"}, ] -[package.dependencies] -setuptools = "*" - [[package]] name = "numpy" version = "1.26.4" @@ -1983,13 +1980,13 @@ files = [ [[package]] name = "requests" -version = "2.32.2" +version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" files = [ - {file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"}, - {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -2004,28 +2001,28 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.4.5" +version = "0.4.6" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8f58e615dec58b1a6b291769b559e12fdffb53cc4187160a2fc83250eaf54e96"}, - {file = "ruff-0.4.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84dd157474e16e3a82745d2afa1016c17d27cb5d52b12e3d45d418bcc6d49264"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25f483ad9d50b00e7fd577f6d0305aa18494c6af139bce7319c68a17180087f4"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63fde3bf6f3ad4e990357af1d30e8ba2730860a954ea9282c95fc0846f5f64af"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e3ba4620dee27f76bbcad97067766026c918ba0f2d035c2fc25cbdd04d9c97"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:441dab55c568e38d02bbda68a926a3d0b54f5510095c9de7f95e47a39e0168aa"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1169e47e9c4136c997f08f9857ae889d614c5035d87d38fda9b44b4338909cdf"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:755ac9ac2598a941512fc36a9070a13c88d72ff874a9781493eb237ab02d75df"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4b02a65985be2b34b170025a8b92449088ce61e33e69956ce4d316c0fe7cce0"}, - {file = "ruff-0.4.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:75a426506a183d9201e7e5664de3f6b414ad3850d7625764106f7b6d0486f0a1"}, - {file = "ruff-0.4.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6e1b139b45e2911419044237d90b60e472f57285950e1492c757dfc88259bb06"}, - {file = "ruff-0.4.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6f29a8221d2e3d85ff0c7b4371c0e37b39c87732c969b4d90f3dad2e721c5b1"}, - {file = "ruff-0.4.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d6ef817124d72b54cc923f3444828ba24fa45c3164bc9e8f1813db2f3d3a8a11"}, - {file = "ruff-0.4.5-py3-none-win32.whl", hash = "sha256:aed8166c18b1a169a5d3ec28a49b43340949e400665555b51ee06f22813ef062"}, - {file = "ruff-0.4.5-py3-none-win_amd64.whl", hash = "sha256:b0b03c619d2b4350b4a27e34fd2ac64d0dabe1afbf43de57d0f9d8a05ecffa45"}, - {file = "ruff-0.4.5-py3-none-win_arm64.whl", hash = "sha256:9d15de3425f53161b3f5a5658d4522e4eee5ea002bf2ac7aa380743dd9ad5fba"}, - {file = "ruff-0.4.5.tar.gz", hash = "sha256:286eabd47e7d4d521d199cab84deca135557e6d1e0f0d01c29e757c3cb151b54"}, + {file = "ruff-0.4.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ef995583a038cd4a7edf1422c9e19118e2511b8ba0b015861b4abd26ec5367c5"}, + {file = "ruff-0.4.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:602ebd7ad909eab6e7da65d3c091547781bb06f5f826974a53dbe563d357e53c"}, + {file = "ruff-0.4.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f9ced5cbb7510fd7525448eeb204e0a22cabb6e99a3cb160272262817d49786"}, + {file = "ruff-0.4.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04a80acfc862e0e1630c8b738e70dcca03f350bad9e106968a8108379e12b31f"}, + {file = "ruff-0.4.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be47700ecb004dfa3fd4dcdddf7322d4e632de3c06cd05329d69c45c0280e618"}, + {file = "ruff-0.4.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1ff930d6e05f444090a0139e4e13e1e2e1f02bd51bb4547734823c760c621e79"}, + {file = "ruff-0.4.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f13410aabd3b5776f9c5699f42b37a3a348d65498c4310589bc6e5c548dc8a2f"}, + {file = "ruff-0.4.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0cf5cc02d3ae52dfb0c8a946eb7a1d6ffe4d91846ffc8ce388baa8f627e3bd50"}, + {file = "ruff-0.4.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea3424793c29906407e3cf417f28fc33f689dacbbadfb52b7e9a809dd535dcef"}, + {file = "ruff-0.4.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1fa8561489fadf483ffbb091ea94b9c39a00ed63efacd426aae2f197a45e67fc"}, + {file = "ruff-0.4.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d5b914818d8047270308fe3e85d9d7f4a31ec86c6475c9f418fbd1624d198e0"}, + {file = "ruff-0.4.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4f02284335c766678778475e7698b7ab83abaf2f9ff0554a07b6f28df3b5c259"}, + {file = "ruff-0.4.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3a6a0a4f4b5f54fff7c860010ab3dd81425445e37d35701a965c0248819dde7a"}, + {file = "ruff-0.4.6-py3-none-win32.whl", hash = "sha256:9018bf59b3aa8ad4fba2b1dc0299a6e4e60a4c3bc62bbeaea222679865453062"}, + {file = "ruff-0.4.6-py3-none-win_amd64.whl", hash = "sha256:a769ae07ac74ff1a019d6bd529426427c3e30d75bdf1e08bb3d46ac8f417326a"}, + {file = "ruff-0.4.6-py3-none-win_arm64.whl", hash = "sha256:735a16407a1a8f58e4c5b913ad6102722e80b562dd17acb88887685ff6f20cf6"}, + {file = "ruff-0.4.6.tar.gz", hash = "sha256:a797a87da50603f71e6d0765282098245aca6e3b94b7c17473115167d8dfb0b7"}, ] [[package]] @@ -2669,4 +2666,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "dea2c5c5e50ed163007e0a92b7d0f66abbcc0335513d0ce953902de8512cf074" +content-hash = "fb19037b99a7d95caaf44c0a244c4c22ca2acbd924c5c315232b96e3b5a2fcaa" diff --git a/pyproject.toml b/pyproject.toml index 7de9dc78634..0cc9955d02c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nautilus_trader" -version = "1.193.0" +version = "1.194.0" description = "A high-performance algorithmic trading platform and event-driven backtester" authors = ["Nautech Systems "] license = "LGPL-3.0-or-later" @@ -83,7 +83,7 @@ docformatter = "^1.7.5" mypy = "^1.10.0" pandas-stubs = "^2.2.2" pre-commit = "^3.7.1" -ruff = "^0.4.5" +ruff = "^0.4.6" types-pytz = "^2023.3" types-requests = "^2.31" types-toml = "^0.10.2" @@ -92,7 +92,7 @@ types-toml = "^0.10.2" optional = true [tool.poetry.group.test.dependencies] -coverage = "^7.5.1" +coverage = "^7.5.3" pytest = "^7.4.4" pytest-aiohttp = "^1.0.5" pytest-asyncio = "==0.21.1" # Pinned due Cython: cannot set '__pytest_asyncio_scoped_event_loop' attribute of immutable type diff --git a/tests/integration_tests/adapters/bybit/resources/ws_messages/public/ws_orderbook_delta_no_asks.json b/tests/integration_tests/adapters/bybit/resources/ws_messages/public/ws_orderbook_delta_no_asks.json new file mode 100644 index 00000000000..71d380c14a0 --- /dev/null +++ b/tests/integration_tests/adapters/bybit/resources/ws_messages/public/ws_orderbook_delta_no_asks.json @@ -0,0 +1,33 @@ +{ + "topic": "orderbook.50.BTCUSDT", + "type": "delta", + "ts": 1687940967466, + "data": { + "s": "BTCUSDT", + "b": [ + [ + "30247.20", + "30.028" + ], + [ + "30245.40", + "0.224" + ], + [ + "30242.10", + "1.593" + ], + [ + "30240.30", + "1.305" + ], + [ + "30240.00", + "0" + ] + ], + "a": [], + "u": 177400507, + "seq": 66544703342 + } +} \ No newline at end of file diff --git a/tests/integration_tests/adapters/bybit/resources/ws_messages/public/ws_orderbook_snapshot_no_asks.json b/tests/integration_tests/adapters/bybit/resources/ws_messages/public/ws_orderbook_snapshot_no_asks.json new file mode 100644 index 00000000000..2a51bd596f3 --- /dev/null +++ b/tests/integration_tests/adapters/bybit/resources/ws_messages/public/ws_orderbook_snapshot_no_asks.json @@ -0,0 +1,21 @@ +{ + "topic": "orderbook.50.BTCUSDT", + "type": "snapshot", + "ts": 1672304484978, + "data": { + "s": "BTCUSDT", + "b": [ + [ + "16493.50", + "0.006" + ], + [ + "16493.00", + "0.100" + ] + ], + "a": [], + "u": 18521288, + "seq": 7961638724 + } +} \ No newline at end of file diff --git a/tests/integration_tests/adapters/bybit/test_ws_decoders.py b/tests/integration_tests/adapters/bybit/test_ws_decoders.py index d8483cd2057..7e4046fdb4b 100644 --- a/tests/integration_tests/adapters/bybit/test_ws_decoders.py +++ b/tests/integration_tests/adapters/bybit/test_ws_decoders.py @@ -49,6 +49,10 @@ from nautilus_trader.adapters.bybit.schemas.ws import BybitWsTickerSpotMsg from nautilus_trader.adapters.bybit.schemas.ws import BybitWsTrade from nautilus_trader.adapters.bybit.schemas.ws import BybitWsTradeMsg +from nautilus_trader.model.enums import RecordFlag +from nautilus_trader.model.identifiers import InstrumentId +from nautilus_trader.model.identifiers import Symbol +from nautilus_trader.model.identifiers import Venue class TestBybitWsDecoders: @@ -132,6 +136,66 @@ def test_ws_public_orderbook_delta(self): assert result.ts == 1687940967466 assert result.type == "delta" + def test_ws_public_orderbook_delta_parse_to_deltas(self): + # Prepare + item = pkgutil.get_data( + "tests.integration_tests.adapters.bybit.resources.ws_messages.public", + "ws_orderbook_delta.json", + ) + assert item is not None + instrument_id = InstrumentId(Symbol("BTCUSDT-LINEAR"), Venue("BYBIT")) + decoder = msgspec.json.Decoder(BybitWsOrderbookDepthMsg) + + # Act + result = decoder.decode(item).data.parse_to_deltas( + instrument_id=instrument_id, + price_precision=2, + size_precision=2, + ts_event=0, + ts_init=0, + ) + + # Assert + assert len(result.deltas) == 12 + assert result.is_snapshot is False + + # Test that only the last delta has a F_LAST flag + for delta_id, delta in enumerate(result.deltas): + if delta_id < len(result.deltas) - 1: + assert delta.flags == 0 + else: + assert delta.flags == RecordFlag.F_LAST + + def test_ws_public_orderbook_delta_parse_to_deltas_no_asks(self): + # Prepare + item = pkgutil.get_data( + "tests.integration_tests.adapters.bybit.resources.ws_messages.public", + "ws_orderbook_delta_no_asks.json", + ) + assert item is not None + instrument_id = InstrumentId(Symbol("BTCUSDT-LINEAR"), Venue("BYBIT")) + decoder = msgspec.json.Decoder(BybitWsOrderbookDepthMsg) + + # Act + result = decoder.decode(item).data.parse_to_deltas( + instrument_id=instrument_id, + price_precision=2, + size_precision=2, + ts_event=0, + ts_init=0, + ) + + # Assert + assert len(result.deltas) == 5 + assert result.is_snapshot is False + + # Test that only the last delta has a F_LAST flag + for delta_id, delta in enumerate(result.deltas): + if delta_id < len(result.deltas) - 1: + assert delta.flags == 0 + else: + assert delta.flags == RecordFlag.F_LAST + def test_ws_public_orderbook_snapshot(self): item = pkgutil.get_data( "tests.integration_tests.adapters.bybit.resources.ws_messages.public", @@ -158,6 +222,66 @@ def test_ws_public_orderbook_snapshot(self): assert result.type == "snapshot" assert result.ts == 1672304484978 + def test_ws_public_orderbook_snapshot_flags(self): + # Prepare + item = pkgutil.get_data( + "tests.integration_tests.adapters.bybit.resources.ws_messages.public", + "ws_orderbook_snapshot.json", + ) + assert item is not None + instrument_id = InstrumentId(Symbol("BTCUSDT-LINEAR"), Venue("BYBIT")) + decoder = msgspec.json.Decoder(BybitWsOrderbookDepthMsg) + + # Act + result = decoder.decode(item).data.parse_to_snapshot( + instrument_id=instrument_id, + price_precision=2, + size_precision=2, + ts_event=0, + ts_init=0, + ) + + # Assert + assert len(result.deltas) == 5 + assert result.is_snapshot + + # Test that only the last delta has a F_LAST flag + for delta_id, delta in enumerate(result.deltas): + if delta_id < len(result.deltas) - 1: + assert delta.flags == 0 + else: + assert delta.flags == RecordFlag.F_LAST + + def test_ws_public_orderbook_snapshot_flags_no_asks(self): + # Prepare + item = pkgutil.get_data( + "tests.integration_tests.adapters.bybit.resources.ws_messages.public", + "ws_orderbook_snapshot_no_asks.json", + ) + assert item is not None + instrument_id = InstrumentId(Symbol("BTCUSDT-LINEAR"), Venue("BYBIT")) + decoder = msgspec.json.Decoder(BybitWsOrderbookDepthMsg) + + # Act + result = decoder.decode(item).data.parse_to_snapshot( + instrument_id=instrument_id, + price_precision=2, + size_precision=2, + ts_event=0, + ts_init=0, + ) + + # Assert + assert len(result.deltas) == 3 + assert result.is_snapshot + + # Test that only the last delta has a F_LAST flag + for delta_id, delta in enumerate(result.deltas): + if delta_id < len(result.deltas) - 1: + assert delta.flags == 0 + else: + assert delta.flags == RecordFlag.F_LAST + def test_ws_public_ticker_linear(self): item = pkgutil.get_data( "tests.integration_tests.adapters.bybit.resources.ws_messages.public", diff --git a/tests/integration_tests/adapters/interactive_brokers/conftest.py b/tests/integration_tests/adapters/interactive_brokers/conftest.py index 9b2c9d714c5..a6f4d806b52 100644 --- a/tests/integration_tests/adapters/interactive_brokers/conftest.py +++ b/tests/integration_tests/adapters/interactive_brokers/conftest.py @@ -15,6 +15,7 @@ import asyncio from unittest.mock import AsyncMock from unittest.mock import MagicMock +from unittest.mock import patch import pytest @@ -33,12 +34,35 @@ 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.mock_client import MockInteractiveBrokersClient from tests.integration_tests.adapters.interactive_brokers.test_kit import IBTestContractStubs # fmt: on +def mocked_ib_client( + loop, + msgbus, + cache, + clock, + host, + port, + client_id, + **kwargs, +) -> MockInteractiveBrokersClient: + client = MockInteractiveBrokersClient( + loop=loop, + msgbus=msgbus, + cache=cache, + clock=clock, + host=host, + port=port, + client_id=client_id, + ) + return client + + @pytest.fixture() def event_loop(): loop = asyncio.get_event_loop() @@ -120,19 +144,15 @@ def instrument_provider(ib_client): @pytest.fixture() -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=event_loop, - msgbus=msgbus, - cache=cache, - clock=clock, - host=data_client_config.ibg_host, - port=data_client_config.ibg_port, - client_id=data_client_config.ibg_client_id, - ), - ) +@patch( + "nautilus_trader.adapters.interactive_brokers.factories.get_cached_ib_client", + new=mocked_ib_client, +) +@patch( + "nautilus_trader.adapters.interactive_brokers.factories.get_cached_interactive_brokers_instrument_provider", + new=InteractiveBrokersInstrumentProvider, +) +def data_client(data_client_config, venue, event_loop, msgbus, cache, clock): client = InteractiveBrokersLiveDataClientFactory.create( loop=event_loop, name=venue.value, @@ -143,26 +163,20 @@ def data_client(mocker, data_client_config, venue, event_loop, msgbus, cache, cl ) 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, event_loop, msgbus, cache, clock): - mocker.patch( - "nautilus_trader.adapters.interactive_brokers.factories.get_cached_ib_client", - return_value=InteractiveBrokersClient( - loop=event_loop, - msgbus=msgbus, - cache=cache, - clock=clock, - host=exec_client_config.ibg_host, - port=exec_client_config.ibg_port, - client_id=exec_client_config.ibg_client_id, - ), - ) +@patch( + "nautilus_trader.adapters.interactive_brokers.factories.get_cached_ib_client", + new=mocked_ib_client, +) +@patch( + "nautilus_trader.adapters.interactive_brokers.factories.get_cached_interactive_brokers_instrument_provider", + new=InteractiveBrokersInstrumentProvider, +) +def exec_client(exec_client_config, venue, event_loop, msgbus, cache, clock): client = InteractiveBrokersLiveExecClientFactory.create( loop=event_loop, name=venue.value, @@ -173,9 +187,7 @@ def exec_client(mocker, exec_client_config, venue, event_loop, msgbus, cache, cl ) 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/adapters/interactive_brokers/mock_client.py b/tests/integration_tests/adapters/interactive_brokers/mock_client.py new file mode 100644 index 00000000000..5c956e6d3e2 --- /dev/null +++ b/tests/integration_tests/adapters/interactive_brokers/mock_client.py @@ -0,0 +1,146 @@ +import asyncio +from collections.abc import Callable +from unittest.mock import MagicMock + +from ibapi.client import EClient + +# fmt: off +from nautilus_trader.adapters.interactive_brokers.client.client import InteractiveBrokersClient +from nautilus_trader.adapters.interactive_brokers.client.wrapper import InteractiveBrokersEWrapper +from nautilus_trader.adapters.interactive_brokers.common import IBContract +from nautilus_trader.adapters.interactive_brokers.parsing.instruments import ib_contract_to_instrument_id +from tests.integration_tests.adapters.interactive_brokers.test_kit import IBTestContractStubs + + +class MockEClient(EClient): + """ + MockEClient is a subclass of EClient which is used for simulating Interactive + Brokers' client operations. + + This class overloads a few methods of the parent class to better accommodate testing + needs. More methods can be added as and when needed, depending on the testing + requirements. + + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._next_valid_counter = 0 + + def _handle_task(self, handler: Callable, **kwargs): + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(handler(**kwargs)) # noqa: RUF006 + else: + loop.run_until_complete(handler(**kwargs)) + + ######################################################################### + ################## Market Data + ######################################################################### + + ######################################################################### + ################## Options + ######################################################################### + + ######################################################################### + ################## Orders + ######################################################################### + + ######################################################################### + ################## Account and Portfolio + ######################################################################### + + ######################################################################### + ################## Daily PnL + ######################################################################### + + ######################################################################### + ################## Executions + ######################################################################### + + ######################################################################### + ################## Contract Details + ######################################################################### + + def reqContractDetails(self, reqId: int, contract: IBContract): + instrument_id = ib_contract_to_instrument_id(contract) + match instrument_id.value: + case "AAPL.NASDAQ": + self._handle_task( + self.wrapper._client.process_contract_details, + req_id=reqId, + contract_details=IBTestContractStubs.aapl_equity_contract_details(), + ) + case "EUR/USD.IDEALPRO": + self._handle_task( + self.wrapper._client.process_contract_details, + req_id=reqId, + contract_details=IBTestContractStubs.eurusd_forex_contract_details(), + ) + + self._handle_task( + self.wrapper._client.process_contract_details_end, + req_id=reqId, + ) + + ######################################################################### + ################## Market Depth + ######################################################################### + + ######################################################################### + ################## News Bulletins + ######################################################################### + + ######################################################################### + ################## Financial Advisors + ######################################################################### + def reqManagedAccts(self): + self._handle_task( + self.wrapper._client.process_managed_accounts, + accounts_list="DU1234567,", + ) + + ######################################################################### + ################## Historical Data + ######################################################################### + + ######################################################################### + ################## Market Scanners + ######################################################################### + + ######################################################################### + ################## Real Time Bars + ######################################################################### + + ######################################################################### + ################## Fundamental Data + ######################################################################### + + ######################################################################## + ################## News + ######################################################################### + + ######################################################################### + ################## Display Groups + ######################################################################### + + +class MockInteractiveBrokersClient(InteractiveBrokersClient): + """ + MockInteractiveBrokersClient is a subclass of InteractiveBrokersClient used for + simulating client operations. + + This class initializes the EClient with a mocked version for testing purposes. + + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._eclient = MockEClient( + wrapper=InteractiveBrokersEWrapper( + nautilus_logger=self._log, + client=self, + ), + ) + self._start = MagicMock() diff --git a/tests/integration_tests/adapters/interactive_brokers/test_execution_order_transform.py b/tests/integration_tests/adapters/interactive_brokers/test_execution_order_transform.py new file mode 100644 index 00000000000..3639c50c1e2 --- /dev/null +++ b/tests/integration_tests/adapters/interactive_brokers/test_execution_order_transform.py @@ -0,0 +1,91 @@ +# ------------------------------------------------------------------------------------------------- +# 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 pytest + +from nautilus_trader.model.enums import TimeInForce +from nautilus_trader.model.identifiers import Venue +from nautilus_trader.test_kit.stubs.data import TestInstrumentProvider +from nautilus_trader.test_kit.stubs.execution import TestExecStubs + + +_AAPL = TestInstrumentProvider.equity("AAPL", "NASDAQ") +_EURUSD = TestInstrumentProvider.default_fx_ccy("EUR/USD", Venue("IDEALPRO")) + + +@pytest.mark.parametrize( + "expected_order_type, expected_tif, nautilus_order", + [ + # fmt: off + ("MKT", "GTC", TestExecStubs.market_order(instrument=_EURUSD, time_in_force=TimeInForce.GTC)), + ("MKT", "DAY", TestExecStubs.market_order(instrument=_EURUSD, time_in_force=TimeInForce.DAY)), + ("MKT", "IOC", TestExecStubs.market_order(instrument=_EURUSD, time_in_force=TimeInForce.IOC)), + ("MKT", "FOK", TestExecStubs.market_order(instrument=_EURUSD, time_in_force=TimeInForce.FOK)), + ("MKT", "OPG", TestExecStubs.market_order(instrument=_EURUSD, time_in_force=TimeInForce.AT_THE_OPEN)), + ("MOC", "DAY", TestExecStubs.market_order(instrument=_EURUSD, time_in_force=TimeInForce.AT_THE_CLOSE)), + # fmt: on + ], +) +@pytest.mark.asyncio +async def test_transform_order_to_ib_order_market( + expected_order_type, + expected_tif, + nautilus_order, + exec_client, +): + # Arrange + await exec_client._instrument_provider.load_async(nautilus_order.instrument_id) + + # Act + ib_order = exec_client._transform_order_to_ib_order(nautilus_order) + + # Assert + assert ( + ib_order.orderType == expected_order_type + ), f"{expected_order_type=}, but got {ib_order.orderType=}" + assert ib_order.tif == expected_tif, f"{expected_tif=}, but got {ib_order.tif=}" + + +@pytest.mark.parametrize( + "expected_order_type, expected_tif, nautilus_order", + [ + # fmt: off + ("LMT", "GTC", TestExecStubs.limit_order(instrument=_EURUSD, time_in_force=TimeInForce.GTC)), + ("LMT", "DAY", TestExecStubs.limit_order(instrument=_EURUSD, time_in_force=TimeInForce.DAY)), + ("LMT", "IOC", TestExecStubs.limit_order(instrument=_EURUSD, time_in_force=TimeInForce.IOC)), + ("LMT", "FOK", TestExecStubs.limit_order(instrument=_EURUSD, time_in_force=TimeInForce.FOK)), + ("LMT", "OPG", TestExecStubs.limit_order(instrument=_EURUSD, time_in_force=TimeInForce.AT_THE_OPEN)), + ("LOC", "DAY", TestExecStubs.limit_order(instrument=_EURUSD, time_in_force=TimeInForce.AT_THE_CLOSE)), + # fmt: on + ], +) +@pytest.mark.asyncio +async def test_transform_order_to_ib_order_limit( + expected_order_type, + expected_tif, + nautilus_order, + exec_client, +): + # Arrange + await exec_client._instrument_provider.load_async(nautilus_order.instrument_id) + + # Act + ib_order = exec_client._transform_order_to_ib_order(nautilus_order) + + # Assert + assert ( + ib_order.orderType == expected_order_type + ), f"{expected_order_type=}, but got {ib_order.orderType=}" + assert ib_order.tif == expected_tif, f"{expected_tif=}, but got {ib_order.tif=}" diff --git a/tests/integration_tests/adapters/sandbox/conftest.py b/tests/integration_tests/adapters/sandbox/conftest.py index 8f2c1fc0eb2..b5103772218 100644 --- a/tests/integration_tests/adapters/sandbox/conftest.py +++ b/tests/integration_tests/adapters/sandbox/conftest.py @@ -40,7 +40,8 @@ def exec_client( clock, venue, ): - SandboxExecutionClient.INSTRUMENTS = [instrument] + cache.add_instrument(instrument) # <-- This might be redundant now + config = SandboxExecutionClientConfig( venue=venue.value, starting_balances=["100_000 USD"], diff --git a/tests/unit_tests/backtest/test_config.py b/tests/unit_tests/backtest/test_config.py index ceeb4dfdf18..cb39a83c6b0 100644 --- a/tests/unit_tests/backtest/test_config.py +++ b/tests/unit_tests/backtest/test_config.py @@ -285,7 +285,7 @@ def test_backtest_run_config_id(self) -> None: TestConfigStubs.backtest_engine_config, ("catalog",), {"persist": True}, - ("ec9a8febb3af4a4b3f7155b9a2c6f9e86597d97a3d62e568a01cd40565a2a7a3",), + ("882b85369baccfa23783b986b9b62a0cf396a9488d8e2e3057bc851c812ee6f6",), ), ( TestConfigStubs.risk_engine_config, diff --git a/tests/unit_tests/backtest/test_exchange_bitmex.py b/tests/unit_tests/backtest/test_exchange_bitmex.py index 71fc9f5ae9b..fe319b1d797 100644 --- a/tests/unit_tests/backtest/test_exchange_bitmex.py +++ b/tests/unit_tests/backtest/test_exchange_bitmex.py @@ -101,13 +101,13 @@ def setup(self): portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, - instruments=[XBTUSD_BITMEX], modules=[], fill_model=FillModel(), fee_model=MakerTakerFeeModel(), clock=self.clock, latency_model=LatencyModel(0), ) + self.exchange.add_instrument(XBTUSD_BITMEX) self.exec_client = BacktestExecClient( exchange=self.exchange, diff --git a/tests/unit_tests/backtest/test_exchange_bracket_if_touched_entries.py b/tests/unit_tests/backtest/test_exchange_bracket_if_touched_entries.py index d138e74983a..1a6f5872d50 100644 --- a/tests/unit_tests/backtest/test_exchange_bracket_if_touched_entries.py +++ b/tests/unit_tests/backtest/test_exchange_bracket_if_touched_entries.py @@ -105,7 +105,6 @@ def setup(self): starting_balances=[Money(200, ETH), Money(1_000_000, USDT)], default_leverage=Decimal(10), leverages={}, - instruments=[ETHUSDT_PERP_BINANCE], modules=[], fill_model=FillModel(), fee_model=MakerTakerFeeModel(), @@ -116,6 +115,7 @@ def setup(self): latency_model=LatencyModel(0), reject_stop_orders=False, ) + self.exchange.add_instrument(ETHUSDT_PERP_BINANCE) self.exec_client = BacktestExecClient( exchange=self.exchange, diff --git a/tests/unit_tests/backtest/test_exchange_cash.py b/tests/unit_tests/backtest/test_exchange_cash.py index c1174f82d0f..3541440064f 100644 --- a/tests/unit_tests/backtest/test_exchange_cash.py +++ b/tests/unit_tests/backtest/test_exchange_cash.py @@ -97,7 +97,6 @@ def setup(self) -> None: starting_balances=[Money(1_000_000, USD)], default_leverage=Decimal(0), leverages={}, - instruments=[_AAPL_XNAS], modules=[], fill_model=FillModel(), fee_model=MakerTakerFeeModel(), @@ -107,6 +106,7 @@ def setup(self) -> None: clock=self.clock, latency_model=LatencyModel(0), ) + self.exchange.add_instrument(_AAPL_XNAS) self.exec_client = BacktestExecClient( exchange=self.exchange, diff --git a/tests/unit_tests/backtest/test_exchange_contingencies.py b/tests/unit_tests/backtest/test_exchange_contingencies.py index de2ecb3ff48..202c516094a 100644 --- a/tests/unit_tests/backtest/test_exchange_contingencies.py +++ b/tests/unit_tests/backtest/test_exchange_contingencies.py @@ -94,7 +94,6 @@ def setup(self): starting_balances=[Money(200, ETH), Money(1_000_000, USDT)], default_leverage=Decimal(10), leverages={}, - instruments=[ETHUSDT_PERP_BINANCE], modules=[], fill_model=FillModel(), fee_model=MakerTakerFeeModel(), @@ -104,6 +103,7 @@ def setup(self): clock=self.clock, latency_model=LatencyModel(0), ) + self.exchange.add_instrument(ETHUSDT_PERP_BINANCE) self.exec_client = BacktestExecClient( exchange=self.exchange, diff --git a/tests/unit_tests/backtest/test_exchange_glbx.py b/tests/unit_tests/backtest/test_exchange_glbx.py index b9c0a4551ce..d4e85e4d521 100644 --- a/tests/unit_tests/backtest/test_exchange_glbx.py +++ b/tests/unit_tests/backtest/test_exchange_glbx.py @@ -95,7 +95,6 @@ def setup(self) -> None: starting_balances=[Money(1_000_000, USD)], default_leverage=Decimal(10), leverages={}, - instruments=[_ESH4_GLBX], modules=[], fill_model=FillModel(), fee_model=MakerTakerFeeModel(), @@ -105,6 +104,7 @@ def setup(self) -> None: clock=self.clock, latency_model=LatencyModel(0), ) + self.exchange.add_instrument(_ESH4_GLBX) self.exec_client = BacktestExecClient( exchange=self.exchange, diff --git a/tests/unit_tests/backtest/test_exchange_l2_mbp.py b/tests/unit_tests/backtest/test_exchange_l2_mbp.py index cc663db39c3..5ec7612e659 100644 --- a/tests/unit_tests/backtest/test_exchange_l2_mbp.py +++ b/tests/unit_tests/backtest/test_exchange_l2_mbp.py @@ -100,7 +100,6 @@ def setup(self): starting_balances=[Money(1_000_000, USD)], default_leverage=Decimal(50), leverages={}, - instruments=[_USDJPY_SIM], modules=[], fill_model=FillModel(), fee_model=MakerTakerFeeModel(), @@ -111,6 +110,7 @@ def setup(self): book_type=BookType.L2_MBP, # <-- L2 MBP book latency_model=LatencyModel(0), ) + self.exchange.add_instrument(_USDJPY_SIM) self.exec_client = BacktestExecClient( exchange=self.exchange, diff --git a/tests/unit_tests/backtest/test_exchange_margin.py b/tests/unit_tests/backtest/test_exchange_margin.py index 304753e8adc..493b02531f7 100644 --- a/tests/unit_tests/backtest/test_exchange_margin.py +++ b/tests/unit_tests/backtest/test_exchange_margin.py @@ -126,7 +126,6 @@ def setup(self) -> None: starting_balances=[Money(1_000_000, USD)], default_leverage=Decimal(50), leverages={_AUDUSD_SIM.id: Decimal(10)}, - instruments=[_USDJPY_SIM], modules=[], fill_model=FillModel(), fee_model=MakerTakerFeeModel(), @@ -136,6 +135,7 @@ def setup(self) -> None: clock=self.clock, latency_model=LatencyModel(0), ) + self.exchange.add_instrument(_USDJPY_SIM) self.exec_client = BacktestExecClient( exchange=self.exchange, @@ -2728,7 +2728,6 @@ def test_adjust_account_when_account_frozen_does_not_change_balance(self) -> Non starting_balances=[Money(1_000_000, USD)], default_leverage=Decimal(50), leverages={}, - instruments=[_USDJPY_SIM], modules=[], fill_model=FillModel(), fee_model=MakerTakerFeeModel(), @@ -2738,6 +2737,7 @@ def test_adjust_account_when_account_frozen_does_not_change_balance(self) -> Non clock=self.clock, frozen_account=True, # <-- Freezing account ) + exchange.add_instrument(_USDJPY_SIM) exchange.register_client(self.exec_client) exchange.reset() @@ -3027,7 +3027,6 @@ def reset(self): starting_balances=[Money(1_000_000, USD)], default_leverage=Decimal(50), leverages={_AUDUSD_SIM.id: Decimal(10)}, - instruments=[_USDJPY_SIM], modules=[self.module], fill_model=FillModel(), fee_model=MakerTakerFeeModel(), @@ -3038,6 +3037,7 @@ def reset(self): latency_model=LatencyModel(0), book_type=BookType.L1_MBP, ) + self.exchange.add_instrument(_USDJPY_SIM) self.exec_client = BacktestExecClient( exchange=self.exchange, diff --git a/tests/unit_tests/backtest/test_exchange_stop_limits.py b/tests/unit_tests/backtest/test_exchange_stop_limits.py index 843b65ba675..be063b8187b 100644 --- a/tests/unit_tests/backtest/test_exchange_stop_limits.py +++ b/tests/unit_tests/backtest/test_exchange_stop_limits.py @@ -98,7 +98,6 @@ def setup(self): starting_balances=[Money(1_000_000, USD)], default_leverage=Decimal(50), leverages={AUDUSD_SIM.id: Decimal(10)}, - instruments=[USDJPY_SIM], modules=[], fill_model=FillModel(), fee_model=MakerTakerFeeModel(), @@ -109,6 +108,7 @@ def setup(self): latency_model=LatencyModel(0), reject_stop_orders=False, ) + self.exchange.add_instrument(USDJPY_SIM) self.exec_client = BacktestExecClient( exchange=self.exchange, diff --git a/tests/unit_tests/backtest/test_exchange_trailing_stops.py b/tests/unit_tests/backtest/test_exchange_trailing_stops.py index b3d4b904562..73af954a713 100644 --- a/tests/unit_tests/backtest/test_exchange_trailing_stops.py +++ b/tests/unit_tests/backtest/test_exchange_trailing_stops.py @@ -107,7 +107,6 @@ def setup(self) -> None: ], default_leverage=Decimal(50), leverages={AUDUSD_SIM.id: Decimal(10)}, - instruments=[USDJPY_SIM], modules=[], fill_model=FillModel(), fee_model=MakerTakerFeeModel(), @@ -117,6 +116,7 @@ def setup(self) -> None: clock=self.clock, latency_model=LatencyModel(0), ) + self.exchange.add_instrument(USDJPY_SIM) self.exec_client = BacktestExecClient( exchange=self.exchange, diff --git a/tests/unit_tests/core/test_correctness.py b/tests/unit_tests/core/test_correctness.py index 911654eff04..1242ef27cdd 100644 --- a/tests/unit_tests/core/test_correctness.py +++ b/tests/unit_tests/core/test_correctness.py @@ -274,7 +274,10 @@ def test_positive_int_when_args_positive_does_nothing(self): @pytest.mark.parametrize( ("value", "start", "end"), - [[-1e-22, 0.0, 1.0], [1.0000001, 0.0, 1.0]], + [ + [-1e16, 0.0, 1.0], + [1 + 1e16, 0.0, 1.0], + ], ) def test_in_range_when_arg_out_of_range_raises_value_error(self, value, start, end): # Arrange, Act, Assert diff --git a/tests/unit_tests/data/test_engine.py b/tests/unit_tests/data/test_engine.py index ebbf308d605..42c314b8308 100644 --- a/tests/unit_tests/data/test_engine.py +++ b/tests/unit_tests/data/test_engine.py @@ -41,6 +41,7 @@ from nautilus_trader.model.enums import BarAggregation from nautilus_trader.model.enums import BookType from nautilus_trader.model.enums import PriceType +from nautilus_trader.model.enums import RecordFlag from nautilus_trader.model.identifiers import ClientId from nautilus_trader.model.identifiers import InstrumentId from nautilus_trader.model.identifiers import Symbol @@ -1035,6 +1036,43 @@ def test_process_order_book_snapshot_when_one_subscriber_then_sends_to_registere # Assert assert isinstance(handler[0], OrderBook) + def test_process_order_book_delta_then_sends_to_registered_handler(self): + # Arrange + self.data_engine.register_client(self.binance_client) + self.binance_client.start() + + self.data_engine.process(ETHUSDT_BINANCE) # <-- add necessary instrument for test + + handler = [] + self.msgbus.subscribe(topic="data.book.deltas.BINANCE.ETHUSDT", handler=handler.append) + + subscribe = Subscribe( + client_id=ClientId(BINANCE.value), + venue=BINANCE, + data_type=DataType( + OrderBookDelta, + { + "instrument_id": ETHUSDT_BINANCE.id, + "book_type": BookType.L3_MBO, + "depth": 5, + "managed": True, + }, + ), + command_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + self.data_engine.execute(subscribe) + + deltas = TestDataStubs.order_book_delta(ETHUSDT_BINANCE.id) + + # Act + self.data_engine.process(deltas) + + # Assert + assert handler[0].instrument_id == ETHUSDT_BINANCE.id + assert isinstance(handler[0], OrderBookDeltas) + def test_process_order_book_deltas_then_sends_to_registered_handler(self): # Arrange self.data_engine.register_client(self.binance_client) @@ -2376,3 +2414,232 @@ def test_request_instruments_for_venue_when_catalog_registered(self): # assert len(handler[0].data) == 21 # assert handler[0].data[0].ts_init == 1637971200000000000 # assert handler[0].data[-1].ts_init == 1638058200000000000 + + +class TestDataBufferEngine: + def setup(self): + # Fixture Setup + self.clock = TestClock() + self.trader_id = TestIdStubs.trader_id() + + self.msgbus = MessageBus( + trader_id=self.trader_id, + clock=self.clock, + ) + + self.cache = TestComponentStubs.cache() + + self.portfolio = Portfolio( + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + ) + + config = DataEngineConfig( + validate_data_sequence=True, + debug=True, + buffer_deltas=True, + ) + self.data_engine = DataEngine( + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + config=config, + ) + + self.binance_client = BacktestMarketDataClient( + client_id=ClientId(BINANCE.value), + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + ) + + self.bitmex_client = BacktestMarketDataClient( + client_id=ClientId(BITMEX.value), + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + ) + + self.quandl = BacktestMarketDataClient( + client_id=ClientId("QUANDL"), + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + ) + + self.betfair = BacktestMarketDataClient( + client_id=ClientId("BETFAIR"), + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + ) + + self.data_engine.process(BTCUSDT_BINANCE) + self.data_engine.process(ETHUSDT_BINANCE) + self.data_engine.process(XBTUSD_BITMEX) + + def test_process_order_book_delta_buffering_then_sends_to_registered_handler(self): + # Arrange + self.data_engine.register_client(self.binance_client) + self.binance_client.start() + + self.data_engine.process(ETHUSDT_BINANCE) # <-- add necessary instrument for test + + handler = [] + self.msgbus.subscribe(topic="data.book.deltas.BINANCE.ETHUSDT", handler=handler.append) + + subscribe = Subscribe( + client_id=ClientId(BINANCE.value), + venue=BINANCE, + data_type=DataType( + OrderBookDelta, + { + "instrument_id": ETHUSDT_BINANCE.id, + "book_type": BookType.L3_MBO, + "depth": 5, + "managed": True, + }, + ), + command_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + self.data_engine.execute(subscribe) + + delta = TestDataStubs.order_book_delta(ETHUSDT_BINANCE.id) + last_delta = TestDataStubs.order_book_delta(ETHUSDT_BINANCE.id, flags=RecordFlag.F_LAST) + + self.data_engine.process(delta) + + assert handler == [] + + # Act + self.data_engine.process(last_delta) + + # Assert + assert handler[0].instrument_id == ETHUSDT_BINANCE.id + assert isinstance(handler[0], OrderBookDeltas) + assert len(handler) == 1 + assert handler[0].deltas == [delta, last_delta] + + def test_process_order_book_delta_buffers_are_cleared(self): + # Arrange + self.data_engine.register_client(self.binance_client) + self.binance_client.start() + + self.data_engine.process(ETHUSDT_BINANCE) # <-- add necessary instrument for test + + handler = [] + self.msgbus.subscribe(topic="data.book.deltas.BINANCE.ETHUSDT", handler=handler.append) + + subscribe = Subscribe( + client_id=ClientId(BINANCE.value), + venue=BINANCE, + data_type=DataType( + OrderBookDelta, + { + "instrument_id": ETHUSDT_BINANCE.id, + "book_type": BookType.L3_MBO, + "depth": 5, + "managed": True, + }, + ), + command_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + self.data_engine.execute(subscribe) + + delta = TestDataStubs.order_book_delta(ETHUSDT_BINANCE.id, flags=RecordFlag.F_LAST) + + # Act + self.data_engine.process(delta) + self.data_engine.process(delta) + + # Assert + assert len(handler) == 2 + assert len(handler[0].deltas) == 1 + assert len(handler[1].deltas) == 1 + + def test_process_order_book_deltas_then_sends_to_registered_handler(self): + # Arrange + self.data_engine.register_client(self.binance_client) + self.binance_client.start() + + self.data_engine.process(ETHUSDT_BINANCE) # <-- add necessary instrument for test + + handler = [] + self.msgbus.subscribe(topic="data.book.deltas.BINANCE.ETHUSDT", handler=handler.append) + + subscribe = Subscribe( + client_id=ClientId(BINANCE.value), + venue=BINANCE, + data_type=DataType( + OrderBookDelta, + { + "instrument_id": ETHUSDT_BINANCE.id, + "book_type": BookType.L3_MBO, + "depth": 5, + "managed": True, + }, + ), + command_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + self.data_engine.execute(subscribe) + + deltas = TestDataStubs.order_book_deltas(ETHUSDT_BINANCE.id) + last_deltas = TestDataStubs.order_book_deltas(ETHUSDT_BINANCE.id, flags=RecordFlag.F_LAST) + + self.data_engine.process(deltas) + + assert handler == [] + + # Act + self.data_engine.process(last_deltas) + + # Assert + assert handler[0].instrument_id == ETHUSDT_BINANCE.id + assert isinstance(handler[0], OrderBookDeltas) + assert len(handler[0].deltas) == 2 + + def test_process_order_book_deltas_buffers_are_cleared(self): + # Arrange + self.data_engine.register_client(self.binance_client) + self.binance_client.start() + + self.data_engine.process(ETHUSDT_BINANCE) # <-- add necessary instrument for test + + handler = [] + self.msgbus.subscribe(topic="data.book.deltas.BINANCE.ETHUSDT", handler=handler.append) + + subscribe = Subscribe( + client_id=ClientId(BINANCE.value), + venue=BINANCE, + data_type=DataType( + OrderBookDelta, + { + "instrument_id": ETHUSDT_BINANCE.id, + "book_type": BookType.L3_MBO, + "depth": 5, + "managed": True, + }, + ), + command_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + self.data_engine.execute(subscribe) + + deltas = TestDataStubs.order_book_deltas(ETHUSDT_BINANCE.id, flags=RecordFlag.F_LAST) + + # Act + self.data_engine.process(deltas) + self.data_engine.process(deltas) + + # Assert + assert len(handler) == 2 + assert len(handler[0].deltas) == 1 + assert len(handler[1].deltas) == 1 diff --git a/tests/unit_tests/execution/test_algorithm.py b/tests/unit_tests/execution/test_algorithm.py index 1101c440d24..c04d137cfa9 100644 --- a/tests/unit_tests/execution/test_algorithm.py +++ b/tests/unit_tests/execution/test_algorithm.py @@ -130,7 +130,6 @@ def setup(self) -> None: starting_balances=[Money(200, ETH), Money(1_000_000, USDT)], default_leverage=Decimal(10), leverages={}, - instruments=[ETHUSDT_PERP_BINANCE], modules=[], fill_model=FillModel(), fee_model=MakerTakerFeeModel(), @@ -139,6 +138,7 @@ def setup(self) -> None: cache=self.cache, clock=self.clock, ) + self.exchange.add_instrument(ETHUSDT_PERP_BINANCE) self.exec_client = BacktestExecClient( exchange=self.exchange, diff --git a/tests/unit_tests/execution/test_emulator_list.py b/tests/unit_tests/execution/test_emulator_list.py index 660ab4677f1..8c60b214eb0 100644 --- a/tests/unit_tests/execution/test_emulator_list.py +++ b/tests/unit_tests/execution/test_emulator_list.py @@ -129,7 +129,6 @@ def setup(self) -> None: starting_balances=[Money(200, ETH), Money(1_000_000, USDT)], default_leverage=Decimal(10), leverages={}, - instruments=[ETHUSDT_PERP_BINANCE], modules=[], fill_model=FillModel(), fee_model=MakerTakerFeeModel(), @@ -139,6 +138,7 @@ def setup(self) -> None: clock=self.clock, support_contingent_orders=False, ) + self.exchange.add_instrument(ETHUSDT_PERP_BINANCE) self.exec_client = BacktestExecClient( exchange=self.exchange, diff --git a/tests/unit_tests/model/test_instrument.py b/tests/unit_tests/model/test_instrument.py index f62b935a099..bf91a7e3991 100644 --- a/tests/unit_tests/model/test_instrument.py +++ b/tests/unit_tests/model/test_instrument.py @@ -443,6 +443,28 @@ def test_calculate_base_quantity_audusd(self): # Assert assert result == Quantity.from_str("1250") + def test_next_bid_price_when_no_tick_scheme(self): + # Arrange, Act + with pytest.raises(ValueError) as exc_info: + BTCUSDT_BINANCE.next_bid_price(100_000.0) + + # Assert + assert ( + str(exc_info.value) + == "No tick scheme for instrument BTCUSDT.BINANCE. You can specify a tick scheme by passing a `tick_scheme_name` at initialization." + ) + + def test_next_ask_price_when_no_tick_scheme(self): + # Arrange, Act + with pytest.raises(ValueError) as exc_info: + BTCUSDT_BINANCE.next_ask_price(100_000.0) + + # Assert + assert ( + str(exc_info.value) + == "No tick scheme for instrument BTCUSDT.BINANCE. You can specify a tick scheme by passing a `tick_scheme_name` at initialization." + ) + @pytest.mark.parametrize( ("instrument", "value", "n", "expected"), [ diff --git a/tests/unit_tests/trading/test_strategy.py b/tests/unit_tests/trading/test_strategy.py index b3a07d1c795..a6c844b646c 100644 --- a/tests/unit_tests/trading/test_strategy.py +++ b/tests/unit_tests/trading/test_strategy.py @@ -124,7 +124,6 @@ def setup(self) -> None: portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, - instruments=[_USDJPY_SIM], modules=[], fill_model=FillModel(), fee_model=MakerTakerFeeModel(), @@ -133,6 +132,7 @@ def setup(self) -> None: support_contingent_orders=False, use_reduce_only=False, ) + self.exchange.add_instrument(_USDJPY_SIM) self.data_client = BacktestMarketDataClient( client_id=ClientId("SIM"), diff --git a/tests/unit_tests/trading/test_trader.py b/tests/unit_tests/trading/test_trader.py index 919a1948d4e..2fb90b387a5 100644 --- a/tests/unit_tests/trading/test_trader.py +++ b/tests/unit_tests/trading/test_trader.py @@ -101,12 +101,12 @@ def setup(self) -> None: portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, - instruments=[USDJPY_SIM], modules=[], fill_model=FillModel(), fee_model=MakerTakerFeeModel(), clock=self.clock, ) + self.exchange.add_instrument(USDJPY_SIM) self.data_client = BacktestMarketDataClient( client_id=ClientId("SIM"), diff --git a/version.json b/version.json index 43ccf52343e..96ed80e58ff 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, "label": "", - "message": "v1.193.0", + "message": "v1.194.0", "color": "orange" }